From b71995f4fbea6a9f0b4af61d83517cfeda71f22c Mon Sep 17 00:00:00 2001 From: kcar Date: Wed, 27 May 2026 12:58:47 +0100 Subject: [PATCH] fix(graph-tree): switch class/timetable data source to Supabase - _query_teacher_classes: now queries class_teachers + class_students tables instead of non-existent Neo4j TEACHER_HAS_CLASS relationship - _build_classes_section: updated signature to (user_id, institute_id, institute_db) - _build_timetable_section: updated signature; loads classes from Supabase, not Neo4j TIMETABLE_HAS_CLASS - TeacherTimetable lazy handler: simplified (classes pre-loaded in section builder) - AcademicWeek timetable-term: Supabase taught_lessons query by date range instead of Neo4j - expose supabase_institute_id from _resolve_institute call Co-Authored-By: Claude Sonnet 4.6 --- routers/database/tools/graph_tree_router.py | 193 ++++++++++---------- 1 file changed, 101 insertions(+), 92 deletions(-) diff --git a/routers/database/tools/graph_tree_router.py b/routers/database/tools/graph_tree_router.py index 7d20e4a..979f87f 100644 --- a/routers/database/tools/graph_tree_router.py +++ b/routers/database/tools/graph_tree_router.py @@ -166,27 +166,55 @@ def _query_month_days(month_uuid: str) -> List[Dict]: return [] -def _query_teacher_classes(institute_db: str, teacher_uuid: str) -> List[Dict]: +def _query_teacher_classes(user_id: str, institute_id: str, institute_db: str) -> List[Dict]: + """Query classes for a teacher or student from Supabase (source of truth).""" try: - with driver_tools.get_session(database=institute_db) as session: - result = session.run( - "MATCH (t:Teacher {uuid_string: $uuid})-[:TEACHER_HAS_CLASS]->(c:SubjectClass) " - "RETURN c ORDER BY c.name", - uuid=teacher_uuid, - ) - return [ - { - "neo4j_node_id": r["c"]["uuid_string"], - "label": r["c"].get("name") or "Class", - "node_type": "SubjectClass", - "neo4j_db_name": institute_db, - "is_section": False, - "has_children": True, - } - for r in result - ] + sb = _sb() + teacher_rows = ( + sb.supabase.table("class_teachers") + .select("class_id, is_primary") + .eq("teacher_id", user_id) + .execute() + .data or [] + ) + teacher_class_ids = {r["class_id"] for r in teacher_rows} + student_rows = ( + sb.supabase.table("class_students") + .select("class_id") + .eq("student_id", user_id) + .eq("status", "active") + .execute() + .data or [] + ) + student_class_ids = {r["class_id"] for r in student_rows} + all_ids = list(teacher_class_ids | student_class_ids) + if not all_ids: + return [] + classes = ( + sb.supabase.table("classes") + .select("id, name, code, subject") + .in_("id", all_ids) + .eq("institute_id", institute_id) + .eq("is_active", True) + .order("name") + .execute() + .data or [] + ) + result = [] + for c in classes: + role = "teacher" if c["id"] in teacher_class_ids else "student" + result.append({ + "neo4j_node_id": c["id"], + "label": c.get("name") or "Class", + "node_type": "SubjectClass", + "neo4j_db_name": institute_db, + "is_section": False, + "has_children": True, + "neo4j_props": {"role": role, "subject": c.get("subject") or ""}, + }) + return result except Exception as e: - logger.warning(f"Could not query classes for teacher {teacher_uuid}: {e}") + logger.warning(f"Could not query classes for user {user_id}: {e}") return [] @@ -235,8 +263,8 @@ def _build_calendar_section() -> Dict: return _section("calendar", "Calendar", "classroomcopilot", "empty") -def _build_timetable_section(institute_db: Optional[str], teacher_uuid: Optional[str]) -> Dict: - if not institute_db or not teacher_uuid: +def _build_timetable_section(user_id: str, institute_id: Optional[str], institute_db: Optional[str], teacher_uuid: Optional[str]) -> Dict: + if not institute_db or not teacher_uuid or not institute_id: return _section("timetable", "My Timetable", "", "no_school") try: @@ -249,27 +277,8 @@ def _build_timetable_section(institute_db: Optional[str], teacher_uuid: Optional if rec: tt = rec["tt"] tt_uuid = tt["uuid_string"] - classes = [] - try: - cls_result = session.run( - "MATCH (tt2:TeacherTimetable {uuid_string: $id})" - "-[:TIMETABLE_HAS_CLASS]->(c:SubjectClass) " - "RETURN c ORDER BY c.name", - id=tt_uuid, - ) - classes = [ - { - "neo4j_node_id": r["c"]["uuid_string"], - "label": r["c"].get("name") or "Class", - "node_type": "SubjectClass", - "neo4j_db_name": institute_db, - "is_section": False, - "has_children": True, - } - for r in cls_result - ] - except Exception: - pass + # Load classes from Supabase (source of truth) + classes = _query_teacher_classes(user_id, institute_id, institute_db) return { **_section("timetable", "My Timetable", institute_db, "populated", has_children=True, children=classes if classes else None), @@ -283,11 +292,11 @@ def _build_timetable_section(institute_db: Optional[str], teacher_uuid: Optional return _section("timetable", "My Timetable", institute_db, "empty") -def _build_classes_section(institute_db: Optional[str], teacher_uuid: Optional[str]) -> Dict: - if not institute_db or not teacher_uuid: +def _build_classes_section(user_id: str, institute_id: Optional[str], institute_db: Optional[str]) -> Dict: + if not institute_db or not institute_id: return _section("classes", "My Classes", "", "no_school") - classes = _query_teacher_classes(institute_db, teacher_uuid) + classes = _query_teacher_classes(user_id, institute_id, institute_db) if classes: return _section("classes", "My Classes", institute_db, "populated", has_children=True, children=classes) @@ -413,28 +422,9 @@ def _get_children_for_node( # TeacherTimetable lazy-load (fallback if not pre-loaded, or for By-Term view) if node_type == "TeacherTimetable" and neo4j_db_name: if section_id in ("", "timetable"): - try: - with driver_tools.get_session(database=neo4j_db_name) as session: - result = session.run( - "MATCH (tt:TeacherTimetable {uuid_string: $id})" - "-[:TIMETABLE_HAS_CLASS]->(c:SubjectClass) " - "RETURN c ORDER BY c.name", - id=neo4j_node_id, - ) - return [ - { - "neo4j_node_id": r["c"]["uuid_string"], - "label": r["c"].get("name") or "Class", - "node_type": "SubjectClass", - "neo4j_db_name": neo4j_db_name, - "is_section": False, - "has_children": True, - } - for r in result - ] - except Exception as e: - logger.warning(f"TeacherTimetable class children failed: {e}") - return [] + # Classes are pre-loaded in _build_timetable_section via Supabase. + # Lazy expansion here is not needed in normal flow. + return [] if section_id == "timetable-term": try: with driver_tools.get_session(database=neo4j_db_name) as session: @@ -566,33 +556,52 @@ def _get_children_for_node( # AcademicWeek → days (or TaughtLessons in timetable-term context) if node_type == "AcademicWeek" and neo4j_db_name: if section_id == "timetable-term" and user_email: + # Supabase: get week date range from Neo4j, then query taught_lessons try: + week_start = None with driver_tools.get_session(database=neo4j_db_name) as session: - result = session.run( + rec = session.run( "MATCH (w:AcademicWeek {uuid_string: $id}) " - "-[:ACADEMIC_WEEK_HAS_ACADEMIC_DAY]->(d:AcademicDay) " - "-[:ACADEMIC_DAY_HAS_PERIOD]->(p:AcademicPeriod) " - "-[:ACADEMIC_PERIOD_HAS_TAUGHT_LESSON]->(tl:TaughtLesson) " - "WHERE tl.teacher_email = " - "RETURN tl, d.date AS date ORDER BY d.date, p.start_time", + "RETURN w.start_date AS start_date", id=neo4j_node_id, - email=user_email, - ) - return [ - { - "neo4j_node_id": r["tl"]["uuid_string"], - "label": (r["tl"].get("period_code") or "") - + " — " - + (r["tl"].get("class_name") or r["tl"].get("subject_class") or "Lesson"), - "node_type": "TaughtLesson", - "neo4j_db_name": neo4j_db_name, - "is_section": False, - "has_children": False, - } - for r in result - ] + ).single() + if rec: + week_start = rec["start_date"] + if week_start: + from datetime import datetime, timedelta + start_dt = datetime.strptime(str(week_start)[:10], "%Y-%m-%d") + end_dt = start_dt + timedelta(days=6) + sb = _sb() + prof = sb.supabase.table("profiles").select("id").eq("email", user_email).single().execute() + teacher_id = (prof.data or {}).get("id") + if teacher_id: + lessons = ( + sb.supabase.table("taught_lessons") + .select("id, date, period_code, class_name, subject") + .eq("teacher_profile_id", teacher_id) + .gte("date", start_dt.strftime("%Y-%m-%d")) + .lte("date", end_dt.strftime("%Y-%m-%d")) + .order("date") + .order("period_code") + .execute() + .data or [] + ) + return [ + { + "neo4j_node_id": tl["id"], + "label": "{} — {}".format( + tl.get("period_code") or "", + tl.get("class_name") or tl.get("subject") or "Lesson" + ), + "node_type": "TaughtLesson", + "neo4j_db_name": neo4j_db_name, + "is_section": False, + "has_children": False, + } + for tl in lessons + ] except Exception as e: - logger.warning(f"AcademicWeek timetable-term lessons failed: {e}") + logger.warning(f"AcademicWeek timetable-term Supabase lessons failed: {e}") return [] try: with driver_tools.get_session(database=neo4j_db_name) as session: @@ -691,12 +700,12 @@ async def get_teacher_graph_tree( "has_children": True, } - _, institute_db, teacher_node_uuid = _resolve_institute(user_id, user_email) + supabase_institute_id, institute_db, teacher_node_uuid = _resolve_institute(user_id, user_email) sections = [ _build_calendar_section(), - _build_timetable_section(institute_db, teacher_node_uuid), - _build_classes_section(institute_db, teacher_node_uuid), + _build_timetable_section(user_id, supabase_institute_id, institute_db, teacher_node_uuid), + _build_classes_section(user_id, supabase_institute_id, institute_db), _build_curriculum_section(institute_db), _build_journal_section(teacher_db), _build_planner_section(teacher_db),