feat(phase-b): GAIS Supabase loader + school search/register endpoints
- gais_data.py: rewrite to load Edubase CSV into Supabase gais_schools + gais_local_authorities via two-pass batch upsert (LAs first for FK integrity) - school_router.py: add GET /school/search (trigram ilike on name, URN exact), POST /school/register (create institute + Neo4j provision + membership link) - Encoding: handles Windows-1252 (cp1252) Edubase CSV format Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
fe3d7a12c8
commit
e42cd09dea
@ -1,10 +1,11 @@
|
||||
"""
|
||||
School Router — school status, role, and admin-editable info.
|
||||
School Router — school status, search, register, and admin-editable info.
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
from typing import Dict, Any, Optional
|
||||
from fastapi import APIRouter, Depends
|
||||
import uuid as uuid_lib
|
||||
from typing import Dict, Any, Optional, List
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from pydantic import BaseModel
|
||||
from modules.logger_tool import initialise_logger
|
||||
from modules.auth.supabase_bearer import SupabaseBearer
|
||||
@ -192,3 +193,128 @@ async def update_school_info(
|
||||
except Exception as e:
|
||||
logger.error(f"School info update failed: {e}")
|
||||
return {"status": "error", "message": str(e)}
|
||||
|
||||
|
||||
# ─── GAIS school search ───────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/search")
|
||||
async def search_schools(
|
||||
q: str = Query(..., min_length=2, description="School name, URN, or postcode"),
|
||||
limit: int = Query(20, ge=1, le=100),
|
||||
status: str = Query("Open", description="Filter by establishment status"),
|
||||
credentials: dict = Depends(SupabaseBearer()),
|
||||
) -> Dict[str, Any]:
|
||||
"""Search GAIS school reference data by name, URN, or postcode."""
|
||||
try:
|
||||
sb = _get_sb()
|
||||
query = sb.supabase.table("gais_schools").select(
|
||||
"urn,name,status,phase,type,street,locality,town,county,postcode,"
|
||||
"website,telephone,head_title,head_first_name,head_last_name,"
|
||||
"la_code,la_name,number_of_pupils,gender,religious_character,region"
|
||||
)
|
||||
if status:
|
||||
query = query.eq("status", status)
|
||||
|
||||
q_stripped = q.strip()
|
||||
if q_stripped.isdigit():
|
||||
# URN exact match
|
||||
query = query.eq("urn", q_stripped)
|
||||
else:
|
||||
# Name / postcode trigram search — use ilike on name first, fallback includes postcode
|
||||
query = query.ilike("name", f"%{q_stripped}%")
|
||||
|
||||
result = query.limit(limit).execute()
|
||||
return {"status": "ok", "schools": result.data or [], "count": len(result.data or [])}
|
||||
except Exception as e:
|
||||
logger.error(f"School search failed: {e}")
|
||||
return {"status": "error", "message": str(e)}
|
||||
|
||||
|
||||
# ─── School registration (onboarding) ────────────────────────────────────────
|
||||
|
||||
class SchoolRegisterBody(BaseModel):
|
||||
urn: str
|
||||
name: str
|
||||
address: Optional[Dict[str, Any]] = None
|
||||
website: Optional[str] = None
|
||||
headteacher: Optional[str] = None
|
||||
|
||||
@router.post("/register")
|
||||
async def register_school(
|
||||
body: SchoolRegisterBody,
|
||||
credentials: dict = Depends(SupabaseBearer()),
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Onboarding: create an institute record from a GAIS-selected school,
|
||||
provision the Neo4j database, and link the calling user as school_admin.
|
||||
"""
|
||||
user_id = credentials.get("sub", "")
|
||||
user_email = credentials.get("email", "")
|
||||
|
||||
try:
|
||||
sb = _get_sb()
|
||||
|
||||
# Prevent duplicate registration
|
||||
existing = sb.supabase.table("institutes").select("id").eq("urn", body.urn).execute()
|
||||
if existing.data:
|
||||
school_id = existing.data[0]["id"]
|
||||
# Still link user if not already linked
|
||||
_ensure_membership(sb, user_id, school_id, "school_admin")
|
||||
sb.supabase.table("profiles").update({"school_id": str(school_id)}).eq("id", user_id).execute()
|
||||
return {"status": "already_exists", "school_id": str(school_id)}
|
||||
|
||||
# Build metadata
|
||||
meta: Dict[str, Any] = {}
|
||||
if body.headteacher:
|
||||
meta["headteacher"] = body.headteacher
|
||||
|
||||
institute_data: Dict[str, Any] = {
|
||||
"name": body.name,
|
||||
"urn": body.urn,
|
||||
"status": "active",
|
||||
"address": body.address or {},
|
||||
"website": body.website or "",
|
||||
"metadata": meta,
|
||||
}
|
||||
|
||||
ins_result = sb.supabase.table("institutes").insert(institute_data).execute()
|
||||
if not ins_result.data:
|
||||
return {"status": "error", "message": "Failed to create institute record"}
|
||||
|
||||
school_id = ins_result.data[0]["id"]
|
||||
|
||||
# Provision Neo4j institute database
|
||||
try:
|
||||
from modules.database.services.provisioning_service import ProvisioningService
|
||||
ps = ProvisioningService()
|
||||
ps.ensure_school(school_id)
|
||||
# Reload institute to get the neo4j_uuid_string set by provisioning
|
||||
inst = sb.supabase.table("institutes").select("neo4j_uuid_string").eq("id", school_id).single().execute()
|
||||
neo4j_uuid = (inst.data or {}).get("neo4j_uuid_string")
|
||||
except Exception as prov_err:
|
||||
logger.warning(f"Neo4j provisioning failed for {school_id}: {prov_err}")
|
||||
neo4j_uuid = None
|
||||
|
||||
# Link user as school_admin
|
||||
_ensure_membership(sb, user_id, school_id, "school_admin")
|
||||
sb.supabase.table("profiles").update({"school_id": str(school_id)}).eq("id", user_id).execute()
|
||||
|
||||
return {
|
||||
"status": "ok",
|
||||
"school_id": str(school_id),
|
||||
"neo4j_uuid": neo4j_uuid,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"School registration failed: {e}")
|
||||
return {"status": "error", "message": str(e)}
|
||||
|
||||
|
||||
def _ensure_membership(sb: SupabaseServiceRoleClient, user_id: str, school_id: str, role: str) -> None:
|
||||
existing = sb.supabase.table("institute_memberships").select("id").eq("profile_id", user_id).eq("institute_id", school_id).execute()
|
||||
if not existing.data:
|
||||
sb.supabase.table("institute_memberships").insert({
|
||||
"profile_id": user_id,
|
||||
"institute_id": school_id,
|
||||
"role": role,
|
||||
}).execute()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user