feat(ofsted): add Report Card system support alongside legacy OEIF grades
All checks were successful
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 47s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 1m11s
Build and Push Docker Images / Build Integrator (push) Successful in 58s
Build and Push Docker Images / Build Kestra Init (push) Successful in 32s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s
All checks were successful
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 47s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 1m11s
Build and Push Docker Images / Build Integrator (push) Successful in 58s
Build and Push Docker Images / Build Kestra Init (push) Successful in 32s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s
Ofsted replaced single overall grades with Report Cards from Nov 2025. Both systems are retained during the transition period. - DB: new framework + 9 RC columns on ofsted_inspections (schema v4) - Integrator: auto-detect OEIF vs Report Card from CSV column headers; parse 5-level RC grades and safeguarding met/not-met - API: expose all new fields in the ofsted response dict - Frontend: branch on framework='ReportCard' to show safeguarding badge + 8-category grid; fall back to legacy OEIF layout otherwise; always show inspection date in both layouts - CSS: rcGrade1–5 and safeguardingMet/NotMet classes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -81,6 +81,65 @@ GRADE_MAP = {
|
||||
"Inadequate": 4, "4": 4, 4: 4,
|
||||
}
|
||||
|
||||
# Report Card grade text → integer (1=Exceptional … 5=Urgent improvement)
|
||||
RC_GRADE_MAP = {
|
||||
"exceptional": 1,
|
||||
"strong standard": 2,
|
||||
"strong": 2,
|
||||
"expected standard": 3,
|
||||
"expected": 3,
|
||||
"needs attention": 4,
|
||||
"urgent improvement": 5,
|
||||
}
|
||||
|
||||
# Column name priority for Report Card fields (best-guess names; Ofsted may vary)
|
||||
RC_COLUMN_PRIORITY = {
|
||||
"rc_safeguarding": [
|
||||
"Safeguarding",
|
||||
"safeguarding",
|
||||
"Safeguarding standards",
|
||||
],
|
||||
"rc_inclusion": [
|
||||
"Inclusion",
|
||||
"inclusion",
|
||||
],
|
||||
"rc_curriculum_teaching": [
|
||||
"Curriculum and teaching",
|
||||
"curriculum_and_teaching",
|
||||
"Curriculum & teaching",
|
||||
],
|
||||
"rc_achievement": [
|
||||
"Achievement",
|
||||
"achievement",
|
||||
],
|
||||
"rc_attendance_behaviour": [
|
||||
"Attendance and behaviour",
|
||||
"attendance_and_behaviour",
|
||||
"Attendance & behaviour",
|
||||
],
|
||||
"rc_personal_development": [
|
||||
"Personal development and well-being",
|
||||
"Personal development and wellbeing",
|
||||
"personal_development_and_wellbeing",
|
||||
"Personal development & well-being",
|
||||
],
|
||||
"rc_leadership_governance": [
|
||||
"Leadership and governance",
|
||||
"leadership_and_governance",
|
||||
"Leadership & governance",
|
||||
],
|
||||
"rc_early_years": [
|
||||
"Early years",
|
||||
"early_years",
|
||||
"Early years provision",
|
||||
],
|
||||
"rc_sixth_form": [
|
||||
"Sixth form",
|
||||
"sixth_form",
|
||||
"Sixth form in schools",
|
||||
],
|
||||
}
|
||||
|
||||
DEST_DIR = SUPPLEMENTARY_DIR / "ofsted"
|
||||
|
||||
|
||||
@@ -137,6 +196,26 @@ def _parse_grade(val) -> int | None:
|
||||
return GRADE_MAP.get(key)
|
||||
|
||||
|
||||
def _parse_rc_grade(val) -> int | None:
|
||||
"""Parse a Report Card grade text to integer 1–5."""
|
||||
if pd.isna(val):
|
||||
return None
|
||||
key = str(val).strip().lower()
|
||||
return RC_GRADE_MAP.get(key)
|
||||
|
||||
|
||||
def _parse_safeguarding(val) -> bool | None:
|
||||
"""Parse safeguarding 'Met'/'Not met' to boolean."""
|
||||
if pd.isna(val):
|
||||
return None
|
||||
s = str(val).strip().lower()
|
||||
if s == "met":
|
||||
return True
|
||||
if s in ("not met", "not_met"):
|
||||
return False
|
||||
return None
|
||||
|
||||
|
||||
def _parse_date(val) -> date | None:
|
||||
if pd.isna(val):
|
||||
return None
|
||||
@@ -148,6 +227,19 @@ def _parse_date(val) -> date | None:
|
||||
return None
|
||||
|
||||
|
||||
def _detect_framework(df: pd.DataFrame) -> str:
|
||||
"""Return 'ReportCard' if new-format columns are present, else 'OEIF'."""
|
||||
rc_indicators = [
|
||||
"inclusion", "curriculum and teaching", "achievement",
|
||||
"attendance and behaviour", "safeguarding standards", "safeguarding",
|
||||
]
|
||||
cols_lower = {c.lower() for c in df.columns}
|
||||
for indicator in rc_indicators:
|
||||
if any(indicator in c for c in cols_lower):
|
||||
return "ReportCard"
|
||||
return "OEIF"
|
||||
|
||||
|
||||
def load(path: Path | None = None, data_dir: Path | None = None) -> dict:
|
||||
if path is None:
|
||||
dest = (data_dir / "supplementary" / "ofsted") if data_dir else DEST_DIR
|
||||
@@ -186,7 +278,11 @@ def load(path: Path | None = None, data_dir: Path | None = None) -> dict:
|
||||
hdr = _find_header_row(path)
|
||||
df = pd.read_csv(path, encoding="latin-1", low_memory=False, header=hdr)
|
||||
|
||||
# Normalise column names: for each target field pick the first source column present
|
||||
# Detect which framework the CSV represents BEFORE any renaming
|
||||
framework = _detect_framework(df)
|
||||
print(f" Ofsted: detected framework '{framework}'")
|
||||
|
||||
# Normalise OEIF column names: for each target field pick the first source column present
|
||||
available = set(df.columns)
|
||||
for target, sources in COLUMN_PRIORITY.items():
|
||||
for src in sources:
|
||||
@@ -194,6 +290,14 @@ def load(path: Path | None = None, data_dir: Path | None = None) -> dict:
|
||||
df.rename(columns={src: target}, inplace=True)
|
||||
break
|
||||
|
||||
# Normalise Report Card column names (if present)
|
||||
available = set(df.columns)
|
||||
for target, sources in RC_COLUMN_PRIORITY.items():
|
||||
for src in sources:
|
||||
if src in available:
|
||||
df.rename(columns={src: target}, inplace=True)
|
||||
break
|
||||
|
||||
if "urn" not in df.columns:
|
||||
raise ValueError(f"URN column not found. Available: {list(df.columns)[:20]}")
|
||||
|
||||
@@ -210,14 +314,18 @@ def load(path: Path | None = None, data_dir: Path | None = None) -> dict:
|
||||
df["_date_parsed"] = df["inspection_date"].apply(_parse_date)
|
||||
df = df.sort_values("_date_parsed", ascending=False).groupby("urn").first().reset_index()
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
for _, row in df.iterrows():
|
||||
urn = int(row["urn"])
|
||||
|
||||
record = {
|
||||
"urn": urn,
|
||||
"framework": framework,
|
||||
"inspection_date": _parse_date(row.get("inspection_date")),
|
||||
"publication_date": _parse_date(row.get("publication_date")),
|
||||
"inspection_type": str(row.get("inspection_type", "")).strip() or None,
|
||||
# OEIF fields
|
||||
"overall_effectiveness": _parse_grade(row.get("overall_effectiveness")),
|
||||
"quality_of_education": _parse_grade(row.get("quality_of_education")),
|
||||
"behaviour_attitudes": _parse_grade(row.get("behaviour_attitudes")),
|
||||
@@ -225,32 +333,57 @@ def load(path: Path | None = None, data_dir: Path | None = None) -> dict:
|
||||
"leadership_management": _parse_grade(row.get("leadership_management")),
|
||||
"early_years_provision": _parse_grade(row.get("early_years_provision")),
|
||||
"previous_overall": None,
|
||||
# Report Card fields
|
||||
"rc_safeguarding_met": _parse_safeguarding(row.get("rc_safeguarding")),
|
||||
"rc_inclusion": _parse_rc_grade(row.get("rc_inclusion")),
|
||||
"rc_curriculum_teaching": _parse_rc_grade(row.get("rc_curriculum_teaching")),
|
||||
"rc_achievement": _parse_rc_grade(row.get("rc_achievement")),
|
||||
"rc_attendance_behaviour": _parse_rc_grade(row.get("rc_attendance_behaviour")),
|
||||
"rc_personal_development": _parse_rc_grade(row.get("rc_personal_development")),
|
||||
"rc_leadership_governance": _parse_rc_grade(row.get("rc_leadership_governance")),
|
||||
"rc_early_years": _parse_rc_grade(row.get("rc_early_years")),
|
||||
"rc_sixth_form": _parse_rc_grade(row.get("rc_sixth_form")),
|
||||
}
|
||||
|
||||
from sqlalchemy import text
|
||||
session.execute(
|
||||
text("""
|
||||
INSERT INTO ofsted_inspections
|
||||
(urn, inspection_date, publication_date, inspection_type,
|
||||
(urn, framework, inspection_date, publication_date, inspection_type,
|
||||
overall_effectiveness, quality_of_education, behaviour_attitudes,
|
||||
personal_development, leadership_management, early_years_provision,
|
||||
previous_overall)
|
||||
previous_overall,
|
||||
rc_safeguarding_met, rc_inclusion, rc_curriculum_teaching,
|
||||
rc_achievement, rc_attendance_behaviour, rc_personal_development,
|
||||
rc_leadership_governance, rc_early_years, rc_sixth_form)
|
||||
VALUES
|
||||
(:urn, :inspection_date, :publication_date, :inspection_type,
|
||||
(:urn, :framework, :inspection_date, :publication_date, :inspection_type,
|
||||
:overall_effectiveness, :quality_of_education, :behaviour_attitudes,
|
||||
:personal_development, :leadership_management, :early_years_provision,
|
||||
:previous_overall)
|
||||
:previous_overall,
|
||||
:rc_safeguarding_met, :rc_inclusion, :rc_curriculum_teaching,
|
||||
:rc_achievement, :rc_attendance_behaviour, :rc_personal_development,
|
||||
:rc_leadership_governance, :rc_early_years, :rc_sixth_form)
|
||||
ON CONFLICT (urn) DO UPDATE SET
|
||||
previous_overall = ofsted_inspections.overall_effectiveness,
|
||||
inspection_date = EXCLUDED.inspection_date,
|
||||
publication_date = EXCLUDED.publication_date,
|
||||
inspection_type = EXCLUDED.inspection_type,
|
||||
previous_overall = ofsted_inspections.overall_effectiveness,
|
||||
framework = EXCLUDED.framework,
|
||||
inspection_date = EXCLUDED.inspection_date,
|
||||
publication_date = EXCLUDED.publication_date,
|
||||
inspection_type = EXCLUDED.inspection_type,
|
||||
overall_effectiveness = EXCLUDED.overall_effectiveness,
|
||||
quality_of_education = EXCLUDED.quality_of_education,
|
||||
behaviour_attitudes = EXCLUDED.behaviour_attitudes,
|
||||
personal_development = EXCLUDED.personal_development,
|
||||
quality_of_education = EXCLUDED.quality_of_education,
|
||||
behaviour_attitudes = EXCLUDED.behaviour_attitudes,
|
||||
personal_development = EXCLUDED.personal_development,
|
||||
leadership_management = EXCLUDED.leadership_management,
|
||||
early_years_provision = EXCLUDED.early_years_provision
|
||||
early_years_provision = EXCLUDED.early_years_provision,
|
||||
rc_safeguarding_met = EXCLUDED.rc_safeguarding_met,
|
||||
rc_inclusion = EXCLUDED.rc_inclusion,
|
||||
rc_curriculum_teaching = EXCLUDED.rc_curriculum_teaching,
|
||||
rc_achievement = EXCLUDED.rc_achievement,
|
||||
rc_attendance_behaviour = EXCLUDED.rc_attendance_behaviour,
|
||||
rc_personal_development = EXCLUDED.rc_personal_development,
|
||||
rc_leadership_governance = EXCLUDED.rc_leadership_governance,
|
||||
rc_early_years = EXCLUDED.rc_early_years,
|
||||
rc_sixth_form = EXCLUDED.rc_sixth_form
|
||||
"""),
|
||||
record,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user