Refactor server.py: extract routes into modular route files

Move routes from monolithic server.py (2716 lines) to dedicated route modules (788 lines remaining):
- routes/main.py: Homepage, books browser, resources page
- routes/misc.py: Search, interlinear landing, random-verse, verse-of-the-day
- routes/timeline.py: Biblical timeline page and PDF
- routes/about.py: Stats, cross-references index, about page
- routes/reading_plans.py: Reading plans index, detail, PDF
- routes/topics.py: Topics index and detail
- routes/strongs.py: Strong's concordance search and entries

This reduces server.py by 71% and improves code organization.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-02 17:00:44 -05:00
parent 2a2e8ebc08
commit d200030361
9 changed files with 1738 additions and 1971 deletions
+16
View File
@@ -8,6 +8,14 @@ from .study_guides import router as study_guides_router, init_templates as init_
from .commentary import router as commentary_router, init_templates as init_commentary_templates
from .stories import router as stories_router, init_templates as init_stories_templates
from .utility import router as utility_router
from .bible import router as bible_router, init_bible_templates, init_commentary_functions as init_bible_commentary
from .reading_plans import router as reading_plans_router, init_templates as init_reading_plans_templates
from .topics import router as topics_router, init_templates as init_topics_templates
from .strongs import router as strongs_router, init_templates as init_strongs_templates
from .timeline import router as timeline_router, init_templates as init_timeline_templates
from .about import router as about_router, init_templates as init_about_templates
from .main import router as main_router, init_templates as init_main_templates
from .misc import router as misc_router, init_templates as init_misc_templates, init_search_family_tree
__all__ = [
'api_router', 'init_api_templates',
@@ -17,4 +25,12 @@ __all__ = [
'commentary_router', 'init_commentary_templates',
'stories_router', 'init_stories_templates',
'utility_router',
'bible_router', 'init_bible_templates', 'init_bible_commentary',
'reading_plans_router', 'init_reading_plans_templates',
'topics_router', 'init_topics_templates',
'strongs_router', 'init_strongs_templates',
'timeline_router', 'init_timeline_templates',
'about_router', 'init_about_templates',
'main_router', 'init_main_templates',
'misc_router', 'init_misc_templates', 'init_search_family_tree',
]
+269
View File
@@ -0,0 +1,269 @@
"""About routes - stats, cross-references index, and about page."""
import json
import re
from collections import defaultdict
from pathlib import Path
from fastapi import APIRouter, Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from ..kjv import bible
from ..utils.books import OT_BOOKS, NT_BOOKS
router = APIRouter()
templates = None
def init_templates(t: Jinja2Templates):
"""Initialize templates for about routes."""
global templates
templates = t
# =============================================================================
# Routes
# =============================================================================
@router.get("/about/stats", response_class=HTMLResponse)
async def stats(request: Request):
"""Hidden statistics page - comprehensive site metrics"""
data_dir = Path(__file__).parent.parent / "data"
# Bible statistics
total_verses = bible.get_verse_count()
total_books = len(bible.get_books())
total_chapters = len(bible.get_chapters())
# Calculate words in Bible
total_words = sum(len(verse.text.split()) for verse in bible.iter_verses())
# Count unique book types
ot_books = len(OT_BOOKS)
nt_books = len(NT_BOOKS)
# Data file statistics
total_json_files = len(list(data_dir.glob('**/*.json')))
# Verse commentary statistics
verse_commentary_files = len(list((data_dir / 'verse_commentary').glob('*.json')))
total_commentary_verses = 0
total_commentary_words = 0
for file in (data_dir / 'verse_commentary').glob('*.json'):
data = json.load(open(file))
commentary = data.get('commentary', {})
for chapter in commentary.values():
for verse_data in chapter.values():
total_commentary_verses += 1
# Count words in analysis + historical
analysis = verse_data.get('analysis', '')
historical = verse_data.get('historical', '')
# Strip HTML tags for accurate word count
clean_analysis = re.sub(r'<[^>]+>', '', analysis)
clean_historical = re.sub(r'<[^>]+>', '', historical)
total_commentary_words += len(clean_analysis.split()) + len(clean_historical.split())
# Cross-reference statistics
cross_reference_files = len(list((data_dir / 'cross_references').glob('*.json')))
total_cross_refs = 0
verses_with_cross_refs = 0
for file in (data_dir / 'cross_references').glob('*.json'):
data = json.load(open(file))
verses_with_cross_refs += len(data)
for verse_refs in data.values():
total_cross_refs += len(verse_refs)
# Red letter statistics
red_letter_data = json.load(open(data_dir / 'red_letter_verses.json'))
total_red_letter_verses = len(red_letter_data['verses'])
# Study resources
study_guide_files = len(list((data_dir / 'study_guides').glob('*.json')))
topic_files = len(list((data_dir / 'topics').glob('*.json')))
resource_files = len(list((data_dir / 'resources').glob('*.json')))
story_files = len(list((data_dir / 'stories').glob('*.json')))
# Interlinear data size
interlinear_file = data_dir / 'interlinear.json.gz'
interlinear_size_mb = interlinear_file.stat().st_size / 1024 / 1024 if interlinear_file.exists() else 0
# Calculate total data directory size
total_data_size = sum(f.stat().st_size for f in data_dir.glob('**/*') if f.is_file())
total_data_size_mb = total_data_size / 1024 / 1024
# Book abbreviations
bible_metadata_file = data_dir / 'bible_metadata.json'
bible_metadata = json.load(open(bible_metadata_file))
total_abbreviations = len(bible_metadata.get('book_abbreviations', {}))
# Biographies
bio_data = json.load(open(data_dir / 'biographies.json'))
total_biographies = len(bio_data.get('biographies', {}))
# Reading plans
reading_plan_files = len(list((data_dir / 'reading_plans').glob('*.json')))
# Strong's concordance
strongs_dir = data_dir / 'strongs'
if strongs_dir.exists():
hebrew_data = json.load(open(strongs_dir / 'hebrew.json'))
greek_data = json.load(open(strongs_dir / 'greek.json'))
total_hebrew_entries = len(hebrew_data)
total_greek_entries = len(greek_data)
else:
total_hebrew_entries = 0
total_greek_entries = 0
stats_data = {
'bible': {
'total_verses': total_verses,
'total_books': total_books,
'ot_books': ot_books,
'nt_books': nt_books,
'total_chapters': total_chapters,
'total_words': total_words,
'avg_words_per_verse': round(total_words / total_verses, 1),
'avg_verses_per_chapter': round(total_verses / total_chapters, 1),
},
'commentary': {
'files': verse_commentary_files,
'verses_covered': total_commentary_verses,
'total_words': total_commentary_words,
'avg_words_per_verse': round(total_commentary_words / total_commentary_verses, 1) if total_commentary_verses > 0 else 0,
'coverage_percent': round((total_commentary_verses / total_verses) * 100, 1),
},
'cross_references': {
'files': cross_reference_files,
'verses_with_refs': verses_with_cross_refs,
'total_references': total_cross_refs,
'avg_refs_per_verse': round(total_cross_refs / verses_with_cross_refs, 1) if verses_with_cross_refs > 0 else 0,
'coverage_percent': round((verses_with_cross_refs / total_verses) * 100, 1),
},
'red_letter': {
'total_verses': total_red_letter_verses,
'percent_of_bible': round((total_red_letter_verses / total_verses) * 100, 1),
},
'study_resources': {
'study_guides': study_guide_files,
'topics': topic_files,
'resources': resource_files,
'stories': story_files,
'biographies': total_biographies,
'reading_plans': reading_plan_files,
},
'language_tools': {
'hebrew_entries': total_hebrew_entries,
'greek_entries': total_greek_entries,
'total_strongs': total_hebrew_entries + total_greek_entries,
'interlinear_size_mb': round(interlinear_size_mb, 1),
},
'data': {
'total_json_files': total_json_files,
'total_size_mb': round(total_data_size_mb, 1),
'book_abbreviations': total_abbreviations,
}
}
books = bible.get_books()
breadcrumbs = [
{"text": "Home", "url": "/"},
{"text": "About", "url": "/about"},
{"text": "Statistics", "url": None}
]
return templates.TemplateResponse(
"stats.html",
{
"request": request,
"books": books,
"stats": stats_data,
"breadcrumbs": breadcrumbs,
}
)
@router.get("/about/cross-references", response_class=HTMLResponse)
async def cross_references_index(request: Request):
"""Cross-references index - list all verses with cross-references"""
data_dir = Path(__file__).parent.parent / "data" / "cross_references"
# Build index of all verses with cross-references, grouped by book
crossref_index = defaultdict(lambda: defaultdict(list))
for file in sorted(data_dir.glob('*.json')):
with open(file, 'r') as f:
data = json.load(f)
for verse_key, refs in data.items():
# Parse verse key: "Book:Chapter:Verse"
parts = verse_key.split(':')
if len(parts) == 3:
book, chapter, verse = parts
crossref_index[book][int(chapter)].append({
'verse': int(verse),
'ref_count': len(refs)
})
# Sort books in biblical order (OT then NT)
biblical_order = OT_BOOKS + NT_BOOKS
book_order = {book: i for i, book in enumerate(biblical_order)}
# Convert to regular dict and sort
crossref_index = {
book: {
chapter: sorted(verses, key=lambda x: x['verse'])
for chapter, verses in sorted(chapters.items())
}
for book, chapters in sorted(crossref_index.items(), key=lambda x: book_order.get(x[0], 999))
}
# Calculate statistics
total_books = len(crossref_index)
total_verses = sum(
len(verses)
for chapters in crossref_index.values()
for verses in chapters.values()
)
total_refs = sum(
sum(v['ref_count'] for v in verses)
for chapters in crossref_index.values()
for verses in chapters.values()
)
books = bible.get_books()
breadcrumbs = [
{"text": "Home", "url": "/"},
{"text": "About", "url": "/about"},
{"text": "Cross-References Index", "url": None}
]
return templates.TemplateResponse(
"cross_references_index.html",
{
"request": request,
"books": books,
"crossref_index": crossref_index,
"total_books": total_books,
"total_verses": total_verses,
"total_refs": total_refs,
"breadcrumbs": breadcrumbs,
}
)
@router.get("/about", response_class=HTMLResponse)
async def about(request: Request):
"""About page - site information, creator, data sources, theological approach"""
books = bible.get_books()
breadcrumbs = [
{"text": "Home", "url": "/"},
{"text": "About", "url": None}
]
return templates.TemplateResponse(
"about.html",
{
"request": request,
"books": books,
"breadcrumbs": breadcrumbs,
}
)
+624
View File
@@ -0,0 +1,624 @@
"""Main page routes - homepage, books browser, and resources."""
import hashlib
import re
from datetime import datetime
from fastapi import APIRouter, Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from ..kjv import bible
router = APIRouter()
templates = None
def init_templates(t: Jinja2Templates):
"""Initialize templates for main routes."""
global templates
templates = t
# =============================================================================
# Helper Functions
# =============================================================================
def verse_reference_to_url(reference: str):
"""Convert a verse reference to a URL path.
Examples:
"John 3:16" -> "/book/John/chapter/3#verse-16"
"Romans 8:38-39" -> "/book/Romans/chapter/8#verse-38-39"
"Ephesians 2:8-9" -> "/book/Ephesians/chapter/2#verse-8-9"
"""
# Pattern: Book Chapter:Verse or Book Chapter:Verse-Verse
match = re.match(r'^(.+?)\s+(\d+):(\d+)(?:-(\d+))?$', reference.strip())
if not match:
return None
book = match.group(1).strip()
chapter = match.group(2)
verse_start = match.group(3)
verse_end = match.group(4)
if verse_end:
# Verse range - link to chapter with anchor
return f"/book/{book}/chapter/{chapter}#verse-{verse_start}-{verse_end}"
else:
# Single verse - link to chapter with anchor
return f"/book/{book}/chapter/{chapter}#verse-{verse_start}"
def get_daily_verse(date_str=None):
"""Get the verse of the day based on a specific date (or current date if not provided)"""
# Use date as seed for consistent daily verse
if date_str is None:
date_str = datetime.now().strftime("%Y-%m-%d")
seed = int(hashlib.md5(date_str.encode()).hexdigest(), 16) % 1000000
# Featured verses for rotation
featured_verses = [
("John", 3, 16),
("Jeremiah", 29, 11),
("Philippians", 4, 13),
("Romans", 8, 28),
("Proverbs", 3, 5),
("Isaiah", 41, 10),
("Matthew", 11, 28),
("1 John", 4, 19),
("Psalms", 23, 1),
("2 Corinthians", 5, 17),
("Ephesians", 2, 8),
("Romans", 10, 9),
("1 Peter", 5, 7),
("James", 1, 5),
("Philippians", 4, 19),
("Psalms", 119, 105),
("Matthew", 6, 33),
("Romans", 12, 2),
("1 Corinthians", 13, 13),
("Galatians", 5, 22),
("Hebrews", 11, 1),
("1 Thessalonians", 5, 18),
("Psalms", 46, 1),
("Isaiah", 40, 31),
("Matthew", 5, 16),
("Romans", 15, 13),
("Colossians", 3, 23),
("1 John", 1, 9),
("Psalms", 37, 4),
("Proverbs", 27, 17)
]
# Select verse based on seed
verse_index = seed % len(featured_verses)
book, chapter, verse = featured_verses[verse_index]
verse_text = bible.get_verse_text(book, chapter, verse)
if not verse_text:
# Fallback to John 3:16
book, chapter, verse = "John", 3, 16
verse_text = bible.get_verse_text(book, chapter, verse)
return {
"book": book,
"chapter": chapter,
"verse": verse,
"text": verse_text,
"reference": f"{book} {chapter}:{verse}",
"date": date_str
}
# =============================================================================
# Routes
# =============================================================================
@router.get("/", response_class=HTMLResponse)
async def read_root(request: Request):
books = bible.get_books()
daily_verse = get_daily_verse()
# Define study guide categories
study_guides = {
"Foundational Studies": [
{
"title": "New Believer's Guide",
"description": "Essential truths for new Christians",
"slug": "new-believer",
"verses": ["John 3:16", "Romans 10:9", "1 John 1:9", "2 Corinthians 5:17"]
},
{
"title": "Salvation by Grace",
"description": "Understanding God's gift of salvation",
"slug": "salvation",
"verses": ["Ephesians 2:8-9", "Romans 3:23", "Romans 6:23", "Titus 3:5"]
},
{
"title": "The Gospel Message",
"description": "The good news of Jesus Christ",
"slug": "gospel",
"verses": ["1 Corinthians 15:3-4", "Romans 1:16", "Mark 16:15", "Acts 4:12"]
}
],
"Character & Living": [
{
"title": "Fruits of the Spirit",
"description": "Developing Christian character",
"slug": "fruits-spirit",
"verses": ["Galatians 5:22-23", "1 Corinthians 13:4-7", "Philippians 4:8", "Colossians 3:12-14"]
},
{
"title": "Prayer & Faith",
"description": "Growing in prayer and trust",
"slug": "prayer-faith",
"verses": ["Matthew 6:9-13", "1 Thessalonians 5:17", "Hebrews 11:1", "James 1:6"]
},
{
"title": "Christian Living",
"description": "Walking as followers of Christ",
"slug": "christian-living",
"verses": ["Romans 12:1-2", "1 Peter 2:9", "Matthew 5:14-16", "Philippians 2:14-16"]
}
],
"Biblical Themes": [
{
"title": "God's Love",
"description": "Understanding the depth of God's love",
"slug": "gods-love",
"verses": ["1 John 4:8", "John 3:16", "Romans 8:38-39", "1 John 3:1"]
},
{
"title": "Hope & Comfort",
"description": "Finding hope in difficult times",
"slug": "hope-comfort",
"verses": ["Romans 15:13", "2 Corinthians 1:3-4", "Psalm 23:4", "Isaiah 41:10"]
},
{
"title": "Wisdom & Guidance",
"description": "Seeking God's wisdom for life",
"slug": "wisdom-guidance",
"verses": ["Proverbs 3:5-6", "James 1:5", "Psalm 119:105", "Proverbs 27:17"]
}
],
"Doctrinal Studies": [
{
"title": "The Trinity",
"description": "Understanding God as Father, Son, and Holy Spirit",
"slug": "trinity",
"verses": ["Matthew 28:19", "2 Corinthians 13:14", "1 Peter 1:2", "John 14:16-17"]
},
{
"title": "The Resurrection",
"description": "Christ's victory over death and our hope",
"slug": "resurrection",
"verses": ["1 Corinthians 15:20-22", "Romans 6:4-5", "John 11:25-26", "1 Thessalonians 4:16-17"]
},
{
"title": "Heaven & Eternity",
"description": "Our eternal home with God",
"slug": "heaven-eternity",
"verses": ["Revelation 21:1-4", "John 14:2-3", "Philippians 3:20-21", "1 Corinthians 2:9"]
}
],
"Family & Relationships": [
{
"title": "Biblical Marriage",
"description": "God's design for marriage",
"slug": "biblical-marriage",
"verses": ["Ephesians 5:22-33", "Genesis 2:24", "1 Corinthians 7:3-5", "Hebrews 13:4"]
},
{
"title": "Raising Children",
"description": "Biblical principles for parenting",
"slug": "raising-children",
"verses": ["Proverbs 22:6", "Ephesians 6:4", "Deuteronomy 6:6-7", "Colossians 3:21"]
},
{
"title": "Money & Stewardship",
"description": "Biblical wisdom on finances",
"slug": "money-stewardship",
"verses": ["Malachi 3:10", "Luke 16:10-11", "1 Timothy 6:10", "Proverbs 3:9-10"]
}
]
}
# 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, "index.html", {"books": books, "daily_verse": daily_verse, "study_guides": study_guides}
)
@router.get("/books", response_class=HTMLResponse)
async def books_page(request: Request):
"""Browse all books of the Bible"""
books = bible.get_books()
# Define book categories with types
book_types = {
# Old Testament
'Genesis': 'law', 'Exodus': 'law', 'Leviticus': 'law', 'Numbers': 'law', 'Deuteronomy': 'law',
'Joshua': 'historical', 'Judges': 'historical', 'Ruth': 'historical',
'1 Samuel': 'historical', '2 Samuel': 'historical', '1 Kings': 'historical', '2 Kings': 'historical',
'1 Chronicles': 'historical', '2 Chronicles': 'historical', 'Ezra': 'historical',
'Nehemiah': 'historical', 'Esther': 'historical',
'Job': 'wisdom', 'Psalms': 'wisdom', 'Proverbs': 'wisdom', 'Ecclesiastes': 'wisdom', 'Song of Solomon': 'wisdom',
'Isaiah': 'major-prophets', 'Jeremiah': 'major-prophets', 'Lamentations': 'major-prophets',
'Ezekiel': 'major-prophets', 'Daniel': 'major-prophets',
'Hosea': 'minor-prophets', 'Joel': 'minor-prophets', 'Amos': 'minor-prophets',
'Obadiah': 'minor-prophets', 'Jonah': 'minor-prophets', 'Micah': 'minor-prophets',
'Nahum': 'minor-prophets', 'Habakkuk': 'minor-prophets', 'Zephaniah': 'minor-prophets',
'Haggai': 'minor-prophets', 'Zechariah': 'minor-prophets', 'Malachi': 'minor-prophets',
# New Testament
'Matthew': 'gospels', 'Mark': 'gospels', 'Luke': 'gospels', 'John': 'gospels',
'Acts': 'acts',
'Romans': 'pauline', '1 Corinthians': 'pauline', '2 Corinthians': 'pauline',
'Galatians': 'pauline', 'Ephesians': 'pauline', 'Philippians': 'pauline', 'Colossians': 'pauline',
'1 Thessalonians': 'pauline', '2 Thessalonians': 'pauline',
'1 Timothy': 'pauline', '2 Timothy': 'pauline', 'Titus': 'pauline', 'Philemon': 'pauline',
'Hebrews': 'general', 'James': 'general', '1 Peter': 'general', '2 Peter': 'general',
'1 John': 'general', '2 John': 'general', '3 John': 'general', 'Jude': 'general',
'Revelation': 'apocalyptic'
}
# Organize books by testament
old_testament_books = [
'Genesis', 'Exodus', 'Leviticus', 'Numbers', 'Deuteronomy', 'Joshua', 'Judges', 'Ruth',
'1 Samuel', '2 Samuel', '1 Kings', '2 Kings', '1 Chronicles', '2 Chronicles', 'Ezra',
'Nehemiah', 'Esther', 'Job', 'Psalms', 'Proverbs', 'Ecclesiastes', 'Song of Solomon',
'Isaiah', 'Jeremiah', 'Lamentations', 'Ezekiel', 'Daniel', 'Hosea', 'Joel', 'Amos',
'Obadiah', 'Jonah', 'Micah', 'Nahum', 'Habakkuk', 'Zephaniah', 'Haggai', 'Zechariah', 'Malachi'
]
new_testament_books = [
'Matthew', 'Mark', 'Luke', 'John', 'Acts', 'Romans', '1 Corinthians', '2 Corinthians',
'Galatians', 'Ephesians', 'Philippians', 'Colossians', '1 Thessalonians', '2 Thessalonians',
'1 Timothy', '2 Timothy', 'Titus', 'Philemon', 'Hebrews', 'James', '1 Peter', '2 Peter',
'1 John', '2 John', '3 John', 'Jude', 'Revelation'
]
# Get chapter counts for each book
def get_chapter_count(book_name):
chapters = bible.get_chapters_for_book(book_name)
return len(chapters)
old_testament = [
{
'name': book,
'chapters': get_chapter_count(book),
'available': book in books,
'type': book_types.get(book, '')
}
for book in old_testament_books
]
new_testament = [
{
'name': book,
'chapters': get_chapter_count(book),
'available': book in books,
'type': book_types.get(book, '')
}
for book in new_testament_books
]
breadcrumbs = [
{"text": "Home", "url": "/"},
{"text": "Books", "url": None}
]
return templates.TemplateResponse(
request,
"books.html",
{
"old_testament": old_testament,
"new_testament": new_testament,
"books": books,
"breadcrumbs": breadcrumbs
}
)
@router.get("/resources", response_class=HTMLResponse)
async def resources_page(request: Request):
"""Browse all theological resources"""
books = bible.get_books()
# Organize resources into categories
resources = {
"People": [
{
"name": "Biblical Prophets",
"url": "/biblical-prophets",
"description": "Explore the prophetic ministry throughout Scripture, from Isaiah to Malachi",
"count": "9 prophets"
},
{
"name": "The Twelve Apostles",
"url": "/the-twelve-apostles",
"description": "The twelve disciples chosen by Jesus to be witnesses of His ministry",
"count": "12 apostles"
},
{
"name": "Women of the Bible",
"url": "/women-of-the-bible",
"description": "Notable women of Scripture and their significance in redemptive history",
"count": "12 women"
}
],
"Theology": [
{
"name": "Biblical Angels",
"url": "/biblical-angels",
"description": "Angelic beings mentioned in Scripture, including Michael, Gabriel, and the heavenly host",
"count": "12 entries"
},
{
"name": "The Tetragrammaton",
"url": "/tetragrammaton",
"description": "The sacred four-letter name of God (YHWH) and its profound significance",
"count": "Deep dive"
},
{
"name": "Names of God",
"url": "/names-of-god",
"description": "The revelation of God's names throughout Scripture and their meanings",
"count": "14 names"
},
{
"name": "Parables of Jesus",
"url": "/parables",
"description": "The parables spoken by Christ to illustrate spiritual truths",
"count": "11 parables"
},
{
"name": "Miracles of Jesus",
"url": "/miracles-of-jesus",
"description": "Signs and wonders manifesting divine authority over nature, disease, demons, and death",
"count": "35+ miracles"
},
{
"name": "I Am Statements",
"url": "/i-am-statements",
"description": "The seven 'I Am' statements of Jesus in John's Gospel revealing His divine nature",
"count": "7 statements"
},
{
"name": "The Beatitudes",
"url": "/beatitudes",
"description": "The blessings proclaimed by Jesus in the Sermon on the Mount",
"count": "8 beatitudes"
},
{
"name": "Ten Commandments",
"url": "/ten-commandments",
"description": "The moral law given by God to Moses on Mount Sinai",
"count": "10 commandments"
},
{
"name": "Armor of God",
"url": "/armor-of-god",
"description": "The spiritual equipment for warfare described in Ephesians 6",
"count": "7 pieces"
},
{
"name": "Prayers of the Bible",
"url": "/prayers-of-the-bible",
"description": "Sacred prayers from the Psalms, Jesus, Paul, and the early church",
"count": "20+ prayers"
},
{
"name": "Biblical Covenants",
"url": "/biblical-covenants",
"description": "Divine covenants established between God and His people",
"count": "7 covenants"
},
{
"name": "Fruits of the Spirit",
"url": "/fruits-of-the-spirit",
"description": "The nine graces of Galatians 5:22-23 manifested in believers through the Holy Spirit",
"count": "9 fruits"
}
],
"Systematic Theology": [
{
"name": "The Trinity",
"url": "/trinity",
"description": "The mystery of God revealed as Father, Son, and Holy Spirit—three Persons, one God",
"count": "4 categories"
},
{
"name": "Christology",
"url": "/christology",
"description": "The Person and work of Jesus Christ—His deity, humanity, and offices",
"count": "4 categories"
},
{
"name": "Pneumatology",
"url": "/pneumatology",
"description": "The doctrine of the Holy Spirit—His Person, deity, and work in believers",
"count": "4 categories"
},
{
"name": "Soteriology",
"url": "/soteriology",
"description": "The doctrine of salvation—from election to glorification",
"count": "5 categories"
},
{
"name": "Ecclesiology",
"url": "/ecclesiology",
"description": "The doctrine of the Church—its nature, mission, and governance",
"count": "4 categories"
},
{
"name": "Eschatology",
"url": "/eschatology",
"description": "The doctrine of last things—Christ's return, judgment, and eternal state",
"count": "5 categories"
},
{
"name": "The Kingdom of God",
"url": "/kingdom-of-god",
"description": "God's sovereign reign inaugurated in Christ and consummated at His return",
"count": "5 categories"
},
{
"name": "Types and Shadows",
"url": "/types-and-shadows",
"description": "Old Testament persons, events, and institutions that prefigure Christ",
"count": "5 categories"
},
{
"name": "Messianic Prophecies",
"url": "/messianic-prophecies",
"description": "Old Testament prophecies fulfilled in Jesus Christ",
"count": "5 categories"
},
{
"name": "The Blood in Scripture",
"url": "/blood-in-scripture",
"description": "The theology of blood, sacrifice, and redemption throughout Scripture",
"count": "5 categories"
},
{
"name": "Names and Titles of Christ",
"url": "/names-of-christ",
"description": "The names and titles ascribed to Jesus revealing His Person and work",
"count": "5 categories"
},
{
"name": "Spirits & Demons",
"url": "/spirits-and-demons",
"description": "Biblical demonology—Satan, evil spirits, Legion, and spiritual warfare",
"count": "7 categories"
},
{
"name": "Personifications",
"url": "/personifications",
"description": "Abstract concepts given human form—Wisdom, Folly, Death, Sin, and more",
"count": "6 categories"
},
{
"name": "Bibliology",
"url": "/bibliology",
"description": "The Doctrine of Scripture—inspiration, authority, sufficiency, and preservation",
"count": "4 categories"
},
{
"name": "Theology Proper",
"url": "/theology-proper",
"description": "The Attributes of God—His incommunicable and communicable perfections",
"count": "4 categories"
},
{
"name": "Anthropology",
"url": "/anthropology",
"description": "The Doctrine of Man—creation, constitution, and condition of humanity",
"count": "4 categories"
},
{
"name": "Hamartiology",
"url": "/hamartiology",
"description": "The Doctrine of Sin—its origin, nature, transmission, and consequences",
"count": "4 categories"
},
{
"name": "Providence",
"url": "/providence",
"description": "Divine Providence—God's preservation, governance, and concurrence in all things",
"count": "4 categories"
},
{
"name": "Grace",
"url": "/grace",
"description": "The Doctrine of Grace—common grace, effectual grace, election, and perseverance",
"count": "4 categories"
},
{
"name": "Justification",
"url": "/justification",
"description": "The Doctrine of Justification—declared righteous through faith in Christ alone",
"count": "4 categories"
},
{
"name": "Sanctification",
"url": "/sanctification",
"description": "The Doctrine of Sanctification—progressive holiness through the Spirit",
"count": "4 categories"
},
{
"name": "Law and Gospel",
"url": "/law-and-gospel",
"description": "The distinction between Law and Gospel—God's demands and His gracious provision",
"count": "4 categories"
},
{
"name": "Worship",
"url": "/worship",
"description": "The Doctrine of Worship—regulative principle, elements, and the heart of worship",
"count": "4 categories"
}
],
"History & Culture": [
{
"name": "Biblical Festivals",
"url": "/biblical-festivals",
"description": "The appointed feasts and holy days ordained in the Law of Moses",
"count": "7 festivals"
},
{
"name": "Biblical Geography",
"url": "/biblical-maps",
"description": "Locations mentioned in Scripture and their historical significance",
"count": "Maps & places",
"badge": "Interactive"
},
{
"name": "Biblical Timeline",
"url": "/biblical-timeline",
"description": "Chronological overview of biblical events from Creation to Revelation",
"count": "Timeline"
},
{
"name": "Genealogies",
"url": "/family-tree",
"description": "Interactive family tree from Adam to Jesus Christ with detailed person profiles",
"count": "160+ people",
"badge": "Interactive"
}
],
"Study Tools": [
{
"name": "Study Guides",
"url": "/study-guides",
"description": "In-depth guides for studying biblical books, themes, and doctrines",
"count": "Multiple guides"
}
]
}
breadcrumbs = [
{"text": "Home", "url": "/"},
{"text": "Resources", "url": None}
]
return templates.TemplateResponse(
request,
"resources.html",
{
"resources": resources,
"books": books,
"breadcrumbs": breadcrumbs
}
)
+225
View File
@@ -0,0 +1,225 @@
"""Miscellaneous routes - search, interlinear, random verse, verse of the day."""
import hashlib
import random
from datetime import datetime, timedelta
from fastapi import APIRouter, Query, Request
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from ..kjv import bible
from ..utils.search import perform_full_text_search
router = APIRouter()
templates = None
# Will be set by init_search_family_tree()
_search_family_tree_fn = None
def init_templates(t: Jinja2Templates):
"""Initialize templates for misc routes."""
global templates
templates = t
def init_search_family_tree(fn):
"""Initialize the search_family_tree function from server.py."""
global _search_family_tree_fn
_search_family_tree_fn = fn
# =============================================================================
# Helper Functions
# =============================================================================
def get_daily_verse(date_str=None):
"""Get the verse of the day based on a specific date (or current date if not provided)"""
# Use date as seed for consistent daily verse
if date_str is None:
date_str = datetime.now().strftime("%Y-%m-%d")
seed = int(hashlib.md5(date_str.encode()).hexdigest(), 16) % 1000000
# Featured verses for rotation
featured_verses = [
("John", 3, 16),
("Jeremiah", 29, 11),
("Philippians", 4, 13),
("Romans", 8, 28),
("Proverbs", 3, 5),
("Isaiah", 41, 10),
("Matthew", 11, 28),
("1 John", 4, 19),
("Psalms", 23, 1),
("2 Corinthians", 5, 17),
("Ephesians", 2, 8),
("Romans", 10, 9),
("1 Peter", 5, 7),
("James", 1, 5),
("Philippians", 4, 19),
("Psalms", 119, 105),
("Matthew", 6, 33),
("Romans", 12, 2),
("1 Corinthians", 13, 13),
("Galatians", 5, 22),
("Hebrews", 11, 1),
("1 Thessalonians", 5, 18),
("Psalms", 46, 1),
("Isaiah", 40, 31),
("Matthew", 5, 16),
("Romans", 15, 13),
("Colossians", 3, 23),
("1 John", 1, 9),
("Psalms", 37, 4),
("Proverbs", 27, 17)
]
# Select verse based on seed
verse_index = seed % len(featured_verses)
book, chapter, verse = featured_verses[verse_index]
verse_text = bible.get_verse_text(book, chapter, verse)
if not verse_text:
# Fallback to John 3:16
book, chapter, verse = "John", 3, 16
verse_text = bible.get_verse_text(book, chapter, verse)
return {
"book": book,
"chapter": chapter,
"verse": verse,
"text": verse_text,
"reference": f"{book} {chapter}:{verse}",
"date": date_str
}
# =============================================================================
# Routes
# =============================================================================
@router.get("/search", response_class=HTMLResponse)
async def search_page(request: Request, q: str = Query(None, description="Search query")):
"""Search page with results (includes Bible verses and family tree)"""
books = bible.get_books()
search_results = []
family_tree_results = []
is_direct_verse = False
if q and len(q.strip()) >= 2:
# Search Bible verses
search_results = perform_full_text_search(q.strip())
# Check if this was a direct verse reference match
if search_results and len(search_results) == 1 and search_results[0].get("score") == 100.0:
is_direct_verse = True
# Also search family tree (limit to 5 results)
if _search_family_tree_fn:
family_tree_results = _search_family_tree_fn(q.strip(), limit=5)
return templates.TemplateResponse(
request,
"search.html",
{
"query": q or "",
"results": search_results,
"family_tree_results": family_tree_results,
"books": books,
"total_results": len(search_results) + len(family_tree_results),
"is_direct_verse": is_direct_verse
}
)
@router.get("/interlinear", response_class=HTMLResponse)
async def interlinear_landing_page(request: Request):
"""Landing page explaining interlinear Bible study"""
books = bible.get_books()
# Featured verses with interlinear data
featured_verses = [
{"reference": "John 3:16", "url": "/book/John/chapter/3/verse/16", "note": "God's love for the world"},
{"reference": "Genesis 1:1", "url": "/book/Genesis/chapter/1/verse/1", "note": "In the beginning"},
{"reference": "Psalm 23:1", "url": "/book/Psalms/chapter/23/verse/1", "note": "The Lord is my shepherd"},
{"reference": "Romans 8:28", "url": "/book/Romans/chapter/8/verse/28", "note": "All things work together for good"},
{"reference": "Matthew 28:19", "url": "/book/Matthew/chapter/28/verse/19", "note": "The Great Commission"},
{"reference": "1 Corinthians 13:4", "url": "/book/1 Corinthians/chapter/13/verse/4", "note": "Love is patient"},
]
# Build breadcrumbs
breadcrumbs = [
{"text": "Home", "url": "/"},
{"text": "Interlinear", "url": None}
]
return templates.TemplateResponse(
request,
"interlinear_landing.html",
{
"books": books,
"featured_verses": featured_verses,
"breadcrumbs": breadcrumbs
}
)
@router.get("/random-verse")
async def random_verse(request: Request):
"""Redirect to a random Bible verse"""
# Get all books
all_books = bible.get_books()
# Pick a random book
book = random.choice(all_books)
# Get all chapters for this book
chapters = bible.get_chapters_for_book(book)
# Pick a random chapter
chapter = random.choice(chapters)
# Get all verses for this chapter
verses = bible.get_verses_by_book_chapter(book, chapter)
# Pick a random verse
verse = random.choice(verses)
# Redirect to the verse page with cache control headers to ensure fresh random verse each time
response = RedirectResponse(url=f"/book/{book}/chapter/{chapter}/verse/{verse.verse}", status_code=302)
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
response.headers["Pragma"] = "no-cache"
response.headers["Expires"] = "0"
return response
@router.get("/verse-of-the-day", response_class=HTMLResponse)
async def verse_of_the_day_page(request: Request):
"""Verse of the day page"""
books = bible.get_books()
daily_verse = get_daily_verse()
# Generate past 30 days of verses
past_verses = []
today = datetime.now()
for i in range(1, 31): # Past 30 days (not including today)
past_date = today - timedelta(days=i)
date_str = past_date.strftime("%Y-%m-%d")
verse = get_daily_verse(date_str)
past_verses.append(verse)
# Build breadcrumbs
breadcrumbs = [
{"text": "Home", "url": "/"},
{"text": "Verse of the Day", "url": "/verse-of-the-day"}
]
return templates.TemplateResponse(
request,
"verse_of_the_day.html",
{
"books": books,
"daily_verse": daily_verse,
"past_verses": past_verses,
"breadcrumbs": breadcrumbs
}
)
+185
View File
@@ -0,0 +1,185 @@
"""Reading plans routes - browse and view Bible reading plans."""
import re
from fastapi import APIRouter, HTTPException, Request
from fastapi.responses import HTMLResponse, StreamingResponse
from fastapi.templating import Jinja2Templates
from ..kjv import bible
from ..reading_plans import get_plan, get_plan_summary
from ..utils.books import normalize_book_name, OT_BOOKS, NT_BOOKS
from ..utils.pdf import WEASYPRINT_AVAILABLE, render_html_to_pdf_async
router = APIRouter()
templates = None
def init_templates(t: Jinja2Templates):
"""Initialize templates for reading plans routes."""
global templates
templates = t
# =============================================================================
# Helper Functions
# =============================================================================
def parse_reading_reference(ref: str) -> list:
"""Parse a reading reference like 'Genesis 1-3' or 'Matthew 1' into chapter list.
Returns list of tuples: [(book, chapter), ...]
"""
# Handle patterns like "Genesis 1-3", "Matthew 1", "1 John 2-3"
# Pattern: optional number prefix + book name + chapter range
pattern = r'^((?:\d\s+)?[A-Za-z]+(?:\s+[A-Za-z]+)?)\s+(\d+)(?:-(\d+))?$'
match = re.match(pattern, ref.strip())
if not match:
return []
book = match.group(1)
start_ch = int(match.group(2))
end_ch = int(match.group(3)) if match.group(3) else start_ch
# Normalize book name - if it's already canonical, use it as-is
normalized = normalize_book_name(book)
if not normalized:
# Check if it's already a valid canonical name
all_books = OT_BOOKS + NT_BOOKS
if book in all_books:
normalized = book
else:
return []
return [(normalized, ch) for ch in range(start_ch, end_ch + 1)]
def get_reading_text(readings: list) -> list:
"""Get the Bible text for a list of reading references.
Returns list of dicts with book, chapter, and verses.
"""
result = []
for ref in readings:
chapters = parse_reading_reference(ref)
for book, chapter in chapters:
verses = bible.get_verses_by_book_chapter(book, chapter)
if verses:
result.append({
'book': book,
'chapter': chapter,
'verses': verses,
'reference': f"{book} {chapter}"
})
return result
# =============================================================================
# Routes
# =============================================================================
@router.get("/reading-plans", response_class=HTMLResponse)
async def reading_plans_page(request: Request):
"""Browse Bible reading plans"""
books = bible.get_books()
plans = get_plan_summary()
breadcrumbs = [
{"text": "Home", "url": "/"},
{"text": "Reading Plans", "url": None}
]
return templates.TemplateResponse(
request,
"reading_plans.html",
{
"plans": plans,
"books": books,
"breadcrumbs": breadcrumbs
}
)
@router.get("/reading-plans/{plan_id}", response_class=HTMLResponse)
async def reading_plan_detail(request: Request, plan_id: str):
"""View a specific reading plan"""
books = bible.get_books()
plan = get_plan(plan_id)
if not plan:
raise HTTPException(status_code=404, detail="Reading plan not found")
# Pass day info without text - text will be lazy loaded via API
all_days = plan.get('days') or plan.get('sample_days', [])
days_data = []
for day in all_days:
day_data = {
'day': day['day'],
'theme': day.get('theme', ''),
'readings': day['readings']
}
days_data.append(day_data)
breadcrumbs = [
{"text": "Home", "url": "/"},
{"text": "Reading Plans", "url": "/reading-plans"},
{"text": plan["name"], "url": None}
]
return templates.TemplateResponse(
request,
"reading_plan_detail.html",
{
"plan": plan,
"plan_id": plan_id,
"books": books,
"breadcrumbs": breadcrumbs,
"pdf_available": WEASYPRINT_AVAILABLE,
"pdf_url": f"/reading-plans/{plan_id}/pdf" if WEASYPRINT_AVAILABLE else None,
"days_data": days_data,
"total_days": plan.get('duration_days', len(days_data))
}
)
@router.get("/reading-plans/{plan_id}/pdf")
async def reading_plan_pdf(plan_id: str):
"""Generate a PDF export for a reading plan."""
if not WEASYPRINT_AVAILABLE:
raise HTTPException(
status_code=503,
detail="PDF generation is not available. WeasyPrint system libraries are not installed."
)
plan = get_plan(plan_id)
if not plan:
raise HTTPException(status_code=404, detail="Reading plan not found")
# Include full Bible text for all plans (including 365-day plans)
include_text = True
days_with_text = None
if include_text:
all_days = plan.get('days') or plan.get('sample_days', [])
days_with_text = []
for day in all_days:
day_data = {
'day': day['day'],
'theme': day.get('theme', ''),
'readings': day['readings'],
'text': get_reading_text(day['readings'])
}
days_with_text.append(day_data)
html_content = templates.get_template("reading_plan_pdf.html").render(
plan=plan,
include_text=include_text,
days_with_text=days_with_text
)
pdf_buffer = await render_html_to_pdf_async(html_content)
filename = f"reading-plan-{plan_id}.pdf"
return StreamingResponse(
pdf_buffer,
media_type="application/pdf",
headers={"Content-Disposition": f"attachment; filename={filename}"}
)
+171
View File
@@ -0,0 +1,171 @@
"""Strong's Concordance routes - Hebrew and Greek word study."""
import re
from fastapi import APIRouter, HTTPException, Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from ..kjv import bible
from ..strongs import format_strongs_entry, search_strongs, get_all_strongs
from ..interlinear_loader import find_verses_by_strongs
router = APIRouter()
templates = None
def init_templates(t: Jinja2Templates):
"""Initialize templates for Strong's routes."""
global templates
templates = t
# =============================================================================
# Routes
# =============================================================================
@router.get("/strongs", response_class=HTMLResponse)
async def strongs_index(request: Request, q: str = None):
"""Strong's Concordance search and lookup page."""
results = []
if q:
results = search_strongs(q, language="both", limit=100)
books = bible.get_books()
breadcrumbs = [
{"text": "Home", "url": "/"},
{"text": "Strong's Concordance", "url": None}
]
return templates.TemplateResponse(
request,
"strongs_index.html",
{
"query": q or "",
"results": results,
"books": books,
"breadcrumbs": breadcrumbs
}
)
@router.get("/strongs/hebrew", response_class=HTMLResponse)
async def strongs_hebrew_index(request: Request, page: int = 1):
"""Paginated index of all Hebrew Strong's entries."""
data = get_all_strongs("hebrew", page=page, per_page=100)
books = bible.get_books()
breadcrumbs = [
{"text": "Home", "url": "/"},
{"text": "Strong's Concordance", "url": "/strongs"},
{"text": "Hebrew", "url": None}
]
return templates.TemplateResponse(
request,
"strongs_language_index.html",
{
"language": "Hebrew",
"language_code": "hebrew",
"entries": data["entries"],
"page": data["page"],
"total_pages": data["total_pages"],
"total": data["total"],
"books": books,
"breadcrumbs": breadcrumbs
}
)
@router.get("/strongs/greek", response_class=HTMLResponse)
async def strongs_greek_index(request: Request, page: int = 1):
"""Paginated index of all Greek Strong's entries."""
data = get_all_strongs("greek", page=page, per_page=100)
books = bible.get_books()
breadcrumbs = [
{"text": "Home", "url": "/"},
{"text": "Strong's Concordance", "url": "/strongs"},
{"text": "Greek", "url": None}
]
return templates.TemplateResponse(
request,
"strongs_language_index.html",
{
"language": "Greek",
"language_code": "greek",
"entries": data["entries"],
"page": data["page"],
"total_pages": data["total_pages"],
"total": data["total"],
"books": books,
"breadcrumbs": breadcrumbs
}
)
@router.get("/strongs/{strongs_number}", response_class=HTMLResponse)
async def strongs_entry(request: Request, strongs_number: str):
"""View a single Strong's concordance entry."""
entry = format_strongs_entry(strongs_number)
if not entry:
raise HTTPException(
status_code=404,
detail=f"Strong's number '{strongs_number}' not found"
)
# Find all verses containing this Strong's number
verse_occurrences = find_verses_by_strongs(strongs_number, limit=10000)
total_occurrences = len(verse_occurrences)
# Fetch full verse text for each occurrence and highlight the word
for occ in verse_occurrences:
verse_text = bible.get_verse_text(occ["book"], occ["chapter"], occ["verse"])
if verse_text:
# Highlight the English word in the verse text
english_word = occ.get("english", "")
if english_word and english_word in verse_text:
occ["verse_text"] = verse_text.replace(
english_word,
f'<mark>{english_word}</mark>',
1 # Only highlight first occurrence
)
else:
occ["verse_text"] = verse_text
else:
occ["verse_text"] = ""
# Extract and fetch related Strong's entries from derivation
related_entries = []
if entry.get("derivation"):
# Find all Strong's references like H1234 or G5678
strongs_refs = re.findall(r'([HG])(\d+)', entry["derivation"])
seen = set()
for prefix, num in strongs_refs:
ref = f"{prefix}{num}"
if ref.upper() != strongs_number.upper() and ref not in seen:
seen.add(ref)
related = format_strongs_entry(ref)
if related:
related_entries.append(related)
books = bible.get_books()
breadcrumbs = [
{"text": "Home", "url": "/"},
{"text": "Strong's Concordance", "url": "/strongs"},
{"text": strongs_number.upper(), "url": None}
]
return templates.TemplateResponse(
request,
"strongs_entry.html",
{
"entry": entry,
"books": books,
"breadcrumbs": breadcrumbs,
"verse_occurrences": verse_occurrences,
"total_occurrences": total_occurrences,
"related_entries": related_entries
}
)
+105
View File
@@ -0,0 +1,105 @@
"""Biblical timeline routes."""
import json
from pathlib import Path
from fastapi import APIRouter, HTTPException, Request
from fastapi.responses import HTMLResponse, StreamingResponse
from fastapi.templating import Jinja2Templates
from ..kjv import bible
from ..utils.pdf import WEASYPRINT_AVAILABLE, render_html_to_pdf_async
router = APIRouter()
templates = None
def init_templates(t: Jinja2Templates):
"""Initialize templates for timeline routes."""
global templates
templates = t
# =============================================================================
# Helper Functions
# =============================================================================
def get_biblical_timeline_context():
"""
Load comprehensive biblical timeline data from JSON file.
Returns tuple of (timeline_events, introduction, chronology_note, chronology_comparison, conclusion)
"""
# Load timeline data from JSON file
data_dir = Path(__file__).parent.parent / "data"
timeline_path = data_dir / "biblical_timeline.json"
with open(timeline_path, 'r', encoding='utf-8') as f:
timeline_data = json.load(f)
timeline_events = timeline_data.get("timeline_events", {})
introduction = timeline_data.get("introduction", "")
chronology_note = timeline_data.get("chronology_note", "")
chronology_comparison = timeline_data.get("chronology_comparison", [])
conclusion = timeline_data.get("conclusion", "")
return timeline_events, introduction, chronology_note, chronology_comparison, conclusion
# =============================================================================
# Routes
# =============================================================================
@router.get("/biblical-timeline", response_class=HTMLResponse)
def biblical_timeline_page(request: Request):
"""Biblical timeline page showing major biblical events chronologically"""
books = bible.get_books()
timeline_events, introduction, chronology_note, chronology_comparison, conclusion = get_biblical_timeline_context()
breadcrumbs = [
{"text": "Home", "url": "/"},
{"text": "Resources", "url": "/resources"},
{"text": "Biblical Timeline", "url": None}
]
return templates.TemplateResponse(
request,
"biblical_timeline.html",
{
"books": books,
"timeline_events": timeline_events,
"introduction": introduction,
"chronology_note": chronology_note,
"chronology_comparison": chronology_comparison,
"conclusion": conclusion,
"breadcrumbs": breadcrumbs,
"pdf_available": WEASYPRINT_AVAILABLE
}
)
@router.get("/biblical-timeline/pdf")
async def biblical_timeline_pdf():
"""Generate PDF export for the biblical timeline."""
if not WEASYPRINT_AVAILABLE:
raise HTTPException(
status_code=503,
detail="PDF generation is not available. WeasyPrint system libraries are not installed."
)
timeline_events, introduction, chronology_note, chronology_comparison, conclusion = get_biblical_timeline_context()
html_content = templates.get_template("biblical_timeline_pdf.html").render(
timeline_events=timeline_events,
introduction=introduction,
chronology_note=chronology_note,
chronology_comparison=chronology_comparison,
conclusion=conclusion
)
pdf_buffer = await render_html_to_pdf_async(html_content)
return StreamingResponse(
pdf_buffer,
media_type="application/pdf",
headers={"Content-Disposition": "attachment; filename=biblical-timeline.pdf"}
)
+100
View File
@@ -0,0 +1,100 @@
"""Topics routes - browse and view topical Bible studies."""
from fastapi import APIRouter, HTTPException, Request
from fastapi.responses import HTMLResponse, StreamingResponse
from fastapi.templating import Jinja2Templates
from ..kjv import bible
from ..topics import get_all_topics, get_topic_with_text
from ..utils.pdf import WEASYPRINT_AVAILABLE, render_html_to_pdf_async
router = APIRouter()
templates = None
def init_templates(t: Jinja2Templates):
"""Initialize templates for topics routes."""
global templates
templates = t
# =============================================================================
# Routes
# =============================================================================
@router.get("/topics", response_class=HTMLResponse)
async def topics_page(request: Request):
"""Browse topical index of Bible themes"""
books = bible.get_books()
topics = get_all_topics()
breadcrumbs = [
{"text": "Home", "url": "/"},
{"text": "Topics", "url": None}
]
return templates.TemplateResponse(
request,
"topics.html",
{
"topics": topics,
"books": books,
"breadcrumbs": breadcrumbs,
"pdf_available": WEASYPRINT_AVAILABLE
}
)
@router.get("/topics/{topic_name}", response_class=HTMLResponse)
async def topic_detail(request: Request, topic_name: str):
"""View verses for a specific topic"""
books = bible.get_books()
topic = get_topic_with_text(topic_name)
if not topic:
raise HTTPException(status_code=404, detail="Topic not found")
breadcrumbs = [
{"text": "Home", "url": "/"},
{"text": "Topics", "url": "/topics"},
{"text": topic_name, "url": None}
]
return templates.TemplateResponse(
request,
"topic_detail.html",
{
"topic": topic,
"topic_name": topic_name,
"books": books,
"breadcrumbs": breadcrumbs,
"pdf_available": WEASYPRINT_AVAILABLE,
"pdf_url": f"/topics/{topic_name}/pdf" if WEASYPRINT_AVAILABLE else None
}
)
@router.get("/topics/{topic_name}/pdf")
async def topic_detail_pdf(topic_name: str):
"""Generate a PDF export for a topic detail page."""
if not WEASYPRINT_AVAILABLE:
raise HTTPException(
status_code=503,
detail="PDF generation is not available. WeasyPrint system libraries are not installed."
)
topic = get_topic_with_text(topic_name)
if not topic:
raise HTTPException(status_code=404, detail="Topic not found")
html_content = templates.get_template("topic_pdf.html").render(
topic=topic,
topic_name=topic_name,
)
pdf_buffer = await render_html_to_pdf_async(html_content)
filename = f"{topic_name}.pdf"
return StreamingResponse(
pdf_buffer,
media_type="application/pdf",
headers={"Content-Disposition": f"attachment; filename={filename}"}
)
+43 -1971
View File
File diff suppressed because it is too large Load Diff