From 62234dbbcb02121f77e1e967e7ee5bdd9423563a Mon Sep 17 00:00:00 2001 From: CC Worker Date: Sat, 6 Jun 2026 18:43:09 +0000 Subject: [PATCH] fix(exam): blank total only for absent AND unmarked; flip status on mark A roster student starts 'absent' and a direct mark would otherwise still show a blank total. Now total is blank only when absent with no marks; recording a mark advances the submission out of absent/unmatched to 'marking'. Co-Authored-By: Claude Opus 4.8 --- routers/exam/batches.py | 18 +++++++++++++++--- tests/test_exam_batches.py | 11 +++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/routers/exam/batches.py b/routers/exam/batches.py index 45bba07..701d95f 100644 --- a/routers/exam/batches.py +++ b/routers/exam/batches.py @@ -187,8 +187,14 @@ def _assemble_results(ctx: ExamContext, batch: Dict[str, Any]) -> Dict[str, Any] results = [] for s in submissions: # every submission incl. absent → A7 sub_marks = by_sub.get(s["id"], {}) - is_absent = s.get("status") == "absent" - total = None if is_absent else sum(v or 0 for v in sub_marks.values()) + # Blank total ONLY for a genuine no-show (absent AND nothing marked). A student with any + # mark gets a real total regardless of status; a present-but-unmarked student totals 0. + if sub_marks: + total = sum(v or 0 for v in sub_marks.values()) + elif s.get("status") == "absent": + total = None + else: + total = 0 results.append({ "submission_id": s["id"], "student_id": s.get("student_id"), @@ -248,7 +254,7 @@ async def upsert_mark( # Derive batch_id from the submission (as-user read → also enforces the caller owns the batch # the submission belongs to). The client never supplies the RLS scoping key directly. submission = _first( - ctx.supabase.table("student_submissions").select("id, batch_id").eq("id", body.submission_id).limit(1).execute() + ctx.supabase.table("student_submissions").select("id, batch_id, status").eq("id", body.submission_id).limit(1).execute() ) if not submission: raise HTTPException(status_code=404, detail="Submission not found") @@ -273,6 +279,12 @@ async def upsert_mark( upserted = _first(ctx.supabase.table("mark_entries").upsert(row).execute()) if not upserted: raise HTTPException(status_code=500, detail="Failed to upsert mark") + + # A marked student is, by definition, not absent — advance the submission out of the + # no-submission states so results/queue reflect that marking has started. + if submission.get("status") in ("absent", "unmatched"): + ctx.supabase.table("student_submissions").update({"status": "marking"}).eq("id", body.submission_id).execute() + return upserted diff --git a/tests/test_exam_batches.py b/tests/test_exam_batches.py index d7e9401..299bb7a 100644 --- a/tests/test_exam_batches.py +++ b/tests/test_exam_batches.py @@ -233,6 +233,17 @@ def test_upsert_mark_derives_batch_and_roundtrips(): assert sum(1 for m in store["mark_entries"] if m["id"] == "mk-1") == 1 +def test_upsert_mark_flips_absent_submission_to_marking(): + store = _batch_with_cohort() # sub2 starts 'absent' + c = make_client(store) + c.put("/api/exam/marks/mk-2", json={"submission_id": "sub2", "question_id": "q1", "awarded_marks": 2}) + sub2 = next(s for s in store["student_submissions"] if s["id"] == "sub2") + assert sub2["status"] == "marking" + # results now show a real total for the (formerly absent) marked student + res = {r["submission_id"]: r for r in c.get("/api/exam/batches/b1/results").json()["results"]} + assert res["sub2"]["total"] == 2 + + def test_upsert_mark_submission_404(): c = make_client(_batch_with_cohort()) assert c.put("/api/exam/marks/mk-x", json={"submission_id": "nope", "question_id": "q1", "awarded_marks": 1}).status_code == 404