feat(exam): Neo4j projection on template save + neo4j-sync (S4-7)
modules/database/services/exam_projection.py projects a saved template into
cc.public.exams: ExamPaper -> Question/Part -> Region + Part-[:ASSESSES]->
SpecPoint, joined by shared UUIDs (exam_questions.id, exam_response_areas.id,
exam_code, spec_code). Full re-sync per exam_code (idempotent). Reads via
service role + writes via system Neo4j driver (R3.5.1 documented graph-writer).
Wiring (R3.5.4/R5.3):
- PUT /templates/{id} enqueues project_template_safe via BackgroundTasks
(swallows failures so a graph hiccup never fails the canvas save).
- POST /templates/{id}/neo4j-sync — manual trigger, as-user auth + owner check,
runs synchronously and returns projection counts.
Unit tests: projection scheduled on PUT; neo4j-sync owner/403/404.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
98be55ab57
commit
77bb0766ff
@@ -15,8 +15,9 @@ from __future__ import annotations
|
||||
import os
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException
|
||||
|
||||
from modules.database.services.exam_projection import project_template, project_template_safe
|
||||
from modules.logger_tool import initialise_logger
|
||||
from routers.exam.dependencies import ExamContext, get_exam_context, lookup_exam_code
|
||||
from routers.exam.schemas import (
|
||||
@@ -137,6 +138,7 @@ async def get_template(
|
||||
async def replace_template(
|
||||
template_id: str,
|
||||
body: TemplateReplaceRequest,
|
||||
background_tasks: BackgroundTasks,
|
||||
ctx: ExamContext = Depends(get_exam_context),
|
||||
) -> Dict[str, Any]:
|
||||
"""Full-replace canvas save (R5.2). Replaces questions/response_areas/boundaries wholesale.
|
||||
@@ -223,6 +225,10 @@ async def replace_template(
|
||||
f"Exam template {template_id} replaced: {len(body.questions)} questions, "
|
||||
f"{len(body.response_areas)} regions, {len(body.boundaries)} boundaries"
|
||||
)
|
||||
# R3.5.4: a successful save enqueues a graph projection into cc.public.exams. BackgroundTasks
|
||||
# is acceptable for Sprint 4 (durability via a real queue is a later step); failures are
|
||||
# swallowed so the canvas save itself never fails on a graph hiccup.
|
||||
background_tasks.add_task(project_template_safe, template_id)
|
||||
return await get_template(template_id, ctx)
|
||||
|
||||
|
||||
@@ -238,6 +244,24 @@ async def archive_template(
|
||||
return {"status": "archived", "id": template_id}
|
||||
|
||||
|
||||
@router.post("/templates/{template_id}/neo4j-sync")
|
||||
async def neo4j_sync(
|
||||
template_id: str,
|
||||
ctx: ExamContext = Depends(get_exam_context),
|
||||
) -> Dict[str, Any]:
|
||||
"""Manual graph-projection trigger (R5.3) for dev/backfill — runs synchronously and returns
|
||||
counts. Auth/ownership is checked as-the-user; the projection itself uses service role
|
||||
(R3.5.1, the documented graph-writer path)."""
|
||||
template = _fetch_template_or_404(ctx, template_id)
|
||||
_require_owner(ctx, template)
|
||||
try:
|
||||
counts = project_template(template_id)
|
||||
except Exception as exc:
|
||||
logger.error(f"Manual neo4j-sync failed for template {template_id}: {exc}")
|
||||
raise HTTPException(status_code=502, detail=f"Projection failed: {exc}")
|
||||
return {"status": "ok", "projection": counts}
|
||||
|
||||
|
||||
# ─── questions (granular edit path, R5.2) ────────────────────────────────────
|
||||
|
||||
@router.patch("/questions/{question_id}")
|
||||
|
||||
Reference in New Issue
Block a user