feat(api): name each school's type group, and send search its nursery provision
The school page and the search rows now print a school's type in the search
filter's terms ("State school", "Independent school") instead of GIAS's 34
establishment types. The list, place and detail payloads carry type_group,
computed with the filter's own type_group_for, so the two never disagree; a
type in no group stays null and the page prints the register's name.
Search rows flag a nursery class, so nursery_provision joins SCHOOL_COLUMNS.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e344298440
commit
62a6bfaf0c
3 files changed
+102
-7
No files matched your search
+20
-7
@@ -634,6 +634,18 @@ def _with_whole_school_pupils(rows: pd.DataFrame, source: pd.DataFrame) -> pd.Da
|
||||
return rows.assign(total_pupils=whole)
|
||||
|
||||
|
||||
def _with_type_group(rows: pd.DataFrame) -> pd.DataFrame:
|
||||
"""Name each row's search-filter type group, or None for a type in no group.
|
||||
|
||||
The school page and the search rows print it ("State school") in place of
|
||||
the GIAS establishment type, and an independent school gets a Fee-paying
|
||||
flag from it.
|
||||
"""
|
||||
if "school_type" not in rows.columns:
|
||||
return rows
|
||||
return rows.assign(type_group=rows["school_type"].map(type_group_for))
|
||||
|
||||
|
||||
# Input validation helpers
|
||||
def _names_in_group(names: pd.Series, in_group) -> set:
|
||||
"""The distinct names in a column that a group predicate accepts.
|
||||
@@ -965,7 +977,7 @@ async def get_schools(
|
||||
total = len(schools_df)
|
||||
start_idx = (page - 1) * page_size
|
||||
end_idx = start_idx + page_size
|
||||
schools_df = schools_df.iloc[start_idx:end_idx]
|
||||
schools_df = _with_type_group(schools_df.iloc[start_idx:end_idx])
|
||||
|
||||
return {
|
||||
"schools": clean_for_json(schools_df),
|
||||
@@ -1029,6 +1041,7 @@ async def get_school_details(request: Request, urn: int):
|
||||
"school_name": latest.get("school_name", ""),
|
||||
"local_authority": latest.get("local_authority", ""),
|
||||
"school_type": latest.get("school_type", ""),
|
||||
"type_group": type_group_for(latest.get("school_type")),
|
||||
"address": latest.get("address", ""),
|
||||
"religious_denomination": latest.get("religious_denomination", ""),
|
||||
"age_range": latest.get("age_range", ""),
|
||||
@@ -1522,13 +1535,12 @@ async def get_place(request: Request, kind: str, slug: str,
|
||||
# warning. Ordered de-duplication keeps the column order and the warning
|
||||
# cannot come back.
|
||||
#
|
||||
# nursery_provision and parliamentary_constituency are not in
|
||||
# SCHOOL_COLUMNS and the place table shows both. The `in rows.columns`
|
||||
# guard is what keeps a mart the pipeline has not rebuilt working: those
|
||||
# two are the optional GIAS columns data_loader degrades to NULL.
|
||||
# parliamentary_constituency is not in SCHOOL_COLUMNS and the place table
|
||||
# shows it. The `in rows.columns` guard is what keeps a mart the pipeline
|
||||
# has not rebuilt working: it and nursery_provision are the optional GIAS
|
||||
# columns data_loader degrades to NULL.
|
||||
cols = [c for c in dict.fromkeys(
|
||||
SCHOOL_COLUMNS + ["latitude", "longitude", "phase",
|
||||
"nursery_provision",
|
||||
"parliamentary_constituency",
|
||||
"rwm_expected_pct", "attainment_8_score",
|
||||
"total_pupils"])
|
||||
@@ -1558,7 +1570,8 @@ async def get_place(request: Request, kind: str, slug: str,
|
||||
# variants that exist rather than 404s.
|
||||
"phases": [ph for ph in ("primary", "secondary")
|
||||
if place.publishes_phase(ph)]},
|
||||
"schools": clean_for_json(_with_whole_school_pupils(rows[cols], rows)),
|
||||
"schools": clean_for_json(
|
||||
_with_type_group(_with_whole_school_pupils(rows[cols], rows))),
|
||||
"averages": averages,
|
||||
}
|
||||
|
||||
|
||||
@@ -569,6 +569,7 @@ SCHOOL_COLUMNS = [
|
||||
"religious_denomination",
|
||||
"age_range",
|
||||
"has_sixth_form",
|
||||
"nursery_provision",
|
||||
"status",
|
||||
"gender",
|
||||
"admissions_policy",
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
"""Payloads name each school's type group.
|
||||
|
||||
The school page and the search rows say "State school" or "Independent
|
||||
school" in the search filter's own terms, not GIAS's 34 establishment types,
|
||||
and an independent school gets a Fee-paying flag. A type in no group keeps a
|
||||
null group, and the page prints the register's own name for it. Search rows
|
||||
also carry nursery_provision, for their "Nursery class" flag.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
def _schools_df() -> pd.DataFrame:
|
||||
base = {
|
||||
"local_authority": "Essex", "phase": "Primary", "year": 202425,
|
||||
"ofsted_grade": 2.0, "ofsted_date": None, "attainment_8_score": np.nan,
|
||||
"town": "Brentwood", "postcode": "CM13 1AA", "status": "Open",
|
||||
"address": "1 Test Street", "latitude": 51.6, "longitude": 0.3,
|
||||
"rwm_expected_pct": 60.0, "nursery_provision": "No Nursery Classes",
|
||||
}
|
||||
rows = [
|
||||
{**base, "urn": 100001, "school_name": "Alpha Academy",
|
||||
"school_type": "Academy converter", "nursery_provision": "Has Nursery Classes"},
|
||||
{**base, "urn": 100002, "school_name": "Beta Prep",
|
||||
"school_type": "Other independent school"},
|
||||
{**base, "urn": 100003, "school_name": "Gamma Unit",
|
||||
"school_type": "Secure units"},
|
||||
]
|
||||
# Enough schools in one town for it to have a place page.
|
||||
rows += [
|
||||
{**base, "urn": 100010 + i, "school_name": f"Delta Primary {i}",
|
||||
"school_type": "Community school"}
|
||||
for i in range(3)
|
||||
]
|
||||
return pd.DataFrame(rows)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def client(monkeypatch):
|
||||
from backend import app as app_module
|
||||
|
||||
monkeypatch.setattr(app_module, "load_latest_school_data", _schools_df)
|
||||
monkeypatch.setattr(app_module, "load_school_data", _schools_df)
|
||||
monkeypatch.setattr(app_module, "get_supplementary_data", lambda db, urn: {})
|
||||
monkeypatch.setattr(app_module, "_place_registry", None)
|
||||
return TestClient(app_module.app, raise_server_exceptions=False)
|
||||
|
||||
|
||||
def _by_urn(schools: list[dict]) -> dict[int, dict]:
|
||||
return {s["urn"]: s for s in schools}
|
||||
|
||||
|
||||
def test_the_list_names_each_type_group(client):
|
||||
resp = client.get("/api/schools?page_size=50")
|
||||
assert resp.status_code == 200, resp.text
|
||||
schools = _by_urn(resp.json()["schools"])
|
||||
assert schools[100001]["type_group"] == "state"
|
||||
assert schools[100002]["type_group"] == "independent"
|
||||
assert schools[100003]["type_group"] is None
|
||||
|
||||
|
||||
def test_the_list_carries_nursery_provision(client):
|
||||
schools = _by_urn(client.get("/api/schools?page_size=50").json()["schools"])
|
||||
assert schools[100001]["nursery_provision"] == "Has Nursery Classes"
|
||||
|
||||
|
||||
def test_the_school_page_names_its_type_group(client):
|
||||
resp = client.get("/api/schools/100002")
|
||||
assert resp.status_code == 200, resp.text
|
||||
assert resp.json()["school_info"]["type_group"] == "independent"
|
||||
|
||||
|
||||
def test_a_place_page_names_each_type_group(client):
|
||||
resp = client.get("/api/places/town/brentwood")
|
||||
assert resp.status_code == 200, resp.text
|
||||
schools = _by_urn(resp.json()["schools"])
|
||||
assert schools[100001]["type_group"] == "state"
|
||||
assert schools[100003]["type_group"] is None
|
||||
Reference in new issue
Block a user