""" School Router — school status, role, and admin-editable info. """ import os import json from typing import Dict, Any, Optional from fastapi import APIRouter, Depends from pydantic import BaseModel from modules.logger_tool import initialise_logger from modules.auth.supabase_bearer import SupabaseBearer from modules.database.supabase.utils.client import SupabaseServiceRoleClient import modules.database.tools.neo4j_driver_tools as driver_tools logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True) router = APIRouter() # ─── Helpers ────────────────────────────────────────────────────────────────── def _get_sb(): return SupabaseServiceRoleClient() def _institute_db(neo4j_uuid: str) -> str: return f"cc.institutes.{neo4j_uuid}" def _find_institute_db_by_email(user_email: str) -> Optional[str]: """Fallback: scan all institute DBs for the teacher's email.""" try: with driver_tools.get_session(database="system") as s: dbs = [r["name"] for r in s.run( "SHOW DATABASES YIELD name " "WHERE name STARTS WITH 'cc.institutes.' " "AND NOT name ENDS WITH '.curriculum' RETURN name" )] for db in dbs: try: with driver_tools.get_session(database=db) as s: rec = s.run( "MATCH (t:Teacher) WHERE t.worker_email = $e " "RETURN t.uuid_string AS uuid LIMIT 1", e=user_email ).single() if rec: return db except Exception: continue except Exception as e: logger.warning(f"Institute DB scan failed: {e}") return None # ─── Status endpoint ───────────────────────────────────────────────────────── @router.get("/status") async def get_school_status(credentials: dict = Depends(SupabaseBearer())) -> Dict[str, Any]: """Return the current user's school role, school info, and calendar/timetable setup status.""" user_id = credentials.get("sub", "") user_email = credentials.get("email", "") try: sb = _get_sb() p = sb.supabase.table("profiles").select("school_id").eq("id", user_id).single().execute() school_id = (p.data or {}).get("school_id") # Fallback: if profiles.school_id not set, check institute_memberships directly if not school_id: m_fb = sb.supabase.table("institute_memberships").select("institute_id,role").eq("profile_id", user_id).single().execute() if m_fb.data and m_fb.data.get("institute_id"): school_id = m_fb.data["institute_id"] user_role = m_fb.data.get("role") or "teacher" # Self-heal: write school_id back to profile try: sb.supabase.table("profiles").update({"school_id": str(school_id)}).eq("id", user_id).execute() except Exception: pass else: return {"status": "no_school"} else: m = sb.supabase.table("institute_memberships").select("role").eq("profile_id", user_id).eq("institute_id", school_id).single().execute() user_role = ((m.data or {}).get("role") or "teacher") i = sb.supabase.table("institutes").select("id,name,urn,website,address,metadata,neo4j_uuid_string").eq("id", school_id).single().execute() inst = i.data or {} neo4j_uuid = inst.get("neo4j_uuid_string") meta = inst.get("metadata") or {} except Exception as e: logger.warning(f"Supabase school status query failed: {e}") return {"status": "error", "message": str(e)} if neo4j_uuid: inst_db = _institute_db(neo4j_uuid) else: inst_db = _find_institute_db_by_email(user_email) if not inst_db: return {"status": "no_school"} school_has_calendar = False teacher_has_timetable = False timetable_id = None periods_template = None try: with driver_tools.get_session(database=inst_db) as s: ay = s.run("MATCH (ay:AcademicYear) RETURN ay LIMIT 1").single() school_has_calendar = ay is not None st = s.run( "MATCH (st:SchoolTimetable) WHERE st.periods_template IS NOT NULL " "RETURN st.periods_template AS p LIMIT 1" ).single() if st: periods_template = json.loads(st["p"]) t = s.run( "MATCH (t:Teacher) WHERE t.worker_email = $e " "RETURN t.uuid_string AS uuid LIMIT 1", e=user_email ).single() if t: teacher_uuid = t["uuid"] tt = s.run( "MATCH (t:Teacher {uuid_string: $u})-[:HAS_TIMETABLE]->(tt:TeacherTimetable) " "RETURN tt.uuid_string AS id LIMIT 1", u=teacher_uuid ).single() if tt: teacher_has_timetable = True timetable_id = tt["id"] except Exception as e: logger.warning(f"Neo4j school status check failed for {inst_db}: {e}") return { "status": "ok", "user_role": user_role, "school_id": str(school_id), "institute_db": inst_db, "school_has_calendar": school_has_calendar, "teacher_has_timetable": teacher_has_timetable, "timetable_id": timetable_id, "periods_template": periods_template, "school_info": { "name": inst.get("name", ""), "urn": inst.get("urn", ""), "website": inst.get("website", ""), "address": inst.get("address") or {}, "headteacher": meta.get("headteacher", ""), "term_dates_url": meta.get("term_dates_url", ""), "staff_list_url": meta.get("staff_list_url", ""), }, } # ─── School info update ─────────────────────────────────────────────────────── class SchoolInfoUpdate(BaseModel): headteacher: Optional[str] = None term_dates_url: Optional[str] = None staff_list_url: Optional[str] = None @router.patch("/info") async def update_school_info( body: SchoolInfoUpdate, credentials: dict = Depends(SupabaseBearer()) ) -> Dict[str, Any]: """Admin: update school metadata fields stored in institutes.metadata jsonb.""" user_id = credentials.get("sub", "") try: sb = _get_sb() p = sb.supabase.table("profiles").select("school_id").eq("id", user_id).single().execute() school_id = (p.data or {}).get("school_id") if not school_id: return {"status": "error", "message": "No school linked"} m = sb.supabase.table("institute_memberships").select("role").eq("profile_id", user_id).eq("institute_id", school_id).single().execute() role = ((m.data or {}).get("role") or "teacher") if role != "school_admin": return {"status": "error", "message": "school_admin role required"} i = sb.supabase.table("institutes").select("metadata").eq("id", school_id).single().execute() meta = dict((i.data or {}).get("metadata") or {}) if body.headteacher is not None: meta["headteacher"] = body.headteacher if body.term_dates_url is not None: meta["term_dates_url"] = body.term_dates_url if body.staff_list_url is not None: meta["staff_list_url"] = body.staff_list_url sb.supabase.table("institutes").update({"metadata": meta}).eq("id", school_id).execute() return {"status": "ok"} except Exception as e: logger.error(f"School info update failed: {e}") return {"status": "error", "message": str(e)}