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:
TudorandClaude Opus 5.5 committed 2026-10-02 22:59:45 +01:00
1 parent e344298440
commit 62a6bfaf0c
3 files changed
+102 -7

No files matched your search

+20 -7
View File
@@ -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,
}