Spaces:
Running
Running
github-actions[bot] commited on
Commit ·
eba7c64
1
Parent(s): b222bcc
🚀 Auto-deploy backend from GitHub (d39af4f)
Browse files- main.py +2 -0
- routes/curriculum_routes.py +66 -0
- services/curriculum_service.py +232 -0
main.py
CHANGED
|
@@ -75,6 +75,7 @@ from services.user_provisioning_service import (
|
|
| 75 |
UserProvisioningService,
|
| 76 |
)
|
| 77 |
from routes.rag_routes import router as rag_router
|
|
|
|
| 78 |
from rag.curriculum_rag import (
|
| 79 |
build_analysis_curriculum_context,
|
| 80 |
build_lesson_prompt,
|
|
@@ -974,6 +975,7 @@ class RequestMiddleware(BaseHTTPMiddleware):
|
|
| 974 |
app.add_middleware(RequestMiddleware)
|
| 975 |
app.add_middleware(AuthMiddleware)
|
| 976 |
app.include_router(rag_router)
|
|
|
|
| 977 |
|
| 978 |
|
| 979 |
# ─── Global Exception Handler ─────────────────────────────────
|
|
|
|
| 75 |
UserProvisioningService,
|
| 76 |
)
|
| 77 |
from routes.rag_routes import router as rag_router
|
| 78 |
+
from routes.curriculum_routes import router as curriculum_router
|
| 79 |
from rag.curriculum_rag import (
|
| 80 |
build_analysis_curriculum_context,
|
| 81 |
build_lesson_prompt,
|
|
|
|
| 975 |
app.add_middleware(RequestMiddleware)
|
| 976 |
app.add_middleware(AuthMiddleware)
|
| 977 |
app.include_router(rag_router)
|
| 978 |
+
app.include_router(curriculum_router)
|
| 979 |
|
| 980 |
|
| 981 |
# ─── Global Exception Handler ─────────────────────────────────
|
routes/curriculum_routes.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import logging
|
| 4 |
+
from typing import Optional
|
| 5 |
+
|
| 6 |
+
from fastapi import APIRouter, HTTPException, Query
|
| 7 |
+
from pydantic import BaseModel
|
| 8 |
+
|
| 9 |
+
from services.curriculum_service import (
|
| 10 |
+
get_subject,
|
| 11 |
+
get_subjects,
|
| 12 |
+
get_topic,
|
| 13 |
+
get_topics,
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
logger = logging.getLogger("mathpulse.curriculum")
|
| 17 |
+
router = APIRouter(prefix="/api/curriculum", tags=["curriculum"])
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class SubjectResponse(BaseModel):
|
| 21 |
+
id: str
|
| 22 |
+
code: str
|
| 23 |
+
name: str
|
| 24 |
+
gradeLevel: str
|
| 25 |
+
semester: str
|
| 26 |
+
color: str
|
| 27 |
+
pdfAvailable: bool
|
| 28 |
+
topics: list
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class TopicResponse(BaseModel):
|
| 32 |
+
id: str
|
| 33 |
+
name: str
|
| 34 |
+
unit: str
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
@router.get("/subjects", response_model=list[SubjectResponse])
|
| 38 |
+
async def list_subjects(grade_level: Optional[str] = Query(None, description="Filter by grade level (e.g., 'Grade 11', 'Grade 12')")):
|
| 39 |
+
"""List all curriculum subjects, optionally filtered by grade level."""
|
| 40 |
+
subjects = get_subjects(grade_level)
|
| 41 |
+
return subjects
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
@router.get("/subjects/{subject_id}", response_model=SubjectResponse)
|
| 45 |
+
async def get_subject_by_id(subject_id: str):
|
| 46 |
+
"""Get a specific subject by ID."""
|
| 47 |
+
subject = get_subject(subject_id)
|
| 48 |
+
if not subject:
|
| 49 |
+
raise HTTPException(status_code=404, detail=f"Subject not found: {subject_id}")
|
| 50 |
+
return subject
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
@router.get("/subjects/{subject_id}/topics", response_model=list[TopicResponse])
|
| 54 |
+
async def list_subject_topics(subject_id: str):
|
| 55 |
+
"""List all topics for a subject."""
|
| 56 |
+
topics = get_topics(subject_id)
|
| 57 |
+
return topics
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
@router.get("/subjects/{subject_id}/topics/{topic_id}", response_model=TopicResponse)
|
| 61 |
+
async def get_topic_by_id(subject_id: str, topic_id: str):
|
| 62 |
+
"""Get a specific topic."""
|
| 63 |
+
topic = get_topic(subject_id, topic_id)
|
| 64 |
+
if not topic:
|
| 65 |
+
raise HTTPException(status_code=404, detail=f"Topic not found: {subject_id}/{topic_id}")
|
| 66 |
+
return topic
|
services/curriculum_service.py
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Curriculum Service - Firestore-backed curriculum data.
|
| 3 |
+
|
| 4 |
+
Fetches subjects, topics, and modules from Firestore.
|
| 5 |
+
Falls back to static data if Firestore is unavailable.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import logging
|
| 9 |
+
import os
|
| 10 |
+
from typing import Any, Dict, List, Optional
|
| 11 |
+
|
| 12 |
+
logger = logging.getLogger(__name__)
|
| 13 |
+
|
| 14 |
+
# Static curriculum data as fallback
|
| 15 |
+
_STATIC_SUBJECTS = [
|
| 16 |
+
{
|
| 17 |
+
"id": "gen-math",
|
| 18 |
+
"code": "GEN MATH",
|
| 19 |
+
"name": "General Mathematics",
|
| 20 |
+
"gradeLevel": "Grade 11",
|
| 21 |
+
"semester": "1st Semester",
|
| 22 |
+
"color": "from-blue-500 to-cyan-500",
|
| 23 |
+
"pdfAvailable": True,
|
| 24 |
+
"topics": [
|
| 25 |
+
{"id": "gen-math-001", "name": "Patterns and Real-Life Relationships", "unit": "Patterns, Relations, and Functions"},
|
| 26 |
+
{"id": "gen-math-002", "name": "Functions as Mathematical Models", "unit": "Patterns, Relations, and Functions"},
|
| 27 |
+
{"id": "gen-math-003", "name": "Function Notation and Evaluation", "unit": "Patterns, Relations, and Functions"},
|
| 28 |
+
{"id": "gen-math-004", "name": "Domain and Range of Functions", "unit": "Patterns, Relations, and Functions"},
|
| 29 |
+
{"id": "gen-math-005", "name": "Operations on Functions", "unit": "Patterns, Relations, and Functions"},
|
| 30 |
+
{"id": "gen-math-006", "name": "Composite Functions", "unit": "Patterns, Relations, and Functions"},
|
| 31 |
+
{"id": "gen-math-007", "name": "Inverse Functions", "unit": "Patterns, Relations, and Functions"},
|
| 32 |
+
{"id": "gen-math-008", "name": "Graphs of Rational Functions", "unit": "Patterns, Relations, and Functions"},
|
| 33 |
+
{"id": "gen-math-009", "name": "Graphs of Exponential Functions", "unit": "Patterns, Relations, and Functions"},
|
| 34 |
+
{"id": "gen-math-010", "name": "Graphs of Logarithmic Functions", "unit": "Patterns, Relations, and Functions"},
|
| 35 |
+
{"id": "gen-math-011", "name": "Simple and Compound Interest", "unit": "Financial Mathematics"},
|
| 36 |
+
{"id": "gen-math-012", "name": "Simple and General Annuities", "unit": "Financial Mathematics"},
|
| 37 |
+
{"id": "gen-math-013", "name": "Present and Future Value", "unit": "Financial Mathematics"},
|
| 38 |
+
{"id": "gen-math-014", "name": "Loans, Amortization, and Sinking Funds", "unit": "Financial Mathematics"},
|
| 39 |
+
{"id": "gen-math-015", "name": "Stocks, Bonds, and Market Indices", "unit": "Financial Mathematics"},
|
| 40 |
+
{"id": "gen-math-016", "name": "Business Decision-Making with Mathematical Models", "unit": "Financial Mathematics"},
|
| 41 |
+
{"id": "gen-math-017", "name": "Propositions and Logical Connectives", "unit": "Logic and Mathematical Reasoning"},
|
| 42 |
+
{"id": "gen-math-018", "name": "Truth Values and Truth Tables", "unit": "Logic and Mathematical Reasoning"},
|
| 43 |
+
{"id": "gen-math-019", "name": "Logical Equivalence and Implication", "unit": "Logic and Mathematical Reasoning"},
|
| 44 |
+
{"id": "gen-math-020", "name": "Quantifiers and Negation", "unit": "Logic and Mathematical Reasoning"},
|
| 45 |
+
{"id": "gen-math-021", "name": "Validity of Arguments", "unit": "Logic and Mathematical Reasoning"},
|
| 46 |
+
]
|
| 47 |
+
},
|
| 48 |
+
{
|
| 49 |
+
"id": "stats-prob",
|
| 50 |
+
"code": "STAT&PROB",
|
| 51 |
+
"name": "Statistics and Probability",
|
| 52 |
+
"gradeLevel": "Grade 11",
|
| 53 |
+
"semester": "2nd Semester",
|
| 54 |
+
"color": "from-sky-500 to-cyan-500",
|
| 55 |
+
"pdfAvailable": True,
|
| 56 |
+
"topics": [
|
| 57 |
+
{"id": "stat-001", "name": "Random Variables", "unit": "Random Variables"},
|
| 58 |
+
{"id": "stat-002", "name": "Discrete Probability Distributions", "unit": "Random Variables"},
|
| 59 |
+
{"id": "stat-003", "name": "Mean and Variance of Discrete RV", "unit": "Random Variables"},
|
| 60 |
+
{"id": "stat-004", "name": "Normal Distribution", "unit": "Normal Distribution"},
|
| 61 |
+
{"id": "stat-005", "name": "Standard Normal Distribution and Z-scores", "unit": "Normal Distribution"},
|
| 62 |
+
{"id": "stat-006", "name": "Areas Under the Normal Curve", "unit": "Normal Distribution"},
|
| 63 |
+
{"id": "stat-007", "name": "Sampling Distributions", "unit": "Sampling and Estimation"},
|
| 64 |
+
{"id": "stat-008", "name": "Central Limit Theorem", "unit": "Sampling and Estimation"},
|
| 65 |
+
{"id": "stat-009", "name": "Point Estimation", "unit": "Sampling and Estimation"},
|
| 66 |
+
{"id": "stat-010", "name": "Confidence Intervals", "unit": "Sampling and Estimation"},
|
| 67 |
+
{"id": "stat-011", "name": "Hypothesis Testing Concepts", "unit": "Hypothesis Testing"},
|
| 68 |
+
{"id": "stat-012", "name": "T-test", "unit": "Hypothesis Testing"},
|
| 69 |
+
{"id": "stat-013", "name": "Z-test", "unit": "Hypothesis Testing"},
|
| 70 |
+
{"id": "stat-014", "name": "Correlation and Regression", "unit": "Correlation and Regression"},
|
| 71 |
+
]
|
| 72 |
+
},
|
| 73 |
+
{
|
| 74 |
+
"id": "pre-calc",
|
| 75 |
+
"code": "PRE-CALC",
|
| 76 |
+
"name": "Pre-Calculus",
|
| 77 |
+
"gradeLevel": "Grade 12",
|
| 78 |
+
"semester": "1st Semester",
|
| 79 |
+
"color": "from-orange-500 to-red-500",
|
| 80 |
+
"pdfAvailable": False,
|
| 81 |
+
"topics": [
|
| 82 |
+
{"id": "pre-calc-001", "name": "Conic Sections - Parabola", "unit": "Analytic Geometry"},
|
| 83 |
+
{"id": "pre-calc-002", "name": "Conic Sections - Ellipse", "unit": "Analytic Geometry"},
|
| 84 |
+
{"id": "pre-calc-003", "name": "Conic Sections - Hyperbola", "unit": "Analytic Geometry"},
|
| 85 |
+
{"id": "pre-calc-004", "name": "Conic Sections - Circle", "unit": "Analytic Geometry"},
|
| 86 |
+
{"id": "pre-calc-005", "name": "Systems of Nonlinear Equations", "unit": "Analytic Geometry"},
|
| 87 |
+
{"id": "pre-calc-006", "name": "Sequences and Series", "unit": "Series and Induction"},
|
| 88 |
+
{"id": "pre-calc-007", "name": "Arithmetic Sequences", "unit": "Series and Induction"},
|
| 89 |
+
{"id": "pre-calc-008", "name": "Geometric Sequences", "unit": "Series and Induction"},
|
| 90 |
+
{"id": "pre-calc-009", "name": "Mathematical Induction", "unit": "Series and Induction"},
|
| 91 |
+
{"id": "pre-calc-010", "name": "Binomial Theorem", "unit": "Series and Induction"},
|
| 92 |
+
{"id": "pre-calc-011", "name": "Angles and Unit Circle", "unit": "Trigonometry"},
|
| 93 |
+
{"id": "pre-calc-012", "name": "Trigonometric Functions", "unit": "Trigonometry"},
|
| 94 |
+
{"id": "pre-calc-013", "name": "Trigonometric Identities", "unit": "Trigonometry"},
|
| 95 |
+
{"id": "pre-calc-014", "name": "Sum and Difference Formulas", "unit": "Trigonometry"},
|
| 96 |
+
{"id": "pre-calc-015", "name": "Inverse Trigonometric Functions", "unit": "Trigonometry"},
|
| 97 |
+
{"id": "pre-calc-016", "name": "Polar Coordinates", "unit": "Trigonometry"},
|
| 98 |
+
]
|
| 99 |
+
},
|
| 100 |
+
{
|
| 101 |
+
"id": "basic-calc",
|
| 102 |
+
"code": "BASIC CALC",
|
| 103 |
+
"name": "Basic Calculus",
|
| 104 |
+
"gradeLevel": "Grade 12",
|
| 105 |
+
"semester": "2nd Semester",
|
| 106 |
+
"color": "from-green-500 to-teal-500",
|
| 107 |
+
"pdfAvailable": True,
|
| 108 |
+
"topics": [
|
| 109 |
+
{"id": "calc-001", "name": "Limits of Functions", "unit": "Limits"},
|
| 110 |
+
{"id": "calc-002", "name": "Limit Theorems", "unit": "Limits"},
|
| 111 |
+
{"id": "calc-003", "name": "One-Sided Limits", "unit": "Limits"},
|
| 112 |
+
{"id": "calc-004", "name": "Infinite Limits and Limits at Infinity", "unit": "Limits"},
|
| 113 |
+
{"id": "calc-005", "name": "Continuity of Functions", "unit": "Limits"},
|
| 114 |
+
{"id": "calc-006", "name": "Definition of the Derivative", "unit": "Derivatives"},
|
| 115 |
+
{"id": "calc-007", "name": "Differentiation Rules", "unit": "Derivatives"},
|
| 116 |
+
{"id": "calc-008", "name": "Chain Rule", "unit": "Derivatives"},
|
| 117 |
+
{"id": "calc-009", "name": "Implicit Differentiation", "unit": "Derivatives"},
|
| 118 |
+
{"id": "calc-010", "name": "Higher-Order Derivatives", "unit": "Derivatives"},
|
| 119 |
+
{"id": "calc-011", "name": "Related Rates", "unit": "Derivatives"},
|
| 120 |
+
{"id": "calc-012", "name": "Extrema and the First Derivative Test", "unit": "Derivatives"},
|
| 121 |
+
{"id": "calc-013", "name": "Concavity and the Second Derivative Test", "unit": "Derivatives"},
|
| 122 |
+
{"id": "calc-014", "name": "Optimization Problems", "unit": "Derivatives"},
|
| 123 |
+
{"id": "calc-015", "name": "Antiderivatives and Indefinite Integrals", "unit": "Integration"},
|
| 124 |
+
{"id": "calc-016", "name": "Definite Integrals and the FTC", "unit": "Integration"},
|
| 125 |
+
{"id": "calc-017", "name": "Integration by Substitution", "unit": "Integration"},
|
| 126 |
+
{"id": "calc-018", "name": "Area Under a Curve", "unit": "Integration"},
|
| 127 |
+
]
|
| 128 |
+
},
|
| 129 |
+
]
|
| 130 |
+
|
| 131 |
+
_firestore_db = None
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def _get_firestore_db():
|
| 135 |
+
"""Initialize Firestore client."""
|
| 136 |
+
global _firestore_db
|
| 137 |
+
if _firestore_db is not None:
|
| 138 |
+
return _firestore_db
|
| 139 |
+
|
| 140 |
+
try:
|
| 141 |
+
import firebase_admin
|
| 142 |
+
from firebase_admin import firestore
|
| 143 |
+
if not firebase_admin._apps:
|
| 144 |
+
# Try service account from env or default credentials
|
| 145 |
+
import json
|
| 146 |
+
svc_account = os.getenv("FIREBASE_SERVICE_ACCOUNT_JSON")
|
| 147 |
+
if svc_account:
|
| 148 |
+
sa_creds = json.loads(svc_account)
|
| 149 |
+
firebase_admin.initialize_app(firebase_admin.Certificate(sa_creds))
|
| 150 |
+
else:
|
| 151 |
+
firebase_admin.initialize_app()
|
| 152 |
+
_firestore_db = firestore.client()
|
| 153 |
+
return _firestore_db
|
| 154 |
+
except Exception as e:
|
| 155 |
+
logger.warning(f"Could not initialize Firestore: {e}")
|
| 156 |
+
return None
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
def get_subjects(grade_level: Optional[str] = None) -> List[Dict[str, Any]]:
|
| 160 |
+
"""
|
| 161 |
+
Fetch all subjects from Firestore.
|
| 162 |
+
Falls back to static data if Firestore unavailable.
|
| 163 |
+
Defaults to Grade 11 (SHS) if no grade specified.
|
| 164 |
+
"""
|
| 165 |
+
# Default to Grade 11 (SHS) - only serve Grade 11 students for now
|
| 166 |
+
if grade_level is None:
|
| 167 |
+
grade_level = "Grade 11"
|
| 168 |
+
|
| 169 |
+
db = _get_firestore_db()
|
| 170 |
+
|
| 171 |
+
if db is not None:
|
| 172 |
+
try:
|
| 173 |
+
subjects_ref = db.collection("subjects")
|
| 174 |
+
if grade_level:
|
| 175 |
+
subjects_ref = subjects_ref.where("gradeLevel", "==", grade_level)
|
| 176 |
+
|
| 177 |
+
docs = subjects_ref.stream()
|
| 178 |
+
subjects = []
|
| 179 |
+
for doc in docs:
|
| 180 |
+
data = doc.to_dict()
|
| 181 |
+
if data:
|
| 182 |
+
data["id"] = doc.id
|
| 183 |
+
subjects.append(data)
|
| 184 |
+
|
| 185 |
+
if subjects:
|
| 186 |
+
logger.info(f"Loaded {len(subjects)} subjects from Firestore")
|
| 187 |
+
return subjects
|
| 188 |
+
except Exception as e:
|
| 189 |
+
logger.warning(f"Firestore fetch failed, using static data: {e}")
|
| 190 |
+
|
| 191 |
+
# Static fallback
|
| 192 |
+
if grade_level:
|
| 193 |
+
return [s for s in _STATIC_SUBJECTS if s.get("gradeLevel") == grade_level]
|
| 194 |
+
return list(_STATIC_SUBJECTS)
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
def get_subject(subject_id: str) -> Optional[Dict[str, Any]]:
|
| 198 |
+
"""Fetch a single subject by ID."""
|
| 199 |
+
db = _get_firestore_db()
|
| 200 |
+
|
| 201 |
+
if db is not None:
|
| 202 |
+
try:
|
| 203 |
+
doc = db.collection("subjects").document(subject_id).get()
|
| 204 |
+
if doc.exists:
|
| 205 |
+
data = doc.to_dict()
|
| 206 |
+
data["id"] = doc.id
|
| 207 |
+
return data
|
| 208 |
+
except Exception as e:
|
| 209 |
+
logger.warning(f"Firestore fetch failed for {subject_id}: {e}")
|
| 210 |
+
|
| 211 |
+
# Static fallback
|
| 212 |
+
for subject in _STATIC_SUBJECTS:
|
| 213 |
+
if subject["id"] == subject_id:
|
| 214 |
+
return dict(subject)
|
| 215 |
+
return None
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
def get_topics(subject_id: str) -> List[Dict[str, Any]]:
|
| 219 |
+
"""Fetch all topics for a subject."""
|
| 220 |
+
subject = get_subject(subject_id)
|
| 221 |
+
if subject:
|
| 222 |
+
return subject.get("topics", [])
|
| 223 |
+
return []
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
def get_topic(subject_id: str, topic_id: str) -> Optional[Dict[str, Any]]:
|
| 227 |
+
"""Fetch a single topic."""
|
| 228 |
+
topics = get_topics(subject_id)
|
| 229 |
+
for topic in topics:
|
| 230 |
+
if topic["id"] == topic_id:
|
| 231 |
+
return topic
|
| 232 |
+
return None
|