fix(nav): fix AcademicWeek Cypher bug, add TeacherTimetable tree handlers, timetable-term view

- Fix uuid_string: $id Cypher bug in AcademicWeek handler (days were never loading)
- Pre-load SubjectClass children in _build_timetable_section (By Class view)
- Add TeacherTimetable handler: By Class (TIMETABLE_HAS_CLASS) + By Term (ACADEMIC_TIMETABLE_HAS_ACADEMIC_YEAR chain)
- Add timetable-term context propagation through AcademicTerm -> AcademicWeek -> TaughtLesson
- AcademicWeek in timetable-term context returns TaughtLessons filtered by teacher email
- Pass user_email from credentials to _get_children_for_node
- Propagate section_id on AcademicWeek nodes so week expansion stays in timetable context

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
kcar 2026-05-27 12:12:46 +01:00
parent b42b409bb2
commit bf3df05632

View File

@ -248,10 +248,32 @@ def _build_timetable_section(institute_db: Optional[str], teacher_uuid: Optional
).single() ).single()
if rec: if rec:
tt = rec["tt"] 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
return { return {
**_section("timetable", "My Timetable", institute_db, "populated", **_section("timetable", "My Timetable", institute_db, "populated",
has_children=True), has_children=True, children=classes if classes else None),
"neo4j_node_id": tt["uuid_string"], "neo4j_node_id": tt_uuid,
"node_type": "TeacherTimetable", "node_type": "TeacherTimetable",
"is_section": True, "is_section": True,
} }
@ -379,6 +401,7 @@ def _get_children_for_node(
neo4j_db_name: str, neo4j_db_name: str,
node_type: str, node_type: str,
section_id: str = "", section_id: str = "",
user_email: str = "",
) -> List[Dict]: ) -> List[Dict]:
# Calendar # Calendar
if node_type == "CalendarYear": if node_type == "CalendarYear":
@ -387,6 +410,57 @@ def _get_children_for_node(
if node_type == "CalendarMonth": if node_type == "CalendarMonth":
return _query_month_days(neo4j_node_id) return _query_month_days(neo4j_node_id)
# 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 []
if section_id == "timetable-term":
try:
with driver_tools.get_session(database=neo4j_db_name) as session:
result = session.run(
"MATCH (tt:TeacherTimetable {uuid_string: $id}) "
"-[:ACADEMIC_TIMETABLE_HAS_ACADEMIC_YEAR]->(ay:AcademicYear) "
"-[:ACADEMIC_YEAR_HAS_ACADEMIC_TERM]->(t:AcademicTerm) "
"RETURN t ORDER BY toInteger(t.term_number)",
id=neo4j_node_id,
)
return [
{
"neo4j_node_id": r["t"]["uuid_string"],
"label": r["t"].get("term_name") or "Term {}".format(r["t"].get("term_number", "")),
"node_type": "AcademicTerm",
"neo4j_db_name": neo4j_db_name,
"section_id": "timetable-term",
"is_section": False,
"has_children": True,
}
for r in result
]
except Exception as e:
logger.warning(f"TeacherTimetable term children failed: {e}")
return []
# Section containers that need lazy loading # Section containers that need lazy loading
if node_type == "Section": if node_type == "Section":
if section_id == "timetable" and neo4j_db_name: if section_id == "timetable" and neo4j_db_name:
@ -489,17 +563,46 @@ def _get_children_for_node(
logger.warning(f"AcademicYear children failed: {e}") logger.warning(f"AcademicYear children failed: {e}")
return [] return []
# AcademicWeek → days # AcademicWeek → days (or TaughtLessons in timetable-term context)
if node_type == "AcademicWeek" and neo4j_db_name: if node_type == "AcademicWeek" and neo4j_db_name:
if section_id == "timetable-term" and user_email:
try: try:
with driver_tools.get_session(database=neo4j_db_name) as session: with driver_tools.get_session(database=neo4j_db_name) as session:
result = session.run( result = session.run(
"MATCH (w:AcademicWeek {uuid_string: })" "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",
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
]
except Exception as e:
logger.warning(f"AcademicWeek timetable-term lessons failed: {e}")
return []
try:
with driver_tools.get_session(database=neo4j_db_name) as session:
result = session.run(
"MATCH (w:AcademicWeek {uuid_string: $id}) "
"-[:ACADEMIC_WEEK_HAS_ACADEMIC_DAY]->(d:AcademicDay) " "-[:ACADEMIC_WEEK_HAS_ACADEMIC_DAY]->(d:AcademicDay) "
"RETURN d ORDER BY d.date", "RETURN d ORDER BY d.date",
id=neo4j_node_id, id=neo4j_node_id,
) )
days = [ return [
{ {
"neo4j_node_id": r["d"]["uuid_string"], "neo4j_node_id": r["d"]["uuid_string"],
"label": r["d"].get("date", ""), "label": r["d"].get("date", ""),
@ -510,7 +613,6 @@ def _get_children_for_node(
} }
for r in result for r in result
] ]
return days
except Exception as e: except Exception as e:
logger.warning(f"AcademicWeek children failed: {e}") logger.warning(f"AcademicWeek children failed: {e}")
return [] return []
@ -528,9 +630,10 @@ def _get_children_for_node(
return [ return [
{ {
"neo4j_node_id": r["w"]["uuid_string"], "neo4j_node_id": r["w"]["uuid_string"],
"label": f"Week {r['w']['academic_week_number']}", "label": "Week {}".format(r["w"].get("academic_week_number", r["w"].get("week_number", "?"))),
"node_type": "AcademicWeek", "node_type": "AcademicWeek",
"neo4j_db_name": neo4j_db_name, "neo4j_db_name": neo4j_db_name,
"section_id": section_id if section_id == "timetable-term" else "",
"is_section": False, "is_section": False,
"has_children": True, "has_children": True,
} }
@ -620,7 +723,8 @@ async def get_node_children(
section_id: str = "", section_id: str = "",
credentials: dict = Depends(SupabaseBearer()), credentials: dict = Depends(SupabaseBearer()),
) -> Dict[str, Any]: ) -> Dict[str, Any]:
children = _get_children_for_node(neo4j_node_id, neo4j_db_name, node_type, section_id) user_email = credentials.get("email", "")
children = _get_children_for_node(neo4j_node_id, neo4j_db_name, node_type, section_id, user_email)
return {"status": "success", "children": children} return {"status": "success", "children": children}