fix(ofsted): badge and compare read the grade still in force
Search badges date a grade by the inspection that awarded or confirmed it (ofsted_grade_date) and say 'Inspected · year' when the latest inspection gave no grade. ofstedDisplay's kinds become graded / confirmed / no_overall_grade, read from current_grade rather than a carried-forward overall_effectiveness. lib/ofstedStatus.ts holds the two sentences the school and compare pages share. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a9e3a6a700
commit
a42c586cc8
7 files changed
+179
-52
No files matched your search
@@ -114,16 +114,13 @@ describe('ofstedDisplay', () => {
|
||||
expect(d.kind).toBe('report_card');
|
||||
});
|
||||
|
||||
it('distinguishes graded from carried-forward grades', () => {
|
||||
const graded = ofstedDisplay(
|
||||
ofsted({ overall_effectiveness: 1, grade_source: 'graded' }),
|
||||
);
|
||||
expect(graded).toMatchObject({ kind: 'graded', gradeLabel: 'Outstanding', carriedForward: false });
|
||||
|
||||
const carried = ofstedDisplay(
|
||||
ofsted({ overall_effectiveness: 2, grade_source: 'ungraded_carried_forward' }),
|
||||
);
|
||||
expect(carried).toMatchObject({ kind: 'carried_forward', gradeLabel: 'Good', carriedForward: true });
|
||||
it('distinguishes a graded grade from a confirmed one', () => {
|
||||
expect(ofstedDisplay(ofsted({ current_grade: { grade: 1, date: '2019-10-09', basis: 'graded' },
|
||||
latest_visit: { date: '2025-02-05', kind: 'ungraded', outcome: 'Some aspects not as strong' } })))
|
||||
.toMatchObject({ kind: 'graded', gradeLabel: 'Outstanding', gradeDate: '2019-10-09' });
|
||||
expect(ofstedDisplay(ofsted({ current_grade: { grade: 2, date: '2024-07-18', basis: 'confirmed' },
|
||||
latest_visit: { date: '2024-07-18', kind: 'ungraded', outcome: 'School remains Good' } })))
|
||||
.toMatchObject({ kind: 'confirmed', gradeLabel: 'Good' });
|
||||
});
|
||||
|
||||
it('handles missing data', () => {
|
||||
@@ -131,11 +128,11 @@ describe('ofstedDisplay', () => {
|
||||
expect(ofstedDisplay(ofsted({})).kind).toBe('none');
|
||||
});
|
||||
|
||||
it('identifies transitional inspections without overall grades', () => {
|
||||
const transitional = ofstedDisplay(
|
||||
ofsted({ overall_effectiveness: null, inspection_date: '2024-11-05' }),
|
||||
);
|
||||
expect(transitional.kind).toBe('transitional');
|
||||
it('has no overall grade when the latest inspection gave none', () => {
|
||||
// Rabbsfarm: the graded inspection's own overall is null, so no grade is
|
||||
// in force even though an older ungraded visit said "remains Good".
|
||||
expect(ofstedDisplay(ofsted({ current_grade: null, overall_effectiveness: null,
|
||||
latest_visit: { date: '2025-06-17', kind: 'graded', outcome: null } })).kind).toBe('no_overall_grade');
|
||||
});
|
||||
|
||||
it('uses the four legacy grade words', () => {
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { gradeSourceLine, latestVisitLine, showLatestVisitLine } from '@/lib/ofstedStatus';
|
||||
import type { OfstedInspection } from '@/lib/types';
|
||||
|
||||
const base = { report_card: {} } as unknown as OfstedInspection;
|
||||
|
||||
describe('gradeSourceLine', () => {
|
||||
it('names the graded inspection and its date', () => {
|
||||
expect(gradeSourceLine({ grade: 2, date: '2016-07-06', basis: 'graded' }))
|
||||
.toBe('Graded inspection, 6 July 2016');
|
||||
});
|
||||
it('names the ungraded visit that confirmed it', () => {
|
||||
expect(gradeSourceLine({ grade: 2, date: '2023-03-14', basis: 'confirmed' }))
|
||||
.toBe('Confirmed at an ungraded inspection, 14 March 2023');
|
||||
});
|
||||
});
|
||||
|
||||
describe('latestVisitLine', () => {
|
||||
it('prints the ungraded outcome', () => {
|
||||
expect(latestVisitLine({ date: '2024-11-13', kind: 'ungraded', outcome: 'Standards maintained' }))
|
||||
.toBe('Ungraded inspection, 13 November 2024: Standards maintained');
|
||||
});
|
||||
it('prints a graded visit without an outcome', () => {
|
||||
expect(latestVisitLine({ date: '2025-06-17', kind: 'graded', outcome: null }))
|
||||
.toBe('Graded inspection, 17 June 2025');
|
||||
});
|
||||
});
|
||||
|
||||
describe('showLatestVisitLine', () => {
|
||||
it('shows a later visit than the one the grade came from', () => {
|
||||
expect(showLatestVisitLine({ ...base,
|
||||
current_grade: { grade: 2, date: '2020-03-03', basis: 'graded' },
|
||||
latest_visit: { date: '2025-05-21', kind: 'ungraded', outcome: 'Standards maintained' } })).toBe(true);
|
||||
});
|
||||
it('hides it when the visit is the grade’s own source', () => {
|
||||
expect(showLatestVisitLine({ ...base,
|
||||
current_grade: { grade: 2, date: '2024-07-18', basis: 'confirmed' },
|
||||
latest_visit: { date: '2024-07-18', kind: 'ungraded', outcome: 'School remains Good' } })).toBe(false);
|
||||
});
|
||||
it('shows an ungraded outcome when there is no grade', () => {
|
||||
expect(showLatestVisitLine({ ...base, current_grade: null,
|
||||
latest_visit: { date: '2024-11-13', kind: 'ungraded', outcome: 'Standards maintained' } })).toBe(true);
|
||||
});
|
||||
it('hides it for a graded visit with no grade (the title already dates it)', () => {
|
||||
expect(showLatestVisitLine({ ...base, current_grade: null,
|
||||
latest_visit: { date: '2025-06-17', kind: 'graded', outcome: null } })).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -147,23 +147,37 @@ describe('ofstedLegacyAreas', () => {
|
||||
|
||||
describe('buildOfstedListBadge', () => {
|
||||
it('returns grade word + year for OEIF Outstanding', () => {
|
||||
const badge = buildOfstedListBadge({ ofsted_grade: 1, ofsted_date: '2023-11-15', ofsted_framework: 'OEIF' });
|
||||
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');
|
||||
});
|
||||
|
||||
it('returns grade word for each OEIF grade', () => {
|
||||
expect(buildOfstedListBadge({ ofsted_grade: 2, ofsted_date: '2022-05-01' }).label).toBe('Good · 2022');
|
||||
expect(buildOfstedListBadge({ ofsted_grade: 3, ofsted_date: '2021-01-01' }).label).toBe('Req. Improvement · 2021');
|
||||
expect(buildOfstedListBadge({ ofsted_grade: 4, ofsted_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('returns grade word without year when date is missing', () => {
|
||||
const badge = buildOfstedListBadge({ ofsted_grade: 2, ofsted_date: null });
|
||||
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');
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
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');
|
||||
|
||||
@@ -100,9 +100,8 @@ export function summariseReportCard(ofsted: OfstedInspection): ReportCardSummary
|
||||
|
||||
export type OfstedDisplay =
|
||||
| { kind: 'none' }
|
||||
| { kind: 'graded'; grade: number; gradeLabel: string; carriedForward: false }
|
||||
| { kind: 'carried_forward'; grade: number; gradeLabel: string; carriedForward: true }
|
||||
| { kind: 'transitional' }
|
||||
| { kind: 'graded' | 'confirmed'; grade: number; gradeLabel: string; gradeDate: string | null }
|
||||
| { kind: 'no_overall_grade' }
|
||||
| { kind: 'report_card'; summary: ReportCardSummary };
|
||||
|
||||
export function ofstedDisplay(
|
||||
@@ -116,19 +115,19 @@ export function ofstedDisplay(
|
||||
return { kind: 'report_card', summary: summariseReportCard(ofsted) };
|
||||
}
|
||||
|
||||
const grade = ofsted.overall_effectiveness;
|
||||
const gradeLabel = grade != null ? OFSTED_LEGACY_GRADES[grade] : undefined;
|
||||
if (grade == null || gradeLabel === undefined) {
|
||||
if (ofsted.inspection_date) {
|
||||
return { kind: 'transitional' };
|
||||
}
|
||||
return { kind: 'none' };
|
||||
// The grade still in force (backend: fact_ofsted_latest), never one carried
|
||||
// past a later inspection that gave none.
|
||||
const current = ofsted.current_grade;
|
||||
const gradeLabel = current ? OFSTED_LEGACY_GRADES[current.grade] : undefined;
|
||||
if (current && gradeLabel !== undefined) {
|
||||
return {
|
||||
kind: current.basis === 'confirmed' ? 'confirmed' : 'graded',
|
||||
grade: current.grade,
|
||||
gradeLabel,
|
||||
gradeDate: current.date,
|
||||
};
|
||||
}
|
||||
|
||||
if (ofsted.grade_source === 'ungraded_carried_forward') {
|
||||
return { kind: 'carried_forward', grade, gradeLabel, carriedForward: true };
|
||||
}
|
||||
return { kind: 'graded', grade, gradeLabel, carriedForward: false };
|
||||
return ofsted.latest_visit ? { kind: 'no_overall_grade' } : { kind: 'none' };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* The sentences the school page and the compare page print about where an
|
||||
* Ofsted grade came from and what the latest visit was. One wording, two pages.
|
||||
* Rule: docs/superpowers/specs/2026-10-05-ofsted-current-status-design.md
|
||||
*/
|
||||
|
||||
import type { OfstedCurrentGrade, OfstedInspection, OfstedLatestVisit } from './types';
|
||||
|
||||
export function formatOfstedDate(iso: string | null | undefined): string {
|
||||
if (!iso) return '';
|
||||
const d = new Date(iso);
|
||||
if (Number.isNaN(d.getTime())) return '';
|
||||
return d.toLocaleDateString('en-GB', { day: 'numeric', month: 'long', year: 'numeric' });
|
||||
}
|
||||
|
||||
const VISIT_KIND: Record<OfstedLatestVisit['kind'], string> = {
|
||||
report_card: 'Report card inspection',
|
||||
graded: 'Graded inspection',
|
||||
ungraded: 'Ungraded inspection',
|
||||
};
|
||||
|
||||
/** "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);
|
||||
const what = current.basis === 'confirmed' ? 'Confirmed at an ungraded inspection' : 'Graded inspection';
|
||||
return when ? `${what}, ${when}` : what;
|
||||
}
|
||||
|
||||
/** "Ungraded inspection, 13 November 2024: Standards maintained". */
|
||||
export function latestVisitLine(visit: OfstedLatestVisit): string {
|
||||
const head = `${VISIT_KIND[visit.kind]}, ${formatOfstedDate(visit.date)}`;
|
||||
return visit.outcome ? `${head}: ${visit.outcome}` : head;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the latest visit needs its own line: when it is not where the grade
|
||||
* came from, or when there is no grade but an ungraded outcome to report. A
|
||||
* graded visit without a grade is already dated by the section title.
|
||||
*/
|
||||
export function showLatestVisitLine(
|
||||
ofsted: Pick<OfstedInspection, 'current_grade' | 'latest_visit'>,
|
||||
): boolean {
|
||||
const visit = ofsted.latest_visit;
|
||||
if (!visit || visit.kind === 'report_card') return false;
|
||||
const current = ofsted.current_grade;
|
||||
if (current) return current.date !== visit.date;
|
||||
return visit.kind === 'ungraded';
|
||||
}
|
||||
+23
-3
@@ -76,10 +76,14 @@ export interface School {
|
||||
parliamentary_constituency?: string | null;
|
||||
|
||||
// Ofsted (for list view — summary only)
|
||||
/** The overall grade still in force; null when the latest inspection gave none. */
|
||||
ofsted_grade?: 1 | 2 | 3 | 4 | null;
|
||||
/** Date the grade was awarded or confirmed (null without a grade). */
|
||||
ofsted_grade_date?: string | null;
|
||||
/** Report-card inspection date (Nov 2025+); non-null identifies a report
|
||||
* card in the list/map, where the full report_card object isn't available. */
|
||||
ofsted_rc_date?: string | null;
|
||||
/** The school's latest inspection of any kind. */
|
||||
ofsted_date?: string | null;
|
||||
ofsted_framework?: string | null;
|
||||
}
|
||||
@@ -95,6 +99,7 @@ export interface OfstedInspection {
|
||||
rc_inspection_date?: string | null;
|
||||
inspection_type: string | null;
|
||||
// OEIF fields (old framework, pre-Nov 2025)
|
||||
/** The graded inspection's own overall grade; never carried forward. */
|
||||
overall_effectiveness: 1 | 2 | 3 | 4 | null;
|
||||
quality_of_education: number | null;
|
||||
behaviour_attitudes: number | null;
|
||||
@@ -115,9 +120,12 @@ export interface OfstedInspection {
|
||||
rc_leadership_governance: number | null;
|
||||
rc_early_years: number | null;
|
||||
rc_sixth_form: number | null;
|
||||
/** Where the effective overall grade came from: a graded (Section 5)
|
||||
* inspection, or carried forward from an ungraded (Section 8) outcome. */
|
||||
grade_source?: 'graded' | 'ungraded_carried_forward' | null;
|
||||
/** The overall grade still in force, dated by the inspection that awarded
|
||||
* ("graded") or confirmed ("confirmed", an ungraded visit) it. Null when the
|
||||
* latest inspection gave no overall grade, or for a report card. */
|
||||
current_grade?: OfstedCurrentGrade | null;
|
||||
/** The school's most recent inspection of any kind. */
|
||||
latest_visit?: OfstedLatestVisit | null;
|
||||
/** Renewed-framework (Nov 2025) area judgements, coded + labelled by the
|
||||
* backend from the live-sampled Ofsted vocabulary. Empty when the school
|
||||
* has no report-card inspection. Safeguarding is never included here. */
|
||||
@@ -127,6 +135,18 @@ export interface OfstedInspection {
|
||||
report_url?: string | null;
|
||||
}
|
||||
|
||||
export interface OfstedCurrentGrade {
|
||||
grade: 1 | 2 | 3 | 4;
|
||||
date: string | null;
|
||||
basis: 'graded' | 'confirmed';
|
||||
}
|
||||
|
||||
export interface OfstedLatestVisit {
|
||||
date: string;
|
||||
kind: 'report_card' | 'graded' | 'ungraded';
|
||||
outcome: string | null;
|
||||
}
|
||||
|
||||
export interface ReportCardEntry {
|
||||
code: number;
|
||||
label: string;
|
||||
|
||||
+15
-13
@@ -837,15 +837,18 @@ export interface OfstedListBadge {
|
||||
* Checked FIRST so it wins over any carried-forward legacy grade — the
|
||||
* list has no full report_card object, and ofsted_framework is the raw
|
||||
* event grouping ("Schools - S5"), never "ReportCard".
|
||||
* - OEIF school (ofsted_grade set): grade word + year, colour-keyed
|
||||
* - Inspected without an overall grade (OEIF post-Sept-2024, where Ofsted no
|
||||
* longer issues an overall judgement): "Inspected · YYYY" — mirrors the
|
||||
* detail page's hero chip so a school never reads as both inspected and
|
||||
* - Current grade (ofsted_grade set): grade word + the year it was awarded
|
||||
* or confirmed (ofsted_grade_date), colour-keyed. Never the year of a later
|
||||
* visit: that paired old grades with new inspections (audit C1).
|
||||
* - Inspected with no grade in force (every inspection Sept 2024 – Nov 2025,
|
||||
* 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
|
||||
*/
|
||||
export function buildOfstedListBadge(school: {
|
||||
ofsted_grade?: 1 | 2 | 3 | 4 | null;
|
||||
ofsted_grade_date?: string | null;
|
||||
ofsted_date?: string | null;
|
||||
ofsted_framework?: string | null;
|
||||
ofsted_rc_date?: string | null;
|
||||
@@ -858,10 +861,7 @@ export function buildOfstedListBadge(school: {
|
||||
return { label: `Report Card · ${rcYear}`, cssClass: 'ofstedRc' };
|
||||
}
|
||||
|
||||
const year = school.ofsted_date
|
||||
? new Date(school.ofsted_date).getFullYear()
|
||||
: null;
|
||||
const yearStr = year ? ` · ${year}` : '';
|
||||
const yearOf = (iso?: string | null) => (iso ? new Date(iso).getFullYear() : null);
|
||||
|
||||
if (school.ofsted_grade) {
|
||||
const labels: Record<number, string> = {
|
||||
@@ -870,17 +870,19 @@ export function buildOfstedListBadge(school: {
|
||||
3: 'Req. Improvement',
|
||||
4: 'Inadequate',
|
||||
};
|
||||
const gradeYear = yearOf(school.ofsted_grade_date);
|
||||
return {
|
||||
label: `${labels[school.ofsted_grade]}${yearStr}`,
|
||||
label: `${labels[school.ofsted_grade]}${gradeYear ? ` · ${gradeYear}` : ''}`,
|
||||
cssClass: `ofsted${school.ofsted_grade}`,
|
||||
};
|
||||
}
|
||||
|
||||
// An inspection is on record (date or framework present) but carries no
|
||||
// overall grade — a post-Sept-2024 OEIF inspection. Distinct from a school
|
||||
// that has genuinely never been inspected.
|
||||
// An inspection is on record but no overall grade is in force: every
|
||||
// inspection from Sept 2024 to Nov 2025, or an ungraded visit whose outcome
|
||||
// names no grade. Dated by the latest visit.
|
||||
if (school.ofsted_date != null || school.ofsted_framework != null) {
|
||||
return { label: `Inspected${yearStr}`, cssClass: 'ofstedInspected' };
|
||||
const visitYear = yearOf(school.ofsted_date);
|
||||
return { label: `Inspected${visitYear ? ` · ${visitYear}` : ''}`, cssClass: 'ofstedInspected' };
|
||||
}
|
||||
|
||||
return { label: 'Not yet inspected', cssClass: 'ofstedPending' };
|
||||
|
||||
Reference in new issue
Block a user