Files
kjvstudy.org/kjvstudy_org/routes/study_guides.py
T
kennethreitz df67749532 Migrate hardcoded data to JSON files for better maintainability
This commit completes a comprehensive data migration, externalizing all hardcoded
theological content from Python modules to structured JSON files in the data/ directory.

## Changes

### New JSON Data Files (3 files, 356 KB total)
- **study_guides.json** (265 KB): 25 complete study guides with 183 sections and 732 verse references
  - 6 categories: Foundational, Character & Living, Biblical Themes, Doctrinal, Thematic, Family
- **word_studies.json** (35 KB): 53 biblical terms with Hebrew/Greek definitions
  - Includes transliterations, meanings, and theological notes for OT and NT
- **verse_commentary.json** (56 KB): 22 verses with detailed theological analysis
  - Includes analysis, historical context, applications, and reflection questions

### Code Refactoring

**routes/study_guides.py** (1,248 lines removed, 85% reduction)
- Replaced 1,070 lines of hardcoded study guide content with JSON loader
- File reduced from 1,463 lines to 215 lines
- Added @lru_cache JSON loaders for performance
- Removed `_get_study_guides_catalog_old()` and `_get_study_guides_content_old()`

**routes/commentary.py** (496 lines removed, 11.7% reduction)
- Replaced 494 lines of hardcoded word studies and verse commentary
- File reduced from 4,222 lines to 3,726 lines
- Added structured JSON loaders with data transformation:
  - `_load_word_studies()`: Converts flat JSON to nested OT/NT structure
  - `_load_verse_commentary()`: Parses verse references into book/chapter/verse hierarchy
- Removed hardcoded `word_studies` (226 lines) and `enhanced_commentary` (268 lines) dictionaries

## Benefits
- **Maintainability**: Non-developers can now edit theological content in JSON
- **Performance**: @lru_cache ensures data loaded once per process
- **Separation of Concerns**: Content separated from application logic
- **Version Control**: Easier to track content changes in structured format
- **Scalability**: Can add new study guides/commentary without code changes

## Testing
- All 252 tests pass
- Verified data structure compatibility
- Confirmed JSON loaders work correctly with existing templates

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-27 18:10:40 -05:00

216 lines
6.9 KiB
Python

"""Study guides routes for KJV Study.
This module contains the study guides routes and content.
"""
import json
from pathlib import Path
from functools import lru_cache
from fastapi import APIRouter, Request, HTTPException
from fastapi.responses import HTMLResponse, StreamingResponse
from ..kjv import bible
from ..utils.pdf import WEASYPRINT_AVAILABLE, render_html_to_pdf, render_html_to_pdf_async
router = APIRouter(tags=["Study Guides"])
# Templates will be set by the main app
templates = None
# Path to study guides JSON file
_STUDY_GUIDES_PATH = Path(__file__).parent.parent / "data" / "study_guides.json"
@lru_cache(maxsize=1)
def _load_study_guides():
"""Load study guides from JSON file. Cached since data never changes."""
with open(_STUDY_GUIDES_PATH, "r", encoding="utf-8") as f:
return json.load(f)
def init_templates(app_templates):
"""Initialize templates from the main app."""
global templates
templates = app_templates
def get_books():
"""Get list of Bible books."""
return bible.get_books()
def verse_reference_to_url(reference: str):
"""Convert a verse reference like 'John 3:16' to a URL."""
import re
# Pattern to parse verse references with optional chapter:verse-verse format
pattern = r'^(?:(\d)\s+)?([A-Za-z]+(?:\s+of\s+[A-Za-z]+)?)\s+(\d+):(\d+)(?:-(\d+))?$'
match = re.match(pattern, reference.strip())
if not match:
return None
number_prefix = match.group(1)
book = match.group(2)
chapter = match.group(3)
verse_start = match.group(4)
verse_end = match.group(5)
if number_prefix:
book = f"{number_prefix} {book}"
if verse_end:
return f"/book/{book}/chapter/{chapter}#verse-{verse_start}-{verse_end}"
else:
return f"/book/{book}/chapter/{chapter}/verse/{verse_start}"
def _get_study_guides_catalog():
"""Return the study guide catalog grouped by category."""
data = _load_study_guides()
return data["catalog"]
def _get_study_guides_content():
"""Return full study guide detail content."""
data = _load_study_guides()
return data["content"]
def _attach_verse_texts(guide: dict):
"""Populate verse text data for each section in a guide."""
for section in guide.get("sections", []):
verse_texts = []
for verse_ref in section.get("verses", []):
try:
verse_text = None
parts = verse_ref.split(" ")
if len(parts) >= 2:
book = " ".join(parts[:-1])
chapter_verse = parts[-1]
if ":" in chapter_verse:
if "-" in chapter_verse:
chapter, verse_range = chapter_verse.split(":")
start_verse, end_verse = verse_range.split("-")
verse_text = ""
for v in range(int(start_verse), int(end_verse) + 1):
text = bible.get_verse_text(book, int(chapter), v)
if text:
verse_text += f"[{v}] {text} "
else:
chapter, verse = chapter_verse.split(":")
verse_text = bible.get_verse_text(book, int(chapter), int(verse))
else:
chapter = int(chapter_verse)
verse_text = f"(See {book} {chapter})"
if verse_text:
verse_texts.append({
"reference": verse_ref,
"text": verse_text,
"url": verse_reference_to_url(verse_ref) or "#"
})
else:
verse_texts.append({
"reference": verse_ref,
"text": f"(See {verse_ref})",
"url": verse_reference_to_url(verse_ref) or "#"
})
except Exception as exc:
print(f"Error parsing verse {verse_ref}: {exc}")
verse_texts.append({
"reference": verse_ref,
"text": f"(See {verse_ref})",
"url": "#"
})
section["verse_texts"] = verse_texts
@router.get("/study-guides", response_class=HTMLResponse)
def study_guides_page(request: Request):
"""Study guides main page"""
books = get_books()
# Define study guide categories
study_guides = _get_study_guides_catalog()
# Process verse references to add URLs
for category in study_guides.values():
for guide in category:
guide['verse_refs'] = [
{
'text': verse,
'url': verse_reference_to_url(verse) or '#'
}
for verse in guide['verses']
]
return templates.TemplateResponse(
request,
"study_guides.html",
{
"books": books,
"study_guides": study_guides
}
)
@router.get("/study-guides/{slug}", response_class=HTMLResponse)
def study_guide_detail(request: Request, slug: str):
"""Individual study guide page"""
books = get_books()
# Study guide content
guides_content = _get_study_guides_content()
if slug not in guides_content:
raise HTTPException(status_code=404, detail="Study guide not found")
guide = guides_content[slug]
_attach_verse_texts(guide)
# Build breadcrumbs
breadcrumbs = [
{"text": "Home", "url": "/"},
{"text": "Study Guides", "url": "/study-guides"},
{"text": guide["title"], "url": None}
]
return templates.TemplateResponse(
request,
"study_guide_detail.html",
{
"books": books,
"guide": guide,
"breadcrumbs": breadcrumbs,
"pdf_available": WEASYPRINT_AVAILABLE,
"pdf_url": f"/study-guides/{slug}/pdf" if WEASYPRINT_AVAILABLE else None
}
)
@router.get("/study-guides/{slug}/pdf")
async def study_guide_pdf(slug: str):
"""Generate a PDF export for a study guide."""
if not WEASYPRINT_AVAILABLE:
raise HTTPException(
status_code=503,
detail="PDF generation is not available. WeasyPrint system libraries are not installed."
)
guides_content = _get_study_guides_content()
guide = guides_content.get(slug)
if not guide:
raise HTTPException(status_code=404, detail="Study guide not found")
_attach_verse_texts(guide)
html_content = templates.get_template("study_guide_pdf.html").render(guide=guide)
pdf_buffer = await render_html_to_pdf_async(html_content)
filename = f"{slug}.pdf"
return StreamingResponse(
pdf_buffer,
media_type="application/pdf",
headers={"Content-Disposition": f"attachment; filename={filename}"}
)