fix(exam): block destructive template PUT once marks recorded (review #1)
PUT full-replace deletes exam_questions, and mark_entries.question_id cascades
ON DELETE — so re-saving the setup canvas after marking began would silently
wipe recorded marks. Guard: 409 if any mark_entry exists for the template's
batches. Mark-scheme edits (PATCH /questions/{id}) are unaffected.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
77bb0766ff
commit
e269e67f27
@@ -61,6 +61,20 @@ def _require_owner(ctx: ExamContext, template: Dict[str, Any]) -> None:
|
||||
raise HTTPException(status_code=403, detail="Only the template owner can modify it")
|
||||
|
||||
|
||||
def _template_has_recorded_marks(ctx: ExamContext, template_id: str) -> bool:
|
||||
"""True if any mark_entry exists for a batch of this template (→ destructive PUT is unsafe)."""
|
||||
batches = _rows(
|
||||
ctx.supabase.table("marking_batches").select("id").eq("template_id", template_id).execute()
|
||||
)
|
||||
batch_ids = [b["id"] for b in batches]
|
||||
if not batch_ids:
|
||||
return False
|
||||
marks = _rows(
|
||||
ctx.supabase.table("mark_entries").select("id").in_("batch_id", batch_ids).limit(1).execute()
|
||||
)
|
||||
return bool(marks)
|
||||
|
||||
|
||||
# ─── templates ───────────────────────────────────────────────────────────────
|
||||
|
||||
@router.post("/templates")
|
||||
@@ -150,6 +164,18 @@ async def replace_template(
|
||||
template = _fetch_template_or_404(ctx, template_id)
|
||||
_require_owner(ctx, template)
|
||||
|
||||
# Data-loss guard: the wholesale question delete below cascades to mark_entries
|
||||
# (mark_entries.question_id → exam_questions ON DELETE CASCADE). Refuse a structural
|
||||
# full-replace once any marks have been recorded against this template's batches, so
|
||||
# re-saving the setup canvas mid-marking can't silently wipe a teacher's marking work.
|
||||
# (Mark-scheme tweaks use PATCH /questions/{id}, which is unaffected.)
|
||||
if _template_has_recorded_marks(ctx, template_id):
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail="Template has recorded marks; structural full-replace is blocked. "
|
||||
"Edit questions individually via PATCH /questions/{id}.",
|
||||
)
|
||||
|
||||
# Optional template-level metadata update alongside the canvas.
|
||||
if body.meta:
|
||||
updates = {k: v for k, v in body.meta.dict().items() if v is not None}
|
||||
|
||||
Reference in New Issue
Block a user