Spaces:
Sleeping
Sleeping
| """Dashboard API. | |
| Two access levels share these routes: | |
| - Admins (is_admin) see ALL teachers' data and can manage users. | |
| - Regular teachers see ONLY their own data (their quizzes/students/results) and | |
| cannot reach user-management or other teachers' records. | |
| Scoping is enforced server-side: list endpoints filter by the caller's teacher | |
| id, and detail endpoints verify ownership before returning anything. | |
| """ | |
| from __future__ import annotations | |
| from typing import Optional | |
| from fastapi import APIRouter, Depends, HTTPException | |
| from pydantic import BaseModel | |
| from .auth import get_current_admin, get_current_user | |
| from .database import ( | |
| admin_list_sessions, | |
| admin_list_students, | |
| delete_session, | |
| delete_user, | |
| get_parsed_data, | |
| get_quiz_results, | |
| get_raw_files, | |
| get_schema_version, | |
| get_session, | |
| get_stats, | |
| get_student, | |
| get_student_timeline, | |
| list_all_users, | |
| set_user_admin, | |
| ) | |
| from .storage import create_signed_url | |
| router = APIRouter(prefix="/api/admin", tags=["admin"]) | |
| def _scope(user: dict) -> Optional[int]: | |
| """None for admins (see everything), else the teacher id to scope to.""" | |
| return None if user.get("is_admin") else user["id"] | |
| def _owns_quiz(user: dict, quiz: dict) -> bool: | |
| return user.get("is_admin") or quiz.get("teacher_id") == user["id"] | |
| def _owns_student(user: dict, student: dict) -> bool: | |
| return user.get("is_admin") or student.get("teacher_id") == user["id"] | |
| async def dashboard_stats(user=Depends(get_current_user)): | |
| stats = await get_stats(_scope(user)) | |
| stats["is_admin"] = bool(user.get("is_admin")) | |
| if user.get("is_admin"): | |
| stats["schema_version"] = await get_schema_version() | |
| return stats | |
| # ---- User management (admin only) ---- | |
| async def admin_users(_admin=Depends(get_current_admin)): | |
| return {"users": await list_all_users()} | |
| class SetAdminRequest(BaseModel): | |
| is_admin: bool | |
| async def admin_set_admin(user_id: int, body: SetAdminRequest, admin=Depends(get_current_admin)): | |
| if user_id == admin["id"] and not body.is_admin: | |
| raise HTTPException(status_code=400, detail="You cannot remove your own admin access") | |
| await set_user_admin(user_id, body.is_admin) | |
| return {"status": "ok", "user_id": user_id, "is_admin": body.is_admin} | |
| async def admin_delete_user(user_id: int, admin=Depends(get_current_admin)): | |
| if user_id == admin["id"]: | |
| raise HTTPException(status_code=400, detail="You cannot delete your own account here") | |
| await delete_user(user_id) | |
| return {"status": "deleted"} | |
| # ---- Sessions / quizzes (scoped) ---- | |
| async def dashboard_sessions(user=Depends(get_current_user)): | |
| return {"sessions": await admin_list_sessions(_scope(user))} | |
| async def dashboard_session_detail(session_id: int, user=Depends(get_current_user)): | |
| """Full quiz detail: raw files (signed URLs), structured parsed data, results.""" | |
| quiz = await get_session(session_id) | |
| if not quiz or not _owns_quiz(user, quiz): | |
| raise HTTPException(status_code=404, detail="Session not found") | |
| parsed = {} | |
| for item in await get_parsed_data(session_id): | |
| parsed[item["data_type"]] = item["structured_data"] | |
| raw_files = [] | |
| for f in await get_raw_files(session_id): | |
| f["download_url"] = await create_signed_url(f.get("storage_path", "")) | |
| raw_files.append(f) | |
| return { | |
| "quiz": quiz, | |
| "raw_files": raw_files, | |
| "parsed_data": parsed, | |
| "results": await get_quiz_results(session_id), | |
| } | |
| async def dashboard_delete_session(session_id: int, user=Depends(get_current_user)): | |
| quiz = await get_session(session_id) | |
| if not quiz or not _owns_quiz(user, quiz): | |
| raise HTTPException(status_code=404, detail="Session not found") | |
| await delete_session(session_id) | |
| return {"status": "deleted"} | |
| # ---- Students (scoped) ---- | |
| async def dashboard_students(user=Depends(get_current_user)): | |
| return {"students": await admin_list_students(_scope(user))} | |
| async def dashboard_student_detail(student_id: int, user=Depends(get_current_user)): | |
| student = await get_student(student_id) | |
| if not student or not _owns_student(user, student): | |
| raise HTTPException(status_code=404, detail="Student not found") | |
| return {"student": student, "timeline": await get_student_timeline(student_id)} | |