mirror of
https://github.com/kennethreitz/kjvstudy.org.git
synced 2026-06-05 23:00:16 +00:00
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:
@@ -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',
|
||||
]
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
)
|
||||
@@ -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}"}
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
)
|
||||
@@ -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"}
|
||||
)
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user