Spaces:
Running
Running
| """Class Analytics API routes.""" | |
| import time | |
| import logging | |
| from typing import Optional | |
| from fastapi import APIRouter, HTTPException, Request | |
| logger = logging.getLogger("mathpulse.class_analytics_routes") | |
| router = APIRouter(prefix="/api/analytics/class", tags=["class-analytics"]) | |
| # Rate limit: 1 refresh per 5 min per class | |
| _refresh_timestamps: dict[str, float] = {} | |
| def _require_teacher(request: Request): | |
| user = getattr(request.state, "user", None) | |
| if not user: | |
| raise HTTPException(status_code=401, detail="Authentication required") | |
| if user.role not in ("teacher", "admin"): | |
| raise HTTPException(status_code=403, detail="Teacher or admin access required") | |
| return user | |
| async def get_class_analytics(class_id: str, request: Request, refresh: bool = False): | |
| """Get full class analytics report. Cached for 30 min unless refresh=true.""" | |
| user = _require_teacher(request) | |
| from services.class_analytics_engine import get_class_analytics_engine | |
| engine = get_class_analytics_engine() | |
| report = await engine.get_class_analytics(class_id, user.uid, force_refresh=refresh) | |
| return report.model_dump() | |
| async def get_class_students( | |
| class_id: str, request: Request, filter: Optional[str] = "all" | |
| ): | |
| """Get student summaries for a class with optional filtering.""" | |
| user = _require_teacher(request) | |
| from services.class_analytics_engine import get_class_analytics_engine | |
| engine = get_class_analytics_engine() | |
| report = await engine.get_class_analytics(class_id, user.uid) | |
| students = report.students | |
| if filter == "top_performers": | |
| students = sorted( | |
| [s for s in students if s.quiz_attempt_count > 0], | |
| key=lambda s: s.avg_score, | |
| reverse=True, | |
| )[:10] | |
| elif filter == "needs_attention": | |
| students = sorted( | |
| [s for s in students if s.risk_level in ("High Risk", "Critical", "Unassessed")], | |
| key=lambda s: s.avg_score, | |
| ) | |
| return [s.model_dump() for s in students] | |
| async def get_class_topics(class_id: str, request: Request): | |
| """Get topic performance sorted by accuracy (worst first).""" | |
| user = _require_teacher(request) | |
| from services.class_analytics_engine import get_class_analytics_engine | |
| engine = get_class_analytics_engine() | |
| report = await engine.get_class_analytics(class_id, user.uid) | |
| topics = report.insights.topic_performance if report.insights else [] | |
| return [t.model_dump() for t in topics] | |
| async def refresh_class_insights(class_id: str, request: Request): | |
| """Force regeneration of AI insights. Rate limited: 1 per 5 min per class.""" | |
| user = _require_teacher(request) | |
| last_refresh = _refresh_timestamps.get(class_id, 0) | |
| if time.time() - last_refresh < 300: | |
| raise HTTPException( | |
| status_code=429, | |
| detail="Insights can only be refreshed once every 5 minutes.", | |
| ) | |
| from services.class_analytics_engine import get_class_analytics_engine | |
| engine = get_class_analytics_engine() | |
| engine.invalidate_cache(class_id) | |
| report = await engine.get_class_analytics(class_id, user.uid, force_refresh=True) | |
| _refresh_timestamps[class_id] = time.time() | |
| if report.insights: | |
| return report.insights.model_dump() | |
| return {"error": "Failed to generate insights"} | |
| async def invalidate_class_cache(class_id: str, request: Request): | |
| """Invalidate cached analytics for a class (called after quiz completion).""" | |
| # Allow any authenticated user (student completing quiz triggers this) | |
| user = getattr(request.state, "user", None) | |
| if not user: | |
| raise HTTPException(status_code=401, detail="Authentication required") | |
| from services.class_analytics_engine import get_class_analytics_engine | |
| engine = get_class_analytics_engine() | |
| engine.invalidate_cache(class_id) | |
| return {"status": "cache_invalidated", "class_id": class_id} | |