Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ba72e4259 | ||
|
|
e48dd73fdf | ||
|
|
547836e04b | ||
|
|
df128508a3 |
@@ -0,0 +1,14 @@
|
|||||||
|
-- G1: Homework as a first-class lesson/planner field.
|
||||||
|
-- Apply to DEV Supabase only before promoting app/api changes.
|
||||||
|
|
||||||
|
alter table if exists public.taught_lessons
|
||||||
|
add column if not exists homework text not null default '';
|
||||||
|
|
||||||
|
alter table if exists public.planned_lessons
|
||||||
|
add column if not exists homework text not null default '';
|
||||||
|
|
||||||
|
comment on column public.taught_lessons.homework is
|
||||||
|
'Teacher-authored homework for this taught lesson; surfaced in lesson/timetable views.';
|
||||||
|
|
||||||
|
comment on column public.planned_lessons.homework is
|
||||||
|
'Default homework carried by a lesson plan and copied to a taught lesson when delivered.';
|
||||||
@@ -53,6 +53,7 @@ class PlannedLessonNode(CCBaseNode):
|
|||||||
subject: str
|
subject: str
|
||||||
teacher_code: str
|
teacher_code: str
|
||||||
planning_status: str
|
planning_status: str
|
||||||
|
homework: Optional[str] = None
|
||||||
topic_code: Optional[str] = None
|
topic_code: Optional[str] = None
|
||||||
topic_name: Optional[str] = None
|
topic_name: Optional[str] = None
|
||||||
lesson_code: Optional[str] = None
|
lesson_code: Optional[str] = None
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ class CreatePlanRequest(BaseModel):
|
|||||||
estimated_duration_minutes: Optional[int] = None
|
estimated_duration_minutes: Optional[int] = None
|
||||||
objectives: Optional[List[Dict[str, Any]]] = None
|
objectives: Optional[List[Dict[str, Any]]] = None
|
||||||
activities: Optional[List[Dict[str, Any]]] = None
|
activities: Optional[List[Dict[str, Any]]] = None
|
||||||
|
homework: Optional[str] = None
|
||||||
status: Optional[str] = "draft"
|
status: Optional[str] = "draft"
|
||||||
tags: Optional[List[str]] = None
|
tags: Optional[List[str]] = None
|
||||||
topic_code: Optional[str] = None
|
topic_code: Optional[str] = None
|
||||||
@@ -99,6 +100,7 @@ class UpdatePlanRequest(BaseModel):
|
|||||||
estimated_duration_minutes: Optional[int] = None
|
estimated_duration_minutes: Optional[int] = None
|
||||||
objectives: Optional[List[Dict[str, Any]]] = None
|
objectives: Optional[List[Dict[str, Any]]] = None
|
||||||
activities: Optional[List[Dict[str, Any]]] = None
|
activities: Optional[List[Dict[str, Any]]] = None
|
||||||
|
homework: Optional[str] = None
|
||||||
status: Optional[str] = None
|
status: Optional[str] = None
|
||||||
tags: Optional[List[str]] = None
|
tags: Optional[List[str]] = None
|
||||||
topic_code: Optional[str] = None
|
topic_code: Optional[str] = None
|
||||||
@@ -249,6 +251,7 @@ async def create_plan(
|
|||||||
"status": body.status or "draft",
|
"status": body.status or "draft",
|
||||||
"objectives": body.objectives or [],
|
"objectives": body.objectives or [],
|
||||||
"activities": body.activities or [],
|
"activities": body.activities or [],
|
||||||
|
"homework": body.homework or "",
|
||||||
"tags": body.tags or [],
|
"tags": body.tags or [],
|
||||||
}
|
}
|
||||||
for field in (
|
for field in (
|
||||||
@@ -362,7 +365,7 @@ async def update_plan(
|
|||||||
updates: Dict[str, Any] = {}
|
updates: Dict[str, Any] = {}
|
||||||
for field in (
|
for field in (
|
||||||
"title", "class_id", "subject", "year_group", "estimated_duration_minutes",
|
"title", "class_id", "subject", "year_group", "estimated_duration_minutes",
|
||||||
"objectives", "activities", "status", "tags", "topic_code",
|
"objectives", "activities", "homework", "status", "tags", "topic_code",
|
||||||
"whiteboard_room_id", "course_id", "sequence_number",
|
"whiteboard_room_id", "course_id", "sequence_number",
|
||||||
):
|
):
|
||||||
val = getattr(body, field)
|
val = getattr(body, field)
|
||||||
@@ -425,7 +428,7 @@ async def deliver_plan(
|
|||||||
|
|
||||||
plan_res = (
|
plan_res = (
|
||||||
sb.supabase.table("planned_lessons")
|
sb.supabase.table("planned_lessons")
|
||||||
.select("id, whiteboard_room_id")
|
.select("id, whiteboard_room_id, homework")
|
||||||
.eq("id", plan_id)
|
.eq("id", plan_id)
|
||||||
.eq("institute_id", institute_id)
|
.eq("institute_id", institute_id)
|
||||||
.single()
|
.single()
|
||||||
@@ -456,6 +459,19 @@ async def deliver_plan(
|
|||||||
|
|
||||||
res = sb.supabase.table("lesson_deliveries").insert(row).execute()
|
res = sb.supabase.table("lesson_deliveries").insert(row).execute()
|
||||||
delivery = (res.data or [{}])[0]
|
delivery = (res.data or [{}])[0]
|
||||||
|
|
||||||
|
# If the plan has default homework and was delivered into a taught lesson,
|
||||||
|
# copy it onto the lesson so homework appears in daily/timetable views.
|
||||||
|
plan_homework = plan_res.data.get("homework")
|
||||||
|
if body.taught_lesson_id and plan_homework is not None:
|
||||||
|
try:
|
||||||
|
sb.supabase.table("taught_lessons").update({
|
||||||
|
"homework": plan_homework,
|
||||||
|
"updated_at": datetime.utcnow().isoformat(),
|
||||||
|
}).eq("id", body.taught_lesson_id).eq("teacher_id", user_id).execute()
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Could not copy homework to taught_lesson {body.taught_lesson_id}: {e}")
|
||||||
|
|
||||||
logger.info(f"Lesson delivery created: {delivery.get('id')} for plan {plan_id} by {user_id}")
|
logger.info(f"Lesson delivery created: {delivery.get('id')} for plan {plan_id} by {user_id}")
|
||||||
return delivery
|
return delivery
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Taught Lessons Router — materialization and lesson CRUD.
|
|||||||
POST /materialize — slot template × academic_periods → taught_lessons rows
|
POST /materialize — slot template × academic_periods → taught_lessons rows
|
||||||
GET /lessons — teacher's lessons for a date range
|
GET /lessons — teacher's lessons for a date range
|
||||||
GET /lessons/{id} — single lesson detail
|
GET /lessons/{id} — single lesson detail
|
||||||
PATCH /lessons/{id} — update lesson_plan, notes, status (teacher-owned)
|
PATCH /lessons/{id} — update lesson_plan, homework, notes, status (teacher-owned)
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
@@ -44,6 +44,7 @@ def _require_institute(user_id: str) -> str:
|
|||||||
|
|
||||||
class UpdateLessonRequest(BaseModel):
|
class UpdateLessonRequest(BaseModel):
|
||||||
lesson_plan: Optional[Dict[str, Any]] = None
|
lesson_plan: Optional[Dict[str, Any]] = None
|
||||||
|
homework: Optional[str] = None
|
||||||
notes: Optional[str] = None
|
notes: Optional[str] = None
|
||||||
status: Optional[str] = None # planned | in_progress | completed | cancelled | substituted
|
status: Optional[str] = None # planned | in_progress | completed | cancelled | substituted
|
||||||
|
|
||||||
@@ -310,7 +311,7 @@ async def get_lessons(
|
|||||||
lessons = (
|
lessons = (
|
||||||
sb.supabase.table("taught_lessons")
|
sb.supabase.table("taught_lessons")
|
||||||
.select(
|
.select(
|
||||||
"id,date,period_code,week_cycle,day_of_week,status,lesson_plan,notes,whiteboard_room_id,"
|
"id,date,period_code,week_cycle,day_of_week,status,lesson_plan,homework,notes,whiteboard_room_id,"
|
||||||
"class_id,academic_period_id"
|
"class_id,academic_period_id"
|
||||||
)
|
)
|
||||||
.eq("teacher_id", user_id)
|
.eq("teacher_id", user_id)
|
||||||
@@ -418,13 +419,15 @@ async def update_lesson(
|
|||||||
body: UpdateLessonRequest,
|
body: UpdateLessonRequest,
|
||||||
credentials: dict = Depends(SupabaseBearer()),
|
credentials: dict = Depends(SupabaseBearer()),
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Teacher updates their own lesson content: plan, notes, status."""
|
"""Teacher updates their own lesson content: plan, homework, notes, status."""
|
||||||
user_id = credentials.get("sub", "")
|
user_id = credentials.get("sub", "")
|
||||||
sb = _sb()
|
sb = _sb()
|
||||||
|
|
||||||
updates: Dict[str, Any] = {}
|
updates: Dict[str, Any] = {}
|
||||||
if body.lesson_plan is not None:
|
if body.lesson_plan is not None:
|
||||||
updates["lesson_plan"] = body.lesson_plan
|
updates["lesson_plan"] = body.lesson_plan
|
||||||
|
if body.homework is not None:
|
||||||
|
updates["homework"] = body.homework
|
||||||
if body.notes is not None:
|
if body.notes is not None:
|
||||||
updates["notes"] = body.notes
|
updates["notes"] = body.notes
|
||||||
if body.status is not None:
|
if body.status is not None:
|
||||||
@@ -508,7 +511,7 @@ async def get_student_lessons(
|
|||||||
lessons = (
|
lessons = (
|
||||||
sb.supabase.table("taught_lessons")
|
sb.supabase.table("taught_lessons")
|
||||||
.select(
|
.select(
|
||||||
"id,date,period_code,week_cycle,day_of_week,status,lesson_plan,notes,whiteboard_room_id,"
|
"id,date,period_code,week_cycle,day_of_week,status,lesson_plan,homework,notes,whiteboard_room_id,"
|
||||||
"class_id,academic_period_id,teacher_id"
|
"class_id,academic_period_id,teacher_id"
|
||||||
)
|
)
|
||||||
.in_("class_id", class_ids)
|
.in_("class_id", class_ids)
|
||||||
|
|||||||
@@ -675,7 +675,7 @@ def _sync_taught_lessons_to_neo4j(
|
|||||||
"""
|
"""
|
||||||
lessons = (
|
lessons = (
|
||||||
sb.supabase.table("taught_lessons")
|
sb.supabase.table("taught_lessons")
|
||||||
.select("id,neo4j_node_id,academic_period_id,teacher_id,date,period_code,week_cycle,day_of_week,status")
|
.select("id,neo4j_node_id,academic_period_id,teacher_id,date,period_code,week_cycle,day_of_week,status,homework")
|
||||||
.eq("institute_id", institute_id)
|
.eq("institute_id", institute_id)
|
||||||
.execute()
|
.execute()
|
||||||
.data or []
|
.data or []
|
||||||
@@ -728,6 +728,7 @@ def _sync_taught_lessons_to_neo4j(
|
|||||||
SET tl.date = date($date), tl.period_code = $pcode,
|
SET tl.date = date($date), tl.period_code = $pcode,
|
||||||
tl.week_cycle = $wc, tl.day_of_week = $dow,
|
tl.week_cycle = $wc, tl.day_of_week = $dow,
|
||||||
tl.status = $status,
|
tl.status = $status,
|
||||||
|
tl.homework = $homework,
|
||||||
tl.node_storage_path = $path
|
tl.node_storage_path = $path
|
||||||
""",
|
""",
|
||||||
id=tl_id,
|
id=tl_id,
|
||||||
@@ -736,6 +737,7 @@ def _sync_taught_lessons_to_neo4j(
|
|||||||
wc=lesson.get("week_cycle", ""),
|
wc=lesson.get("week_cycle", ""),
|
||||||
dow=lesson.get("day_of_week", ""),
|
dow=lesson.get("day_of_week", ""),
|
||||||
status=lesson.get("status", "planned"),
|
status=lesson.get("status", "planned"),
|
||||||
|
homework=lesson.get("homework") or "",
|
||||||
path=f"taught_lessons/{tl_id}",
|
path=f"taught_lessons/{tl_id}",
|
||||||
)
|
)
|
||||||
count += 1
|
count += 1
|
||||||
|
|||||||
@@ -80,6 +80,9 @@ class ResponseAreaPayload(BaseModel):
|
|||||||
] = None
|
] = None
|
||||||
# Optional Context differentiation (v1 generic; future graph/chart/data_table/diagram/code_block/passage).
|
# Optional Context differentiation (v1 generic; future graph/chart/data_table/diagram/code_block/passage).
|
||||||
context_type: Optional[str] = None
|
context_type: Optional[str] = None
|
||||||
|
# Rich recognition payload (75-exam-marker-region-meta.sql): figure name/description, OMR geometry, unit…
|
||||||
|
# Carried on canvas save so a named context figure survives a round-trip.
|
||||||
|
meta: Optional[Dict[str, Any]] = None
|
||||||
source: Literal["manual", "ai"] = "manual"
|
source: Literal["manual", "ai"] = "manual"
|
||||||
confirmed: bool = True
|
confirmed: bool = True
|
||||||
confidence: Optional[float] = Field(default=None, ge=0, le=1)
|
confidence: Optional[float] = Field(default=None, ge=0, le=1)
|
||||||
|
|||||||
@@ -745,6 +745,9 @@ def _map_service_contract_to_rows(template_id: str, contract: Dict[str, Any],
|
|||||||
"bounds": _frac_box_to_canvas(q.get("bounds"), q.get("page") or 1, pages),
|
"bounds": _frac_box_to_canvas(q.get("bounds"), q.get("page") or 1, pages),
|
||||||
"page": q.get("page"), "source": "ai", "confirmed": False,
|
"page": q.get("page"), "source": "ai", "confirmed": False,
|
||||||
"confidence": _safe_confidence(q.get("confidence")), "derivation": "extract-service",
|
"confidence": _safe_confidence(q.get("confidence")), "derivation": "extract-service",
|
||||||
|
# analyse contract v2 (migration 78): command verb + stem prose per part
|
||||||
|
"command_word": (q.get("command_word") or None) if not q.get("is_container") else None,
|
||||||
|
"preamble": q.get("preamble") or None,
|
||||||
})
|
})
|
||||||
for q in questions: # FK safety: de-parent a dangling parent_id
|
for q in questions: # FK safety: de-parent a dangling parent_id
|
||||||
if q["parent_id"] and q["parent_id"] not in q_ids:
|
if q["parent_id"] and q["parent_id"] not in q_ids:
|
||||||
@@ -799,6 +802,10 @@ def _run_service_extract_merge(ctx: ExamContext, template_id: str, pdf_bytes: by
|
|||||||
updates: Dict[str, Any] = {"extraction_meta": {
|
updates: Dict[str, Any] = {"extraction_meta": {
|
||||||
"engine": "extract-service", "slug": slug, "audit": meta.get("audit") or {},
|
"engine": "extract-service", "slug": slug, "audit": meta.get("audit") or {},
|
||||||
"counts": {k: len(v) for k, v in rows.items()},
|
"counts": {k: len(v) for k, v in rows.items()},
|
||||||
|
# question-layout signals for the setup UI: paper sections (e.g. A-level Section A/B) + EITHER/OR choices
|
||||||
|
"sections": meta.get("sections") or [],
|
||||||
|
"choice_groups": meta.get("choice_groups") or [],
|
||||||
|
"marks_confidence": meta.get("marks_confidence"),
|
||||||
}}
|
}}
|
||||||
n_pages = meta.get("n_pages") or meta.get("pages")
|
n_pages = meta.get("n_pages") or meta.get("pages")
|
||||||
if n_pages:
|
if n_pages:
|
||||||
@@ -1140,6 +1147,7 @@ async def replace_template(
|
|||||||
"kind": ra.kind,
|
"kind": ra.kind,
|
||||||
"response_form": ra.response_form,
|
"response_form": ra.response_form,
|
||||||
"context_type": ra.context_type, # 73: optional Context differentiation
|
"context_type": ra.context_type, # 73: optional Context differentiation
|
||||||
|
"meta": ra.meta, # 75: rich recognition payload (name/description/OMR/…)
|
||||||
"source": ra.source,
|
"source": ra.source,
|
||||||
"confirmed": ra.confirmed,
|
"confirmed": ra.confirmed,
|
||||||
"confidence": ra.confidence,
|
"confidence": ra.confidence,
|
||||||
|
|||||||
Reference in New Issue
Block a user