feat(api): expose progress CIs, KS4 banding/gaps, admissions detail, report-card labels
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0146VHeLAWjDVE2B5uU67jCB
This commit is contained in:
+84
-48
@@ -21,6 +21,7 @@ from .models import (
|
|||||||
FactOfstedInspection, FactAdmissions,
|
FactOfstedInspection, FactAdmissions,
|
||||||
FactDeprivation, FactFinance, FactPupilCharacteristics,
|
FactDeprivation, FactFinance, FactPupilCharacteristics,
|
||||||
)
|
)
|
||||||
|
from .ofsted_codes import ofsted_page_url, report_card_labels
|
||||||
from .schemas import SCHOOL_TYPE_MAP
|
from .schemas import SCHOOL_TYPE_MAP
|
||||||
from .gias_codes import (
|
from .gias_codes import (
|
||||||
ADMISSIONS_POLICY,
|
ADMISSIONS_POLICY,
|
||||||
@@ -190,13 +191,20 @@ _MAIN_QUERY = text("""
|
|||||||
p.reading_high_pct,
|
p.reading_high_pct,
|
||||||
p.reading_avg_score,
|
p.reading_avg_score,
|
||||||
p.reading_progress,
|
p.reading_progress,
|
||||||
|
p.reading_progress_lower_ci,
|
||||||
|
p.reading_progress_upper_ci,
|
||||||
p.writing_expected_pct,
|
p.writing_expected_pct,
|
||||||
p.writing_high_pct,
|
p.writing_high_pct,
|
||||||
p.writing_progress,
|
p.writing_progress,
|
||||||
|
p.writing_progress_lower_ci,
|
||||||
|
p.writing_progress_upper_ci,
|
||||||
|
p.writing_working_towards_pct,
|
||||||
p.maths_expected_pct,
|
p.maths_expected_pct,
|
||||||
p.maths_high_pct,
|
p.maths_high_pct,
|
||||||
p.maths_avg_score,
|
p.maths_avg_score,
|
||||||
p.maths_progress,
|
p.maths_progress,
|
||||||
|
p.maths_progress_lower_ci,
|
||||||
|
p.maths_progress_upper_ci,
|
||||||
p.gps_expected_pct,
|
p.gps_expected_pct,
|
||||||
p.gps_high_pct,
|
p.gps_high_pct,
|
||||||
p.gps_avg_score,
|
p.gps_avg_score,
|
||||||
@@ -225,6 +233,9 @@ _MAIN_QUERY = text("""
|
|||||||
p.progress_8_maths,
|
p.progress_8_maths,
|
||||||
p.progress_8_ebacc,
|
p.progress_8_ebacc,
|
||||||
p.progress_8_open,
|
p.progress_8_open,
|
||||||
|
p.progress_8_banding,
|
||||||
|
p.attainment_8_disadvantage_gap,
|
||||||
|
p.progress_8_disadvantage_gap,
|
||||||
p.english_maths_strong_pass_pct,
|
p.english_maths_strong_pass_pct,
|
||||||
p.english_maths_standard_pass_pct,
|
p.english_maths_standard_pass_pct,
|
||||||
p.ebacc_entry_pct,
|
p.ebacc_entry_pct,
|
||||||
@@ -514,6 +525,77 @@ def get_data_info(db: Session = None) -> dict:
|
|||||||
# SUPPLEMENTARY DATA — per-school detail page
|
# SUPPLEMENTARY DATA — per-school detail page
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
|
def _ofsted_block(o, urn: int) -> dict:
|
||||||
|
"""Serialize the latest Ofsted inspection row for API responses.
|
||||||
|
|
||||||
|
`grade_source` records where the effective overall grade came from:
|
||||||
|
a graded (Section 5) inspection, or carried forward from an ungraded
|
||||||
|
(Section 8) outcome — materially different claims a UI must be able
|
||||||
|
to distinguish. `report_card` holds coded+labelled renewed-framework
|
||||||
|
(Nov 2025) area judgements; safeguarding is a separate boolean and
|
||||||
|
never appears among the graded areas.
|
||||||
|
"""
|
||||||
|
if o.overall_effectiveness is not None:
|
||||||
|
grade_source = "graded"
|
||||||
|
overall = o.overall_effectiveness
|
||||||
|
elif o.ungraded_grade is not None:
|
||||||
|
# Fall back to the grade parsed from an ungraded (Section 8) outcome
|
||||||
|
# (e.g. "School remains Good") so the detail page matches the list badge.
|
||||||
|
grade_source = "ungraded_carried_forward"
|
||||||
|
overall = o.ungraded_grade
|
||||||
|
else:
|
||||||
|
grade_source = None
|
||||||
|
overall = None
|
||||||
|
|
||||||
|
block = {
|
||||||
|
"framework": o.framework,
|
||||||
|
"inspection_date": o.inspection_date.isoformat() if o.inspection_date else None,
|
||||||
|
"inspection_type": o.inspection_type,
|
||||||
|
"overall_effectiveness": overall,
|
||||||
|
"grade_source": grade_source,
|
||||||
|
"quality_of_education": o.quality_of_education,
|
||||||
|
"behaviour_attitudes": o.behaviour_attitudes,
|
||||||
|
"personal_development": o.personal_development,
|
||||||
|
"leadership_management": o.leadership_management,
|
||||||
|
"early_years_provision": o.early_years_provision,
|
||||||
|
"sixth_form_provision": o.sixth_form_provision,
|
||||||
|
"previous_overall": None, # Not available in new schema
|
||||||
|
"rc_safeguarding_met": o.rc_safeguarding_met,
|
||||||
|
"rc_inclusion": o.rc_inclusion,
|
||||||
|
"rc_curriculum_teaching": o.rc_curriculum_teaching,
|
||||||
|
"rc_achievement": o.rc_achievement,
|
||||||
|
"rc_attendance_behaviour": o.rc_attendance_behaviour,
|
||||||
|
"rc_personal_development": o.rc_personal_development,
|
||||||
|
"rc_leadership_governance": o.rc_leadership_governance,
|
||||||
|
"rc_early_years": o.rc_early_years,
|
||||||
|
"rc_sixth_form": o.rc_sixth_form,
|
||||||
|
"report_url": o.report_url,
|
||||||
|
"ofsted_page_url": ofsted_page_url(urn),
|
||||||
|
}
|
||||||
|
block["report_card"] = report_card_labels(block)
|
||||||
|
return block
|
||||||
|
|
||||||
|
|
||||||
|
def _admissions_row_dict(a) -> dict:
|
||||||
|
"""Serialize one fact_admissions row for API responses."""
|
||||||
|
return {
|
||||||
|
"year": a.year,
|
||||||
|
"school_phase": a.school_phase,
|
||||||
|
"places_offered": a.places_offered,
|
||||||
|
"total_applications": a.total_applications,
|
||||||
|
"first_preference_applications": a.first_preference_applications,
|
||||||
|
"first_preference_offers": a.first_preference_offers,
|
||||||
|
"first_preference_offer_pct": a.first_preference_offer_pct,
|
||||||
|
"oversubscription_ratio": a.oversubscription_ratio,
|
||||||
|
"oversubscribed": a.oversubscribed,
|
||||||
|
"total_offers": a.total_offers,
|
||||||
|
"second_preference_offers": a.second_preference_offers,
|
||||||
|
"third_preference_offers": a.third_preference_offers,
|
||||||
|
"cross_la_applications": a.cross_la_applications,
|
||||||
|
"cross_la_offers": a.cross_la_offers,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_supplementary_data(db: Session, urn: int) -> dict:
|
def get_supplementary_data(db: Session, urn: int) -> dict:
|
||||||
"""Fetch all supplementary data for a single school URN."""
|
"""Fetch all supplementary data for a single school URN."""
|
||||||
result = {}
|
result = {}
|
||||||
@@ -532,40 +614,7 @@ def get_supplementary_data(db: Session, urn: int) -> dict:
|
|||||||
|
|
||||||
# Latest Ofsted inspection
|
# Latest Ofsted inspection
|
||||||
o = safe_query(FactOfstedInspection, "urn", "inspection_date")
|
o = safe_query(FactOfstedInspection, "urn", "inspection_date")
|
||||||
result["ofsted"] = (
|
result["ofsted"] = _ofsted_block(o, urn) if o else None
|
||||||
{
|
|
||||||
"framework": o.framework,
|
|
||||||
"inspection_date": o.inspection_date.isoformat() if o.inspection_date else None,
|
|
||||||
"inspection_type": o.inspection_type,
|
|
||||||
# Fall back to the grade parsed from an ungraded (Section 8) outcome
|
|
||||||
# (e.g. "School remains Good") when there's no graded grade, so the
|
|
||||||
# detail page matches the list badge.
|
|
||||||
"overall_effectiveness": (
|
|
||||||
o.overall_effectiveness
|
|
||||||
if o.overall_effectiveness is not None
|
|
||||||
else o.ungraded_grade
|
|
||||||
),
|
|
||||||
"quality_of_education": o.quality_of_education,
|
|
||||||
"behaviour_attitudes": o.behaviour_attitudes,
|
|
||||||
"personal_development": o.personal_development,
|
|
||||||
"leadership_management": o.leadership_management,
|
|
||||||
"early_years_provision": o.early_years_provision,
|
|
||||||
"sixth_form_provision": o.sixth_form_provision,
|
|
||||||
"previous_overall": None, # Not available in new schema
|
|
||||||
"rc_safeguarding_met": o.rc_safeguarding_met,
|
|
||||||
"rc_inclusion": o.rc_inclusion,
|
|
||||||
"rc_curriculum_teaching": o.rc_curriculum_teaching,
|
|
||||||
"rc_achievement": o.rc_achievement,
|
|
||||||
"rc_attendance_behaviour": o.rc_attendance_behaviour,
|
|
||||||
"rc_personal_development": o.rc_personal_development,
|
|
||||||
"rc_leadership_governance": o.rc_leadership_governance,
|
|
||||||
"rc_early_years": o.rc_early_years,
|
|
||||||
"rc_sixth_form": o.rc_sixth_form,
|
|
||||||
"report_url": o.report_url,
|
|
||||||
}
|
|
||||||
if o
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
|
|
||||||
# Census (latest year of fact_pupil_characteristics)
|
# Census (latest year of fact_pupil_characteristics)
|
||||||
pc = safe_query(FactPupilCharacteristics, "urn", "year")
|
pc = safe_query(FactPupilCharacteristics, "urn", "year")
|
||||||
@@ -583,19 +632,6 @@ def get_supplementary_data(db: Session, urn: int) -> dict:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Admissions — all years, oldest first (for the multi-year trend view).
|
# Admissions — all years, oldest first (for the multi-year trend view).
|
||||||
def _admissions_row(a):
|
|
||||||
return {
|
|
||||||
"year": a.year,
|
|
||||||
"school_phase": a.school_phase,
|
|
||||||
"places_offered": a.places_offered,
|
|
||||||
"total_applications": a.total_applications,
|
|
||||||
"first_preference_applications": a.first_preference_applications,
|
|
||||||
"first_preference_offers": a.first_preference_offers,
|
|
||||||
"first_preference_offer_pct": a.first_preference_offer_pct,
|
|
||||||
"oversubscription_ratio": a.oversubscription_ratio,
|
|
||||||
"oversubscribed": a.oversubscribed,
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
admissions_rows = (
|
admissions_rows = (
|
||||||
db.query(FactAdmissions)
|
db.query(FactAdmissions)
|
||||||
@@ -609,7 +645,7 @@ def get_supplementary_data(db: Session, urn: int) -> dict:
|
|||||||
db.rollback()
|
db.rollback()
|
||||||
admissions_rows = []
|
admissions_rows = []
|
||||||
|
|
||||||
history = [_admissions_row(a) for a in admissions_rows]
|
history = [_admissions_row_dict(a) for a in admissions_rows]
|
||||||
result["admissions_history"] = history
|
result["admissions_history"] = history
|
||||||
# Keep the single latest-year object for backwards-compatible consumers
|
# Keep the single latest-year object for backwards-compatible consumers
|
||||||
# (hero chips, etc.).
|
# (hero chips, etc.).
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
"""Supplementary-block enrichment for the compare redesign: report-card
|
||||||
|
labels, provider-page URL, graded-vs-carried-forward provenance, and the
|
||||||
|
admissions preference/cross-LA detail promoted in the data-foundation PR."""
|
||||||
|
|
||||||
|
import types
|
||||||
|
|
||||||
|
from backend.data_loader import _admissions_row_dict, _ofsted_block
|
||||||
|
|
||||||
|
|
||||||
|
def _row(**kw):
|
||||||
|
base = dict(
|
||||||
|
framework="RC", inspection_date=None, inspection_type=None,
|
||||||
|
overall_effectiveness=None, quality_of_education=None,
|
||||||
|
behaviour_attitudes=None, personal_development=None,
|
||||||
|
leadership_management=None, early_years_provision=None,
|
||||||
|
sixth_form_provision=None, ungraded_outcome=None, ungraded_grade=None,
|
||||||
|
rc_safeguarding_met=None, rc_inclusion=None, rc_curriculum_teaching=None,
|
||||||
|
rc_achievement=None, rc_attendance_behaviour=None,
|
||||||
|
rc_personal_development=None, rc_leadership_governance=None,
|
||||||
|
rc_early_years=None, rc_sixth_form=None, report_url=None,
|
||||||
|
)
|
||||||
|
base.update(kw)
|
||||||
|
return types.SimpleNamespace(**base)
|
||||||
|
|
||||||
|
|
||||||
|
def test_report_card_block_and_provider_url():
|
||||||
|
o = _row(rc_achievement=2, rc_inclusion=3, rc_safeguarding_met=True)
|
||||||
|
block = _ofsted_block(o, urn=100140)
|
||||||
|
assert block["report_card"]["rc_achievement"]["label"] == "Strong standard"
|
||||||
|
assert "rc_safeguarding_met" not in block["report_card"]
|
||||||
|
assert block["rc_safeguarding_met"] is True
|
||||||
|
assert block["ofsted_page_url"] == "https://reports.ofsted.gov.uk/provider/21/100140"
|
||||||
|
|
||||||
|
|
||||||
|
def test_grade_source_graded_vs_carried_forward():
|
||||||
|
assert _ofsted_block(_row(overall_effectiveness=1), urn=1)["grade_source"] == "graded"
|
||||||
|
carried = _ofsted_block(_row(ungraded_grade=2), urn=1)
|
||||||
|
assert carried["grade_source"] == "ungraded_carried_forward"
|
||||||
|
assert carried["overall_effectiveness"] == 2
|
||||||
|
assert _ofsted_block(_row(), urn=1)["grade_source"] is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_ofsted_block_keeps_existing_keys():
|
||||||
|
block = _ofsted_block(_row(overall_effectiveness=2, quality_of_education=2), urn=1)
|
||||||
|
for key in ("framework", "inspection_date", "overall_effectiveness",
|
||||||
|
"quality_of_education", "rc_inclusion", "report_url"):
|
||||||
|
assert key in block
|
||||||
|
|
||||||
|
|
||||||
|
def test_admissions_row_new_fields():
|
||||||
|
a = types.SimpleNamespace(
|
||||||
|
year=202627, school_phase="Primary", places_offered=80,
|
||||||
|
total_applications=185, first_preference_applications=74,
|
||||||
|
first_preference_offers=74, first_preference_offer_pct=100.0,
|
||||||
|
oversubscription_ratio=0.925, oversubscribed=False,
|
||||||
|
total_offers=80, second_preference_offers=4, third_preference_offers=2,
|
||||||
|
cross_la_applications=12, cross_la_offers=3,
|
||||||
|
)
|
||||||
|
d = _admissions_row_dict(a)
|
||||||
|
for k in ("total_offers", "second_preference_offers", "third_preference_offers",
|
||||||
|
"cross_la_applications", "cross_la_offers"):
|
||||||
|
assert d[k] == getattr(a, k)
|
||||||
|
# Existing keys unchanged
|
||||||
|
assert d["first_preference_offer_pct"] == 100.0
|
||||||
|
assert d["oversubscribed"] is False
|
||||||
Reference in New Issue
Block a user