feat(exam): batches, scans, marks, results, CSV (S4-6)
Adds routers/exam/batches.py (mounted alongside templates under /api/exam):
- POST/GET /batches — batch creation seeds the cohort from class_students AS
THE USER (cs_read requires caller teaches/admins the class); each active
enrollee becomes a student_submissions row (status='absent') so no student
is ever dropped from results (A7). Display names denormalised via a
documented service-role profiles read (deny-all as-user, E4).
- GET /batches/{id}/queue — submissions + per-submission mark counts + progress.
- GET /batches/{id}/results + /csv — every roster student incl. absent (blank
marks/total); CSV row always present (A7 baked into the contract).
- PUT /marks/{id} — upsert; batch_id derived server-side from the submission
(client never supplies the RLS scoping key).
- POST /batches/{id}/scans — E3 guards: MIME check, hard size ceiling (chunked
read), %PDF magic-byte sniff; owner-only; stores via service-role storage;
manual/ordered matching (QR-decode is a follow-on, no QR fixtures yet).
Unit tests cover batch/roster-seed/list, queue, results+CSV A7, mark upsert
round-trip, and all scan guards + owner check.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
96f9fb2446
commit
5ad9c01cde
@@ -100,6 +100,34 @@ async def get_exam_context(payload: Dict[str, Any] = Depends(auth)) -> ExamConte
|
||||
return ExamContext(user_id, access_token, supabase, institute_ids)
|
||||
|
||||
|
||||
def resolve_student_names(student_ids: List[str]) -> Dict[str, str]:
|
||||
"""Map profile id → display name for roster students (batch-creation denormalisation).
|
||||
|
||||
Documented service-role exception (S1, mirrors lookup_exam_code): `profiles` has no as-user
|
||||
SELECT policy (E4), so the roster's display names can't be read as-the-user. The caller's
|
||||
right to the roster itself is already enforced as-user (class_students.cs_read requires the
|
||||
caller to teach/admin the class); this only resolves names for ids already authorised, and
|
||||
the result is denormalised onto student_submissions so later reads need no profiles access.
|
||||
"""
|
||||
if not student_ids:
|
||||
return {}
|
||||
try:
|
||||
sb = SupabaseServiceRoleClient().supabase
|
||||
res = (
|
||||
sb.table("profiles")
|
||||
.select("id, full_name, display_name, email")
|
||||
.in_("id", list(student_ids))
|
||||
.execute()
|
||||
)
|
||||
out: Dict[str, str] = {}
|
||||
for p in getattr(res, "data", None) or []:
|
||||
out[p["id"]] = p.get("full_name") or p.get("display_name") or p.get("email") or ""
|
||||
return out
|
||||
except Exception as exc:
|
||||
logger.warning(f"student name resolution failed: {exc}")
|
||||
return {}
|
||||
|
||||
|
||||
def lookup_exam_code(exam_id: str) -> Optional[str]:
|
||||
"""Resolve eb_exams.exam_code for a catalogue paper (denormalised onto the template).
|
||||
|
||||
|
||||
Reference in New Issue
Block a user