PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m3s
PR Checks / Backend Smoke (pull_request) Successful in 7s
PR Checks / Build Backend (no push) (pull_request) Successful in 31s
PR Checks / Build Frontend (no push) (pull_request) Successful in 44s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 1m12s
PR Checks / AI Code Review (Claude) (pull_request) Failing after 2m7s
Adds the cut-off distance a parent actually asks about — "how close do we
need to live?" — end to end: a Singer tap, dbt staging and mart models, an
Airflow DAG, and a tile on both detail templates. 3,597 schools across 57
local authorities carry a figure; the rest are unchanged.
There is no national source for this. Each LA publishes its own cut-offs in
its own format, and the collected CSV is transcribed from PDFs, spreadsheets
and web pages — so most of the work here is deciding what is safe to show.
Data
* tap-uk-school-distance loads the CSV verbatim into raw. Keyed on
(urn, year, school_name), because school_name carries the admission
route: (urn, year) alone collides on 118 keys and a reload would have
silently dropped every band but one.
* stg_school_distance applies a 25 m – 25 km plausibility band. The source
contains 0.0-mile rows (published where a school filled on a higher
criterion), 1-metre cut-offs, and one reading 533 miles — ~4% of rows,
all of which would put a visibly wrong number on a live page.
* fact_admission_distance collapses routes to one row per school per year
using the furthest, and keeps route_count so the page can say the figure
is the widest of several bands rather than the one for a given child.
Serving
* Kept out of fact_admissions: that mart is EES-derived and near-complete
for England, this one covers 57 LAs, and the two refresh independently.
* Latest year only. Coverage is ragged — a school may have 2021 and 2026
and nothing between — so a history array would invite a trend line drawn
through gaps that are absences of publication, not of a cut-off.
* The Admissions section now renders on either source. 3% of the schools
that render have a cut-off and no EES admissions row, and gating on
admissions alone would have hidden the figure on those pages.
Interface
* The year travels with the figure everywhere it appears; a cut-off
detached from its admissions round is not a fact about anything.
* "Not a fixed catchment — it moves every year" sits under every instance,
because that is the inference a parent will otherwise draw.
* Replaces a hardcoded "Historical distance cut-off data is not available
for this school" that appeared on every secondary page, including the
ones whose council does publish it. The absence is now stated only when
it is real, and names the authority that would hold it.
The tint costs the muted tokens their AA margin: measured on the composited
backdrop (not the computed one, which reports the untinted card), --text-muted
falls to 4.09:1 in dark theme. The tile uses --text-secondary instead — 6.50:1
dark, 6.60:1 light.
The DAG is manual, like the other annual ones: councils publish on allocation
day, each on its own timetable, so there is no date worth scheduling against.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WDvkyqqHABm4bmth2kjAxE
146 lines
5.2 KiB
TypeScript
146 lines
5.2 KiB
TypeScript
/**
|
|
* PrimarySchoolSections — the section sequence for primary and all-through
|
|
* detail pages. Server component.
|
|
*
|
|
* All-through schools route here rather than to SecondarySchoolSections,
|
|
* because this list renders BOTH the KS2 and KS4 blocks (ResultsSection and
|
|
* HistorySection branch on isAllThrough); the secondary list is KS4-only and
|
|
* would silently drop their SATs data.
|
|
*
|
|
* The render conditions here MUST match buildNavItems in lib/schoolSections,
|
|
* or the sticky nav will link to sections that do not exist.
|
|
*/
|
|
|
|
import type {
|
|
School, SchoolResult, AbsenceData, OfstedInspection, SchoolCensus,
|
|
SchoolAdmissions, SchoolAdmissionDistance, SchoolDeprivation, SchoolFinance, NationalAverages,
|
|
} from '@/lib/types';
|
|
import { ofstedLegacyAreas } from '@/lib/utils';
|
|
import type { SchoolFlags } from '@/lib/schoolSections';
|
|
import { OfstedSection } from './OfstedSection';
|
|
import { ResultsSection } from './ResultsSection';
|
|
import { AdmissionsSection } from './AdmissionsSection';
|
|
import { InclusionSection } from './InclusionSection';
|
|
import { HistorySection } from './HistorySection';
|
|
import { SchoolLifeSection } from './SchoolLifeSection';
|
|
import { LocalAreaSection } from './LocalAreaSection';
|
|
import { FinancesSection } from './FinancesSection';
|
|
|
|
export interface PrimarySchoolSectionsProps {
|
|
schoolInfo: School;
|
|
yearlyData: SchoolResult[];
|
|
absenceData: AbsenceData | null;
|
|
ofsted: OfstedInspection | null;
|
|
census: SchoolCensus | null;
|
|
admissions: SchoolAdmissions | null;
|
|
admissionsHistory: SchoolAdmissions[];
|
|
admissionDistance: SchoolAdmissionDistance | null;
|
|
deprivation: SchoolDeprivation | null;
|
|
finance: SchoolFinance | null;
|
|
nationalAvg: NationalAverages | null;
|
|
flags: SchoolFlags;
|
|
}
|
|
|
|
export function PrimarySchoolSections({
|
|
schoolInfo, yearlyData, absenceData, ofsted, census,
|
|
admissions, admissionsHistory, admissionDistance, deprivation, finance, nationalAvg, flags,
|
|
}: PrimarySchoolSectionsProps) {
|
|
const primaryAvg = nationalAvg?.primary ?? {};
|
|
const secondaryAvg = nationalAvg?.secondary ?? {};
|
|
|
|
const isReportCard = !!(ofsted?.report_card && Object.keys(ofsted.report_card).length > 0);
|
|
// Report cards are dated by their own inspection (rc_inspection_date), never
|
|
// the legacy inspection_date (report cards exist only from Nov 2025).
|
|
const ofstedInspectedDate = isReportCard
|
|
? ofsted?.rc_inspection_date ?? null
|
|
: ofsted?.inspection_date ?? null;
|
|
const oeifAreas = ofsted ? ofstedLegacyAreas(ofsted) : [];
|
|
const oeifAllSameGrade =
|
|
!!ofsted &&
|
|
!isReportCard &&
|
|
oeifAreas.length >= 3 &&
|
|
oeifAreas.every((a) => a.value === ofsted.overall_effectiveness);
|
|
|
|
return (
|
|
<>
|
|
{ofsted && (
|
|
<OfstedSection
|
|
ofsted={ofsted}
|
|
urn={schoolInfo.urn}
|
|
isReportCard={isReportCard}
|
|
ofstedInspectedDate={ofstedInspectedDate}
|
|
oeifAllSameGrade={oeifAllSameGrade}
|
|
oeifAreas={oeifAreas}
|
|
variant="primary"
|
|
/>
|
|
)}
|
|
|
|
{flags.hasAnyResults && flags.latestResults && (
|
|
<ResultsSection
|
|
latestResults={flags.latestResults}
|
|
schoolInfo={schoolInfo}
|
|
primaryAvg={primaryAvg}
|
|
secondaryAvg={secondaryAvg}
|
|
isAllThrough={flags.isAllThrough}
|
|
isSecondary={flags.isSecondary}
|
|
isSpecial={flags.isSpecial}
|
|
hasKS2Results={flags.hasKS2Results}
|
|
hasKS4Results={flags.hasKS4Results}
|
|
ks2Placeholder={flags.ks2Placeholder}
|
|
suppressKs2Comparison={flags.suppressKs2Comparison}
|
|
suppressKs4Comparison={flags.suppressKs4Comparison}
|
|
/>
|
|
)}
|
|
|
|
{/* Either source is enough to justify the section. The cut-off distance
|
|
and the EES admissions figures come from different places and a
|
|
minority of schools have one without the other — gating on admissions
|
|
alone would hide a published distance on those pages. */}
|
|
{(admissions || admissionDistance) && (
|
|
<AdmissionsSection
|
|
admissions={admissions}
|
|
admissionsHistory={admissionsHistory}
|
|
admissionDistance={admissionDistance}
|
|
isAllThrough={flags.isAllThrough}
|
|
/>
|
|
)}
|
|
|
|
{flags.hasInclusionData && (
|
|
<InclusionSection
|
|
latestResults={flags.latestResults}
|
|
census={census}
|
|
hasGenderSplit={flags.hasGenderSplit}
|
|
primaryAvg={primaryAvg}
|
|
/>
|
|
)}
|
|
|
|
{yearlyData.length > 0 && (
|
|
<HistorySection
|
|
yearlyData={yearlyData}
|
|
schoolInfo={schoolInfo}
|
|
nationalAvg={nationalAvg}
|
|
primaryAvg={primaryAvg}
|
|
secondaryAvg={secondaryAvg}
|
|
isAllThrough={flags.isAllThrough}
|
|
isPrimary={flags.isPrimary}
|
|
isSecondary={flags.isSecondary}
|
|
hasKS2Results={flags.hasKS2Results}
|
|
hasKS4Results={flags.hasKS4Results}
|
|
suppressKs2Comparison={flags.suppressKs2Comparison}
|
|
suppressKs4Comparison={flags.suppressKs4Comparison}
|
|
/>
|
|
)}
|
|
|
|
{flags.hasSchoolLife && (
|
|
<SchoolLifeSection absenceData={absenceData} primaryAvg={primaryAvg} />
|
|
)}
|
|
|
|
{flags.hasDeprivation && deprivation && (
|
|
<LocalAreaSection deprivation={deprivation} />
|
|
)}
|
|
|
|
{flags.hasFinance && finance && <FinancesSection finance={finance} />}
|
|
</>
|
|
);
|
|
}
|