Files
kjvstudy.org/kjvstudy_org/routes/about.py
T
kennethreitz 9d989e2189 Add accessibility page and improve site-wide accessibility
- Create dedicated accessibility page at /about/accessibility
- Add skip link for keyboard/screen reader users
- Add keyboard navigation to interlinear landing page
- Implement 2D grid navigation for theological studies on homepage
- Add accessibility section to homepage with keyboard shortcut info
- Fix verse text color contrast on fruits of the spirit page
- Fix malformed proverbs commentary data
- Update about page with accessibility link

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-03 01:13:29 -05:00

324 lines
12 KiB
Python

"""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')))
# Bible Stories statistics
total_stories = 0
stories_with_kids = 0
total_story_characters = set()
total_story_themes = set()
total_story_words = 0
total_kids_story_words = 0
for file in (data_dir / 'stories').glob('*.json'):
try:
story_data = json.load(open(file))
stories = story_data.get('stories', [])
total_stories += len(stories)
for story in stories:
# Count words in narrative
narrative = story.get('narrative', '')
total_story_words += len(narrative.split())
if story.get('kids_narrative'):
stories_with_kids += 1
total_kids_story_words += len(story.get('kids_narrative', '').split())
for char in story.get('characters', []):
total_story_characters.add(char)
for theme in story.get('themes', []):
total_story_themes.add(theme)
except (json.JSONDecodeError, IOError):
continue
# 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,
},
'bible_stories': {
'categories': story_files,
'total_stories': total_stories,
'stories_with_kids': stories_with_kids,
'unique_characters': len(total_story_characters),
'unique_themes': len(total_story_themes),
'total_words': total_story_words,
'kids_words': total_kids_story_words,
},
'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,
}
)
@router.get("/about/accessibility", response_class=HTMLResponse)
async def accessibility(request: Request):
"""Accessibility page - keyboard navigation, screen readers, text-to-speech"""
books = bible.get_books()
breadcrumbs = [
{"text": "Home", "url": "/"},
{"text": "About", "url": "/about"},
{"text": "Accessibility", "url": None}
]
return templates.TemplateResponse(
"accessibility.html",
{
"request": request,
"books": books,
"breadcrumbs": breadcrumbs,
}
)