Compare commits

..
Author SHA1 Message Date
TudorandClaude Opus 5.5 0269eec9c4 test(e2e): never pick an ISI school for the not-yet-inspected journey (review)
PR Checks / Frontend Typecheck + Tests (pull_request) Waiting to run
PR Checks / Backend Smoke (pull_request) Waiting to run
PR Checks / Build Backend (no push) (pull_request) Waiting to run
PR Checks / Build Frontend (no push) (pull_request) Waiting to run
PR Checks / Build Pipeline (no push) (pull_request) Waiting to run
PR Checks / AI Code Review (Claude) (pull_request) Waiting to run
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:55:32 +01:00
TudorandClaude Opus 5.5 81de008e4a test(e2e): further education, ISI and not-yet-inspected states (H4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:35:53 +01:00
TudorandClaude Opus 5.5 9feb37b974 fix(school, compare): ISI, not-yet-inspected and further-education states (H4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:35:10 +01:00
TudorandClaude Opus 5.5 ab25d83b9d fix(search): no "Not yet inspected" without Ofsted's word; ISI badge (H4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:33:14 +01:00
TudorandClaude Opus 5.5 69da9d84db fix(api): inspectorate and Ofsted listing in payloads; Ofsted links that resolve (H4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:32:06 +01:00
28 changed files with 367 additions and 60 deletions

No files matched your search

+8
View File
@@ -1048,6 +1048,11 @@ async def get_school_details(request: Request, urn: int):
"age_range": latest.get("age_range", ""),
"has_sixth_form": latest.get("has_sixth_form"),
"nursery_provision": latest.get("nursery_provision"),
# What the Ofsted badge may say when there is no inspection record
# (audit H4): the inspectorate GIAS names, and whether Ofsted's own
# data lists the school at all.
"inspectorate": latest.get("inspectorate"),
"ofsted_listed": latest.get("ofsted_listed"),
# The header's Selective flag reads it (lib/schoolFacts).
"admissions_policy": latest.get("admissions_policy"),
"status": latest.get("status"),
@@ -1210,6 +1215,9 @@ async def compare_schools(
# schools get entrance-test framing, never the distance template.
"admissions_policy": convert_to_native(latest.get("admissions_policy")),
"has_sixth_form": convert_to_native(latest.get("has_sixth_form")),
# Audit H4: what compare may say when there is no inspection record.
"inspectorate": convert_to_native(latest.get("inspectorate")),
"ofsted_listed": convert_to_native(latest.get("ofsted_listed")),
"capacity": convert_to_native(latest.get("capacity")),
"gias_total_pupils": convert_to_native(latest.get("gias_total_pupils")),
"trust_name": convert_to_native(latest.get("trust_name")),
+8 -1
View File
@@ -251,6 +251,8 @@ _MAIN_QUERY = text("""
s.website,
s.telephone,
s.nursery_provision,
s.inspectorate,
s.ofsted_listed,
foi.current_grade AS ofsted_grade,
foi.current_grade_date AS ofsted_grade_date,
foi.latest_visit_date AS ofsted_date,
@@ -376,7 +378,8 @@ for _old, _new in _LEGACY_NAME_REPLACEMENTS:
_MAIN_QUERY_LEGACY_NAMES = text(_MAIN_QUERY_LEGACY_NAMES)
# Fallback used when marts predate the optional GIAS/location detail columns
# (telephone, nursery provision, county, parliamentary constituency) — i.e. the
# (telephone, nursery provision, county, parliamentary constituency, and the
# inspectorate / Ofsted-listing columns added for audit H4) — i.e. the
# nightly dbt pipeline hasn't rebuilt the mart yet on this DB. Keeps each column
# present as NULL so the whole data load degrades gracefully instead of failing.
_EXTRA_OPTIONAL_COLUMNS = (
@@ -384,6 +387,8 @@ _EXTRA_OPTIONAL_COLUMNS = (
"nursery_provision",
"county",
"parliamentary_constituency",
"inspectorate",
"ofsted_listed",
)
_MAIN_QUERY_NO_EXTRA_COLS = str(_MAIN_QUERY)
for _col, _tbl in (
@@ -391,6 +396,8 @@ for _col, _tbl in (
("nursery_provision", "s"),
("county", "l"),
("parliamentary_constituency", "l"),
("inspectorate", "s"),
("ofsted_listed", "s"),
):
_src = f"{_tbl}.{_col},"
assert _src in _MAIN_QUERY_NO_EXTRA_COLS, (
+6 -3
View File
@@ -39,6 +39,9 @@ def report_card_labels(ofsted: dict) -> dict:
def ofsted_page_url(urn: int) -> str:
"""The school's page on ofsted.gov.uk (all its reports live there —
we never deep-link an individual report)."""
return f"https://reports.ofsted.gov.uk/provider/21/{urn}"
"""The school's page on ofsted.gov.uk (all its reports live there — we
never deep-link an individual report). Ofsted's provider pages sit under a
category (21 primary, 23 secondary, 25 special, 40 FE colleges, …); this
address redirects to the right one, where provider/21 404s for every
non-primary school."""
return f"https://reports.ofsted.gov.uk/inspection-reports/find-inspection-report/provider/ELS/{urn}"
+2
View File
@@ -578,6 +578,8 @@ SCHOOL_COLUMNS = [
"ofsted_date",
"ofsted_framework",
"ofsted_rc_date",
"inspectorate",
"ofsted_listed",
"latitude",
"longitude",
]
@@ -0,0 +1,63 @@
"""Audit H4: every payload that feeds an Ofsted badge carries the inspectorate
GIAS names and whether Ofsted's own data lists the school, so the site never
says "Not yet inspected" on the strength of missing data."""
import pandas as pd
import pytest
from fastapi.testclient import TestClient
def _df():
base = dict(year=202425, local_authority='Cambridgeshire', local_authority_code=873,
postcode='CB1 1AA', latitude=52.2, longitude=0.12, address='1 High St',
status='Open', town='Cambridge')
return pd.DataFrame([
dict(base, urn=136083, school_name='Abbey College Cambridge', phase='Secondary',
school_type='Other independent school', inspectorate='ISI', ofsted_listed=False),
dict(base, urn=150031, school_name='Abbas and Templecombe Church School', phase='Primary',
school_type='Academy converter', inspectorate='Not applicable', ofsted_listed=True),
])
@pytest.fixture()
def client(monkeypatch):
from backend import app as app_module
from backend import database as database_module
def _no_db():
raise RuntimeError('no database in this test')
monkeypatch.setattr(app_module, 'load_latest_school_data', _df)
monkeypatch.setattr(app_module, 'load_school_data', _df)
monkeypatch.setattr(app_module, 'get_supplementary_data', lambda db, urn: {})
monkeypatch.setattr(app_module, '_place_registry', None)
monkeypatch.setattr(database_module, 'SessionLocal', _no_db)
return TestClient(app_module.app, raise_server_exceptions=False)
def test_the_list_carries_inspectorate_and_listing(client):
resp = client.get('/api/schools?page_size=50')
assert resp.status_code == 200, resp.text
by_urn = {s['urn']: s for s in resp.json()['schools']}
assert by_urn[136083]['inspectorate'] == 'ISI'
assert by_urn[150031]['ofsted_listed'] is True
def test_the_school_page_carries_them(client):
info = client.get('/api/schools/136083').json()['school_info']
assert info['inspectorate'] == 'ISI'
assert info['ofsted_listed'] is False
def test_compare_carries_them(client):
body = client.get('/api/compare?urns=136083,150031').json()
assert body['comparison']['150031']['school_info']['ofsted_listed'] is True
assert body['comparison']['136083']['school_info']['inspectorate'] == 'ISI'
def test_a_mart_built_before_the_columns_still_loads():
# Missing columns make the main query retry with NULLs (data_loader).
from backend import data_loader
assert {'inspectorate', 'ofsted_listed'} <= set(data_loader._EXTRA_OPTIONAL_COLUMNS)
fallback = str(data_loader._MAIN_QUERY_NO_EXTRA_COLS)
assert 'NULL AS inspectorate,' in fallback and 'NULL AS ofsted_listed,' in fallback
+1 -1
View File
@@ -42,4 +42,4 @@ def test_unknown_code_is_skipped_not_crashed():
def test_provider_url():
assert ofsted_page_url(138690) == "https://reports.ofsted.gov.uk/provider/21/138690"
assert ofsted_page_url(138690) == "https://reports.ofsted.gov.uk/inspection-reports/find-inspection-report/provider/ELS/138690"
@@ -33,7 +33,7 @@ def test_report_card_block_and_provider_url():
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"
assert block["ofsted_page_url"] == "https://reports.ofsted.gov.uk/inspection-reports/find-inspection-report/provider/ELS/100140"
def test_no_grade_is_carried_past_a_newer_inspection():
+43
View File
@@ -554,6 +554,49 @@ test('a grade confirmed at an ungraded visit says so and is dated by it', async
await expect(section.getByText('Confirmed at an ungraded inspection, 18 July 2024')).toBeVisible();
});
test('a further education college shows its Ofsted result, not "Not yet inspected"', async ({ page }) => {
// Queen Mary's College (145175) is inspected under Ofsted's further education
// framework, which the site did not load: its badge said "Not yet inspected"
// (audit H4). Its result may change; the label must never be absent or wrong.
const res = await page.request.get('/api/schools/145175');
expect(res.ok()).toBeTruthy();
await page.goto('/school/145175');
const ofsted = page.locator('#ofsted');
await expect(ofsted).toBeVisible();
await expect(ofsted.getByText('Further education and skills')).toBeVisible();
await expect(ofsted).not.toContainText('Not yet inspected');
});
test('an ISI-inspected school says so instead of "Not yet inspected"', async ({ page }) => {
const res = await page.request.get('/api/schools?school_type=independent&phase=secondary&page_size=100');
expect(res.ok()).toBeTruthy();
const school = ((await res.json()).schools ?? []).find(
(s: { inspectorate?: string | null; ofsted_date?: string | null; ofsted_rc_date?: string | null }) =>
s.inspectorate === 'ISI' && !s.ofsted_date && !s.ofsted_rc_date);
test.skip(!school, 'no ISI school without an Ofsted record here');
await page.goto(`/school/${school.urn}`);
await expect(page.getByText('Inspected by the Independent Schools Inspectorate (ISI)')).toBeVisible();
await expect(page.locator('#ofsted')).not.toContainText('Not yet inspected');
});
test('"Not yet inspected" appears only for a school Ofsted lists without an inspection', async ({ page }) => {
const res = await page.request.get('/api/schools?phase=primary&page_size=500');
expect(res.ok()).toBeTruthy();
type Row = { urn: number; inspectorate?: string | null; ofsted_listed?: boolean | null; ofsted_date?: string | null; ofsted_rc_date?: string | null; ofsted_grade?: number | null };
const schools: Row[] = (await res.json()).schools ?? [];
// No Ofsted record, and not an ISI school (whose badge names ISI instead).
const uninspected = (s: Row) => !s.ofsted_date && !s.ofsted_rc_date && !s.ofsted_grade && s.inspectorate !== 'ISI';
const unlisted = schools.find(s => !s.ofsted_listed && uninspected(s));
const listed = schools.find(s => s.ofsted_listed && uninspected(s));
test.skip(!listed, 'no listed, uninspected primary on this page');
await page.goto(`/school/${listed!.urn}`);
await expect(page.locator('#ofsted').getByText('Not yet inspected')).toBeVisible();
if (unlisted) {
await page.goto(`/school/${unlisted.urn}`);
await expect(page.getByText('Not yet inspected')).toHaveCount(0);
}
});
test('school detail page renders name and performance data', async ({ page }) => {
await searchByName(page, 'primary');
const firstSchool = schoolLinks(page).first();
@@ -69,6 +69,18 @@ const data: Record<string, ComparisonData> = {
};
describe('CompareOfsted', () => {
it('words a school without an Ofsted record from the evidence (audit H4)', () => {
const isi = { ...school(4, 'ISI School'), inspectorate: 'ISI' } as School;
const listed = { ...school(5, 'New Academy'), ofsted_listed: true } as School;
render(<CompareOfsted schools={[isi, listed]} data={{
'4': { school_info: isi, yearly_data: [] } as unknown as ComparisonData,
'5': { school_info: listed, yearly_data: [] } as unknown as ComparisonData,
}} />);
expect(screen.getAllByText('Inspected by ISI').length).toBeGreaterThan(0);
expect(screen.getAllByText('Not yet inspected').length).toBeGreaterThan(0);
expect(screen.queryByText(/No inspection outcome in our dataset/)).toBeNull();
});
it('renders the three regimes without inventing an overall grade for report cards', () => {
render(<CompareOfsted schools={schools} data={data} />);
@@ -0,0 +1,20 @@
import { render, screen } from '@testing-library/react';
import { InspectionNotice } from '@/components/school/InspectionNotice';
it('names ISI and links its reports for an ISI school (audit H4)', () => {
render(<InspectionNotice school={{ urn: 136083, inspectorate: 'ISI', ofsted_listed: false }} />);
expect(screen.getByText('Inspected by the Independent Schools Inspectorate (ISI)')).toBeInTheDocument();
expect(screen.getByRole('link', { name: /ISI reports/ })).toHaveAttribute('href', 'https://www.isi.net/reports/');
});
it('says "Not yet inspected" for a school Ofsted lists without an inspection', () => {
render(<InspectionNotice school={{ urn: 150031, inspectorate: 'Not applicable', ofsted_listed: true }} />);
expect(screen.getByText('Not yet inspected')).toBeInTheDocument();
expect(screen.getByRole('link', { name: /Ofsted reports/ })).toHaveAttribute(
'href', 'https://reports.ofsted.gov.uk/inspection-reports/find-inspection-report/provider/ELS/150031');
});
it('renders nothing without inspection data, including an unknown listing', () => {
const { container } = render(<InspectionNotice school={{ urn: 1, inspectorate: null, ofsted_listed: null }} />);
expect(container).toBeEmptyDOMElement();
});
@@ -82,3 +82,14 @@ it('prints an ungraded outcome when there is no grade (Oakgrove)', () => {
expect(screen.getByText('No overall grade')).toBeInTheDocument();
expect(screen.getByText(/Ungraded inspection, 13 November 2024: Standards maintained/)).toBeInTheDocument();
});
it('labels a further education record and prints its short inspection (audit H4)', () => {
renderSection({
framework: 'Further education and skills',
current_grade: { grade: 2, date: '2019-03-05', basis: 'graded' },
latest_visit: { date: '2023-02-01', kind: 'short', outcome: null },
});
expect(screen.getByText('Further education and skills')).toBeInTheDocument();
expect(screen.getByText('Good')).toBeInTheDocument();
expect(screen.getByText('Latest visit: Short inspection, 1 February 2023')).toBeInTheDocument();
});
@@ -52,3 +52,8 @@ describe('SchoolRow shares the school page flags', () => {
expect(screen.getByText("Boys' school")).toBeInTheDocument();
});
});
it('renders no Ofsted badge for a school with no inspection data (audit H4)', () => {
const { container } = render(<SchoolRow school={{ ...base, ofsted_grade: null, ofsted_date: null, ofsted_listed: false, inspectorate: null }} />);
expect(container.textContent).not.toMatch(/Not yet inspected|Inspected/);
});
@@ -19,6 +19,7 @@ import {
summariseAdmissions,
summariseReportCard,
verdict,
noInspectionLabel,
} from '@/lib/compareLogic';
import type { OfstedInspection, SchoolAdmissions } from '@/lib/types';
@@ -359,3 +360,14 @@ describe('basket limits per comparison group', () => {
expect(kept).toEqual([0, 1, 2, 3, 4, 7, 8, 9]);
});
});
it('shows an FE report card with only a safeguarding result as a report card (audit H4)', () => {
const display = ofstedDisplay(ofsted({ rc_inspection_date: '2026-05-12', rc_safeguarding_met: true, report_card: {} }));
expect(display.kind).toBe('report_card');
});
it('words the no-record compare cell from the evidence', () => {
expect(noInspectionLabel({ inspectorate: 'ISI' })).toBe('Inspected by ISI');
expect(noInspectionLabel({ ofsted_listed: true })).toBe('Not yet inspected');
expect(noInspectionLabel({ ofsted_listed: null })).toBe('No inspection in our dataset');
});
+10 -1
View File
@@ -1,4 +1,4 @@
import { gradeSourceLine, latestVisitLine, showLatestVisitLine } from '@/lib/ofstedStatus';
import { gradeSourceLine, hasReportCard, latestVisitLine, showLatestVisitLine } from '@/lib/ofstedStatus';
import type { OfstedInspection } from '@/lib/types';
const base = { report_card: {} } as unknown as OfstedInspection;
@@ -45,3 +45,12 @@ describe('showLatestVisitLine', () => {
latest_visit: { date: '2025-06-17', kind: 'graded', outcome: null } })).toBe(false);
});
});
it('names a short inspection as Ofsted does', () => {
expect(latestVisitLine({ date: '2023-02-01', kind: 'short', outcome: null })).toBe('Short inspection, 1 February 2023');
});
it('treats a report card with only a safeguarding result as a report card (FE)', () => {
expect(hasReportCard({ rc_inspection_date: '2026-05-12', report_card: {} })).toBe(true);
expect(hasReportCard({ rc_inspection_date: null, report_card: {} })).toBe(false);
});
+38 -26
View File
@@ -148,40 +148,40 @@ describe('ofstedLegacyAreas', () => {
describe('buildOfstedListBadge', () => {
it('returns grade word + year for OEIF Outstanding', () => {
const badge = buildOfstedListBadge({ ofsted_grade: 1, ofsted_grade_date: '2023-11-15', ofsted_date: '2023-11-15', ofsted_framework: 'OEIF' });
expect(badge.label).toBe('Outstanding · 2023');
expect(badge.cssClass).toBe('ofsted1');
expect(badge!.label).toBe('Outstanding · 2023');
expect(badge!.cssClass).toBe('ofsted1');
});
it('returns grade word for each OEIF grade', () => {
expect(buildOfstedListBadge({ ofsted_grade: 2, ofsted_grade_date: '2022-05-01' }).label).toBe('Good · 2022');
expect(buildOfstedListBadge({ ofsted_grade: 3, ofsted_grade_date: '2021-01-01' }).label).toBe('Req. Improvement · 2021');
expect(buildOfstedListBadge({ ofsted_grade: 4, ofsted_grade_date: '2020-03-01' }).label).toBe('Inadequate · 2020');
expect(buildOfstedListBadge({ ofsted_grade: 2, ofsted_grade_date: '2022-05-01' })!.label).toBe('Good · 2022');
expect(buildOfstedListBadge({ ofsted_grade: 3, ofsted_grade_date: '2021-01-01' })!.label).toBe('Req. Improvement · 2021');
expect(buildOfstedListBadge({ ofsted_grade: 4, ofsted_grade_date: '2020-03-01' })!.label).toBe('Inadequate · 2020');
});
it('prints a grade without a year when its date is missing', () => {
const badge = buildOfstedListBadge({ ofsted_grade: 2, ofsted_grade_date: null, ofsted_date: '2025-01-01' });
expect(badge.label).toBe('Good');
expect(badge.cssClass).toBe('ofsted2');
expect(badge!.label).toBe('Good');
expect(badge!.cssClass).toBe('ofsted2');
});
it('dates a grade by the inspection that awarded it, not the latest visit', () => {
// Washwood Heath: Good from a March 2020 graded inspection; latest visit
// an ungraded one in May 2025 (audit M1).
const badge = buildOfstedListBadge({ ofsted_grade: 2, ofsted_grade_date: '2020-03-03', ofsted_date: '2025-05-21' });
expect(badge.label).toBe('Good · 2020');
expect(badge!.label).toBe('Good · 2020');
});
it('shows Inspected for a latest inspection that gave no grade (Rabbsfarm)', () => {
// Audit C1: this used to read "Good · 2025", from a 2020 ungraded visit.
const badge = buildOfstedListBadge({ ofsted_grade: null, ofsted_grade_date: null, ofsted_date: '2025-06-17' });
expect(badge.label).toBe('Inspected · 2025');
expect(badge.cssClass).toBe('ofstedInspected');
expect(badge!.label).toBe('Inspected · 2025');
expect(badge!.cssClass).toBe('ofstedInspected');
});
it('returns a Report Card badge when ofsted_rc_date is present', () => {
const badge = buildOfstedListBadge({ ofsted_grade: null, ofsted_rc_date: '2026-02-03' });
expect(badge.label).toBe('Report Card · 2026');
expect(badge.cssClass).toBe('ofstedRc');
expect(badge!.label).toBe('Report Card · 2026');
expect(badge!.cssClass).toBe('ofstedRc');
});
it('a report card wins over a carried-forward legacy grade', () => {
@@ -194,34 +194,46 @@ describe('buildOfstedListBadge', () => {
ofsted_framework: 'Schools - S5',
ofsted_rc_date: '2026-02-03',
});
expect(badge.label).toBe('Report Card · 2026');
expect(badge.cssClass).toBe('ofstedRc');
expect(badge!.label).toBe('Report Card · 2026');
expect(badge!.cssClass).toBe('ofstedRc');
});
it('returns an "Inspected" badge for an OEIF inspection with no overall grade (post-Sept-2024)', () => {
// Inspected after Sept 2024: inspection on record (date + framework) but
// Ofsted no longer issues an overall grade. Must NOT read as "Not yet inspected".
const badge = buildOfstedListBadge({ ofsted_grade: null, ofsted_date: '2024-11-01', ofsted_framework: 'OEIF' });
expect(badge.label).toBe('Inspected · 2024');
expect(badge.cssClass).toBe('ofstedInspected');
expect(badge!.label).toBe('Inspected · 2024');
expect(badge!.cssClass).toBe('ofstedInspected');
});
it('returns an "Inspected" badge without a year when the grade is missing and date is absent but a record exists', () => {
const badge = buildOfstedListBadge({ ofsted_grade: null, ofsted_date: null, ofsted_framework: 'OEIF' });
expect(badge.label).toBe('Inspected');
expect(badge.cssClass).toBe('ofstedInspected');
expect(badge!.label).toBe('Inspected');
expect(badge!.cssClass).toBe('ofstedInspected');
});
it('returns pending badge when no grade and no inspection on record', () => {
const badge = buildOfstedListBadge({ ofsted_grade: null, ofsted_date: null, ofsted_framework: null });
expect(badge.label).toBe('Not yet inspected');
expect(badge.cssClass).toBe('ofstedPending');
it('says "Not yet inspected" only when Ofsted lists the school uninspected (audit H4)', () => {
const badge = buildOfstedListBadge({ ofsted_grade: null, ofsted_date: null, ofsted_listed: true });
expect(badge).toEqual({ label: 'Not yet inspected', cssClass: 'ofstedPending' });
});
it('returns pending badge when all fields are undefined', () => {
const badge = buildOfstedListBadge({});
expect(badge.label).toBe('Not yet inspected');
expect(badge.cssClass).toBe('ofstedPending');
it('names ISI for a school GIAS says ISI inspects', () => {
expect(buildOfstedListBadge({ ofsted_date: null, inspectorate: 'ISI', ofsted_listed: false }))
.toEqual({ label: 'Inspected by ISI', cssClass: 'ofstedPending' });
});
it('an Ofsted record wins over ISI', () => {
expect(buildOfstedListBadge({ ofsted_grade: 2, ofsted_grade_date: '2022-03-01', inspectorate: 'ISI' })!.label)
.toBe('Good · 2022');
});
it('gives no badge without any inspection data', () => {
expect(buildOfstedListBadge({ ofsted_grade: null, ofsted_date: null, ofsted_framework: null })).toBeNull();
expect(buildOfstedListBadge({ ofsted_listed: null, inspectorate: 'Not applicable' })).toBeNull();
});
it('gives no badge when all fields are undefined (audit H4)', () => {
expect(buildOfstedListBadge({})).toBeNull();
});
});
+1 -1
View File
@@ -104,7 +104,7 @@ function cardHtml(school: School, ctx: CardContext): string {
? `${metric ? ' · ' : ''}${school.total_pupils.toLocaleString('en-GB')} pupils` : '';
return `<div class="sc-card">
<div class="sc-card-head"><a class="sc-card-name" href="${href}">${escapeHtml(school.school_name)}</a>${distance}</div>
<div class="sc-card-tags"><span class="sc-ofsted sc-${badge.cssClass}">${escapeHtml(badge.label)}</span>${type}</div>
<div class="sc-card-tags">${badge ? `<span class="sc-ofsted sc-${badge.cssClass}">${escapeHtml(badge.label)}</span>` : ''}${type}</div>
${metric || pupils ? `<div class="sc-card-metric">${metric}${pupils}</div>` : ''}
<div class="sc-card-actions">
<a class="btn btn-tertiary btn-sm" href="${href}">View</a>
+5 -3
View File
@@ -66,9 +66,11 @@ export function SchoolRow({
<a href={schoolUrl(school.urn, school.school_name)} className={styles.schoolName}>
{school.school_name}
</a>
<span className={`${styles.ofstedBadge} ${styles[ofstedBadge.cssClass]}`}>
{ofstedBadge.label}
</span>
{ofstedBadge && (
<span className={`${styles.ofstedBadge} ${styles[ofstedBadge.cssClass]}`}>
{ofstedBadge.label}
</span>
)}
</div>
{/* Line 2: Context tags */}
+5 -3
View File
@@ -67,9 +67,11 @@ export function SecondarySchoolRow({
<a href={schoolUrl(school.urn, school.school_name)} className={styles.schoolName}>
{school.school_name}
</a>
<span className={`${styles.ofstedBadge} ${styles[ofstedBadge.cssClass]}`}>
{ofstedBadge.label}
</span>
{ofstedBadge && (
<span className={`${styles.ofstedBadge} ${styles[ofstedBadge.cssClass]}`}>
{ofstedBadge.label}
</span>
)}
</div>
{/* Line 2: Context tags */}
@@ -14,6 +14,7 @@ import {
summariseAdmissions,
verdict,
type ReportCardSummary,
noInspectionLabel,
} from '@/lib/compareLogic';
import type { Benchmarks, ComparisonData, NationalAverages, School } from '@/lib/types';
import { Cell, Chip, Measure, Section, SectionGrid, sectionStyles as s } from './sectionShared';
@@ -91,7 +92,7 @@ export function CompareAtAGlance({
No overall grade
</span>
)}
{display.kind === 'none' && <span className={s.small}>No inspection in our dataset</span>}
{display.kind === 'none' && <span className={s.small}>{noInspectionLabel(data[String(school.urn)]?.school_info ?? school)}</span>}
</Cell>
);
})}
@@ -12,6 +12,7 @@ import {
ofstedDisplay,
rcAreaLabel,
type OfstedDisplay,
noInspectionLabel,
} from '@/lib/compareLogic';
import { gradeSourceLine } from '@/lib/ofstedStatus';
import type { ComparisonData, OfstedCurrentGrade, OfstedInspection, School } from '@/lib/types';
@@ -41,9 +42,11 @@ function yearsSince(iso: string | null): number | null {
return (Date.now() - d.getTime()) / (365.25 * 24 * 3600 * 1000);
}
function ResultCell({ display, current }: { display: OfstedDisplay; current: OfstedCurrentGrade | null }) {
function ResultCell({ display, current, school }: {
display: OfstedDisplay; current: OfstedCurrentGrade | null; school: School;
}) {
if (display.kind === 'none') {
return <span className={s.small}>No inspection outcome in our dataset</span>;
return <span className={s.small}>{noInspectionLabel(school)}</span>;
}
if (display.kind === 'report_card') {
return (
@@ -175,7 +178,7 @@ export function CompareOfsted({
<Measure label="Result">
{schools.map((school, i) => (
<Cell key={school.urn} school={school} index={i}>
<ResultCell display={displays[i]} current={data[String(school.urn)]?.ofsted?.current_grade ?? null} />
<ResultCell display={displays[i]} current={data[String(school.urn)]?.ofsted?.current_grade ?? null} school={data[String(school.urn)]?.school_info ?? school} />
</Cell>
))}
</Measure>
@@ -213,7 +216,7 @@ export function CompareOfsted({
schoolName={school.school_name}
/>
) : (
<span className={s.small}>No inspection in our dataset</span>
<span className={s.small}>{noInspectionLabel(data[String(school.urn)]?.school_info ?? school)}</span>
)}
</Cell>
);
@@ -228,7 +231,7 @@ export function CompareOfsted({
{schools.map((school, i) => {
const url =
data[String(school.urn)]?.ofsted?.ofsted_page_url ??
`https://reports.ofsted.gov.uk/provider/21/${school.urn}`;
`https://reports.ofsted.gov.uk/inspection-reports/find-inspection-report/provider/ELS/${school.urn}`;
return (
<Cell key={school.urn} school={school} index={i}>
{/* Short visible label to save space on mobile (the coloured
@@ -0,0 +1,46 @@
/**
* InspectionNotice — the school page's inspection section when there is no
* Ofsted inspection record (audit H4): the inspectorate GIAS names (ISI), or
* Ofsted's own "not yet inspected" when its data lists the school. Nothing
* otherwise: no claim the data cannot support. Server component.
*/
import type { School } from '@/lib/types';
import { Section, sectionStyles as styles } from './sectionShared';
const OFSTED_REPORTS = 'https://reports.ofsted.gov.uk/inspection-reports/find-inspection-report/provider/ELS/';
const ISI_REPORTS = 'https://www.isi.net/reports/';
export function InspectionNotice({ school }: { school: Pick<School, 'urn' | 'inspectorate' | 'ofsted_listed'> }) {
if (school.inspectorate === 'ISI') {
return (
<Section id="ofsted">
<h2 className={styles.sectionTitle}>
Inspection
<a href={ISI_REPORTS} target="_blank" rel="noopener noreferrer" className={styles.ofstedReportLink}
data-umami-event="external_link_clicked" data-umami-event-target="isi">
ISI reports ↗
</a>
</h2>
<p className={styles.ofstedDisclaimer}>Inspected by the Independent Schools Inspectorate (ISI)</p>
</Section>
);
}
if (school.ofsted_listed) {
return (
<Section id="ofsted">
<h2 className={styles.sectionTitle}>
Ofsted Rating
<a href={`${OFSTED_REPORTS}${school.urn}`} target="_blank" rel="noopener noreferrer"
className={styles.ofstedReportLink} data-umami-event="external_link_clicked" data-umami-event-target="ofsted">
Ofsted reports ↗
</a>
</h2>
<div className={styles.ofstedHeader}>
<span className={styles.ofstedGrade}>Not yet inspected</span>
</div>
</Section>
);
}
return null;
}
@@ -12,7 +12,7 @@
*/
import { ofstedDisplay } from '@/lib/compareLogic';
import { formatOfstedDate, gradeSourceLine, latestVisitLine, showLatestVisitLine } from '@/lib/ofstedStatus';
import { FE_FRAMEWORK, formatOfstedDate, gradeSourceLine, latestVisitLine, showLatestVisitLine } from '@/lib/ofstedStatus';
import type { OfstedInspection } from '@/lib/types';
import { Section, sectionStyles as styles } from './sectionShared';
@@ -71,6 +71,10 @@ export function OfstedSection({
</a>
</h2>
{ofsted.framework === FE_FRAMEWORK && (
<p className={styles.ofstedDisclaimer}>{FE_FRAMEWORK}</p>
)}
{isReportCard ? (
/* ── New Report Card layout ── */
<>
@@ -19,6 +19,8 @@ import type {
import { ofstedLegacyAreas } from '@/lib/utils';
import type { SchoolFlags } from '@/lib/schoolSections';
import { OfstedSection } from './OfstedSection';
import { InspectionNotice } from './InspectionNotice';
import { hasReportCard } from '@/lib/ofstedStatus';
import { ResultsSection } from './ResultsSection';
import { AdmissionsSection } from './AdmissionsSection';
import { DistanceSection } from './DistanceSection';
@@ -54,7 +56,8 @@ export function PrimarySchoolSections({
const primaryAvg = nationalAvg?.primary ?? {};
const secondaryAvg = nationalAvg?.secondary ?? {};
const isReportCard = !!(ofsted?.report_card && Object.keys(ofsted.report_card).length > 0);
// By date as well as area labels: an FE report card has only a safeguarding result (audit H4).
const isReportCard = !!ofsted && hasReportCard(ofsted);
// Report cards are dated by their own inspection (rc_inspection_date);
// anything else by the school's latest visit, never the older inspection a
// grade may come from.
@@ -71,7 +74,7 @@ export function PrimarySchoolSections({
return (
<>
{ofsted && (
{ofsted ? (
<OfstedSection
ofsted={ofsted}
urn={schoolInfo.urn}
@@ -80,7 +83,7 @@ export function PrimarySchoolSections({
oeifAllSameGrade={oeifAllSameGrade}
oeifAreas={oeifAreas}
/>
)}
) : <InspectionNotice school={schoolInfo} />}
{flags.hasAnyResults && flags.latestResults && (
<ResultsSection
@@ -19,6 +19,8 @@ import type {
import { ofstedLegacyAreas } from '@/lib/utils';
import type { SecondaryFlags } from '@/lib/schoolSections';
import { OfstedSection } from './OfstedSection';
import { InspectionNotice } from './InspectionNotice';
import { hasReportCard } from '@/lib/ofstedStatus';
import { GcseSection } from './GcseSection';
import { DestinationsSection } from './DestinationsSection';
import { Post16DestinationsSection } from './Post16DestinationsSection';
@@ -60,7 +62,8 @@ export function SecondarySchoolSections({
}: SecondarySchoolSectionsProps) {
const secondaryAvg = nationalAvg?.secondary ?? {};
const isReportCard = !!(ofsted?.report_card && Object.keys(ofsted.report_card).length > 0);
// By date as well as area labels: an FE report card has only a safeguarding result (audit H4).
const isReportCard = !!ofsted && hasReportCard(ofsted);
const ofstedInspectedDate = isReportCard
? ofsted?.rc_inspection_date ?? null
: ofsted?.latest_visit?.date ?? null;
@@ -74,7 +77,7 @@ export function SecondarySchoolSections({
return (
<div className={styles.secondaryScope}>
{ofsted && (
{ofsted ? (
<OfstedSection
ofsted={ofsted}
urn={schoolInfo.urn}
@@ -83,7 +86,7 @@ export function SecondarySchoolSections({
oeifAllSameGrade={oeifAllSameGrade}
oeifAreas={oeifAreas}
/>
)}
) : <InspectionNotice school={schoolInfo} />}
{flags.hasResults && flags.latestResults && (
<GcseSection
+13 -2
View File
@@ -10,7 +10,8 @@
* methodology instead of thresholding point estimates.
*/
import type { OfstedInspection, SchoolAdmissions } from './types';
import { hasReportCard } from './ofstedStatus';
import type { OfstedInspection, School, SchoolAdmissions } from './types';
// ---------------------------------------------------------------------------
// Verdicts against an anchor (England average or state-school benchmark)
@@ -104,6 +105,16 @@ export type OfstedDisplay =
| { kind: 'no_overall_grade' }
| { kind: 'report_card'; summary: ReportCardSummary };
/**
* The compare cell when a school has no Ofsted record (audit H4): ISI per GIAS,
* Ofsted's own "not yet inspected", or plainly that we hold nothing.
*/
export function noInspectionLabel(school: Pick<School, 'inspectorate' | 'ofsted_listed'>): string {
if (school.inspectorate === 'ISI') return 'Inspected by ISI';
if (school.ofsted_listed) return 'Not yet inspected';
return 'No inspection in our dataset';
}
export function ofstedDisplay(
ofsted: OfstedInspection | null | undefined,
): OfstedDisplay {
@@ -111,7 +122,7 @@ export function ofstedDisplay(
// A report card is the newest inspection format; when present it wins —
// never derive or prefer an overall grade alongside it.
if (ofsted.report_card && Object.keys(ofsted.report_card).length > 0) {
if (hasReportCard(ofsted)) {
return { kind: 'report_card', summary: summariseReportCard(ofsted) };
}
+14
View File
@@ -17,8 +17,22 @@ const VISIT_KIND: Record<OfstedLatestVisit['kind'], string> = {
report_card: 'Report card inspection',
graded: 'Graded inspection',
ungraded: 'Ungraded inspection',
short: 'Short inspection',
};
/** fact_ofsted_latest.framework for Ofsted's further education and skills records (audit H4). */
export const FE_FRAMEWORK = 'Further education and skills';
/**
* A report card is in force. Checked by its date as well as its area labels:
* an FE report card carries a safeguarding result and no school areas.
*/
export function hasReportCard(
ofsted: Pick<OfstedInspection, 'rc_inspection_date' | 'report_card'>,
): boolean {
return !!ofsted.rc_inspection_date || Object.keys(ofsted.report_card ?? {}).length > 0;
}
/** "Graded inspection, 6 July 2016" or "Confirmed at an ungraded inspection, 14 March 2023". */
export function gradeSourceLine(current: OfstedCurrentGrade): string {
const when = formatOfstedDate(current.date);
+7 -2
View File
@@ -86,6 +86,10 @@ export interface School {
/** The school's latest inspection of any kind. */
ofsted_date?: string | null;
ofsted_framework?: string | null;
/** The inspectorate GIAS names ("ISI", "Ofsted", "Not applicable"). Audit H4. */
inspectorate?: string | null;
/** Whether Ofsted's own data lists the school, inspected or not. Audit H4. */
ofsted_listed?: boolean | null;
}
// ============================================================================
@@ -93,7 +97,8 @@ export interface School {
// ============================================================================
export interface OfstedInspection {
framework: 'OEIF' | 'ReportCard' | null;
/** Ofsted's event grouping (e.g. "Schools - S5"), or "Further education and skills" for FE records (audit H4). */
framework: string | null;
inspection_date: string | null;
/** Start date of the report-card inspection itself (Nov 2025+); null otherwise. */
rc_inspection_date?: string | null;
@@ -143,7 +148,7 @@ export interface OfstedCurrentGrade {
export interface OfstedLatestVisit {
date: string;
kind: 'report_card' | 'graded' | 'ungraded';
kind: 'report_card' | 'graded' | 'ungraded' | 'short';
outcome: string | null;
}
+9 -3
View File
@@ -678,7 +678,9 @@ export interface OfstedListBadge {
* or an ungraded visit whose outcome names no grade): "Inspected · YYYY",
* dated by the latest visit, so a school never reads as both inspected and
* "Not yet inspected"
* - No inspection on record: "Not yet inspected" in grey
* - No inspection on record (audit H4): "Inspected by ISI" when GIAS names
* ISI; "Not yet inspected" only when Ofsted's own data lists the school;
* otherwise no badge, never a claim the data cannot support.
*/
export function buildOfstedListBadge(school: {
ofsted_grade?: 1 | 2 | 3 | 4 | null;
@@ -686,7 +688,9 @@ export function buildOfstedListBadge(school: {
ofsted_date?: string | null;
ofsted_framework?: string | null;
ofsted_rc_date?: string | null;
}): OfstedListBadge {
inspectorate?: string | null;
ofsted_listed?: boolean | null;
}): OfstedListBadge | null {
// A report card wins over any carried-forward legacy grade — signalled by
// ofsted_rc_date. ofsted_framework is the raw event grouping ("Schools -
// S5"), never "ReportCard", so it can't detect report cards.
@@ -719,7 +723,9 @@ export function buildOfstedListBadge(school: {
return { label: `Inspected${visitYear ? ` · ${visitYear}` : ''}`, cssClass: 'ofstedInspected' };
}
return { label: 'Not yet inspected', cssClass: 'ofstedPending' };
if (school.inspectorate === 'ISI') return { label: 'Inspected by ISI', cssClass: 'ofstedPending' };
if (school.ofsted_listed) return { label: 'Not yet inspected', cssClass: 'ofstedPending' };
return null;
}
// ============================================================================