From c3ba7aae0df58d3139336cd64541628b8f37551d Mon Sep 17 00:00:00 2001 From: Tudor Date: Sun, 23 Aug 2026 10:56:33 +0100 Subject: [PATCH] feat(flags): ship last-distance-offered dark behind a flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One gate, at the source. The frontend needs no change: DistanceSection already returns null when distance_m is missing, and the admissions block already conditions on (admissions || admissionDistance). Only 57 local authorities publish cut-offs, so the off-path is the commonest path on the site and is well covered already. Absent, not null. /api/schools/ is public and unauthenticated, so a field left in the payload is a published field — the reasoning already recorded in c9a1892 when history was withheld. The two are also different claims: null says this school has no cut-off, absent says cut-offs are not being published at all. The frontend type now says so. The feature is on main and live on staging and has never reached production, which is what makes it the right first consumer: the flag lets the code promote without the feature appearing. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj --- backend/app.py | 8 ++++- backend/tests/test_flags.py | 68 +++++++++++++++++++++++++++++++++++++ nextjs-app/lib/types.ts | 7 +++- 3 files changed, 81 insertions(+), 2 deletions(-) diff --git a/backend/app.py b/backend/app.py index 4828a8b..d352de5 100644 --- a/backend/app.py +++ b/backend/app.py @@ -808,7 +808,13 @@ async def get_school_details(request: Request, urn: int): "census": supplementary.get("census"), "admissions": supplementary.get("admissions"), "admissions_history": supplementary.get("admissions_history") or [], - "admission_distance": supplementary.get("admission_distance"), + # Behind a flag, and withheld at the source rather than rendered-but- + # hidden: this endpoint is public and unauthenticated, so a field left + # in the payload is a published field. The key is absent, not null — + # null would state that this school has no cut-off, which is a + # different claim from "we are not publishing cut-offs". + **({"admission_distance": supplementary.get("admission_distance")} + if flags.is_enabled("admission_distance") else {}), "sen_detail": supplementary.get("sen_detail"), "phonics": supplementary.get("phonics"), "deprivation": supplementary.get("deprivation"), diff --git a/backend/tests/test_flags.py b/backend/tests/test_flags.py index 114a219..24fdb53 100644 --- a/backend/tests/test_flags.py +++ b/backend/tests/test_flags.py @@ -93,3 +93,71 @@ def test_the_flags_endpoint_answers_false_when_unleash_is_unreachable(monkeypatc res = _client().get("/api/flags") assert res.status_code == 200 assert all(v is False for v in res.json().values()) + + +def _school_payload(monkeypatch, *, flag_on: bool): + """Fetch one school's payload with the distance flag forced on or off. + + The DataFrame shape is copied from test_school_details.py rather than + minimised: the endpoint reads a wide set of GIAS columns, and a trimmed + frame fails for reasons that have nothing to do with flags. + """ + import numpy as np + import pandas as pd + from fastapi.testclient import TestClient + from backend import app as app_module + + df = pd.DataFrame([{ + "urn": 150275, + "school_name": "West London Performing Arts Academy", + "phase": "Secondary", + "school_type": "Special post 16 institution", + "trust_name": None, + "religious_denomination": "Does not apply", + "gender": None, + "age_range": "16-25", + "admissions_policy": None, + "capacity": np.nan, + "gias_total_pupils": np.nan, + "headteacher_name": None, + "website": None, + "ofsted_grade": np.nan, + "local_authority": "Ealing", + "address": "268 Northfield Avenue, London, W5 4UB", + "postcode": "W5 4UB", + "latitude": 51.4986, + "longitude": -0.3148, + "year": np.nan, + "total_pupils": np.nan, + "eligible_pupils": np.nan, + "rwm_expected_pct": np.nan, + }]) + + monkeypatch.setattr(app_module, "load_school_data", lambda: df) + # Two arguments: get_supplementary_data(db, urn). See backend/app.py. + monkeypatch.setattr( + app_module, "get_supplementary_data", + lambda db, urn: {"admission_distance": {"distance_m": 772.49, + "year": 2024}}) + monkeypatch.setattr(flags, "is_enabled", lambda name: flag_on) + + client = TestClient(app_module.app, raise_server_exceptions=False) + res = client.get("/api/schools/150275") + assert res.status_code == 200, res.text + return res.json() + + +def test_the_distance_field_is_absent_when_the_flag_is_off(monkeypatch): + """Absent, not null, and withheld at the source. + + /api/schools/ is public and unauthenticated. Leaving a withheld field in + the payload while declining to render it hands the record to anyone who + opens the network tab — the reasoning already recorded in c9a1892. + """ + body = _school_payload(monkeypatch, flag_on=False) + assert "admission_distance" not in body + + +def test_the_distance_field_is_present_when_the_flag_is_on(monkeypatch): + body = _school_payload(monkeypatch, flag_on=True) + assert body["admission_distance"]["distance_m"] == 772.49 diff --git a/nextjs-app/lib/types.ts b/nextjs-app/lib/types.ts index 7c1f751..601fd1f 100644 --- a/nextjs-app/lib/types.ts +++ b/nextjs-app/lib/types.ts @@ -361,7 +361,12 @@ export interface SchoolDetailsResponse { * held back as a paid feature and are not part of this public payload — see * data_loader._admission_distance. */ - admission_distance: SchoolAdmissionDistance | null; + /** + * Absent — not null — when the admission_distance flag is off. Null means + * "this school has no published cut-off"; absent means "cut-offs are not + * being published at all". They are different claims and the type says so. + */ + admission_distance?: SchoolAdmissionDistance | null; deprivation: SchoolDeprivation | null; finance: SchoolFinance | null; }