Fixes the staging E2E failure after #177 merged (a selective school is flagged Selective, on its page and in search), and a live Admissions-section bug the fix would otherwise have made worse.
Root cause
The header's Selective flag reads school_info.admissions_policy. The detail endpoint (/api/schools/{urn}) never sent that field: on staging both Tiffin and The Grammar School at Leeds return school_info without it. The list API does send it, so the search row said Selective and the school page couldn't. My Jest fixtures set the field directly, which hid the gap. The E2E journey caught it.
Why it is more than one line
Sending the field switches on two older copies of the tag logic #176 fixed in the search rows:
Admissions section (SecondaryAdmissionsSection): includes('selective'), so every non-selective secondary would have started reading "entry to this school is by selective examination". It also counted "None" as a faith, and that half is live now: Burntwood's Admissions section reads "Faith priority: this school has a faith-based admissions priority (None)."
Cut-off note (describeCutoffAbsence): the same substring test. The distance feature is off on staging, but it would have misread every comprehensive once the feature is on.
All three callers now share isSelective() (exact match) and hasReligiousCharacter(). The latter now also treats "Not applicable" as no faith, matching the rule the place table (PlaceView's NO_FAITH) already documents. No includes('selective') is left in the frontend.
Tests
Backend: test_school_page_flag_fields.py checks the detail payload carries every field the header's flags read, so this class of gap fails CI. 305 pass.
Jest: Admissions-section notes (selective, non-selective, None, Does not apply, Church of England); describeCutoffAbsence with "Non-selective"; "Not applicable" as no faith. 72 suites, 629 pass; tsc clean.
E2E: a non-selective, no-faith secondary's Admissions section makes neither claim. It fails against staging today on "Faith priority:". The failing Selective journey from #177 should pass once this deploys.
Fixes the staging E2E failure after #177 merged (`a selective school is flagged Selective, on its page and in search`), and a live Admissions-section bug the fix would otherwise have made worse.
## Root cause
The header's Selective flag reads `school_info.admissions_policy`. The detail endpoint (`/api/schools/{urn}`) **never sent that field**: on staging both Tiffin and The Grammar School at Leeds return `school_info` without it. The list API does send it, so the search row said Selective and the school page couldn't. My Jest fixtures set the field directly, which hid the gap. The E2E journey caught it.
## Why it is more than one line
Sending the field switches on two older copies of the tag logic #176 fixed in the search rows:
- **Admissions section** (`SecondaryAdmissionsSection`): `includes('selective')`, so every non-selective secondary would have started reading *"entry to this school is by selective examination"*. It also counted "None" as a faith, and that half is **live now**: Burntwood's Admissions section reads *"Faith priority: this school has a faith-based admissions priority (None)."*
- **Cut-off note** (`describeCutoffAbsence`): the same substring test. The distance feature is off on staging, but it would have misread every comprehensive once the feature is on.
All three callers now share `isSelective()` (exact match) and `hasReligiousCharacter()`. The latter now also treats "Not applicable" as no faith, matching the rule the place table (`PlaceView`'s `NO_FAITH`) already documents. No `includes('selective')` is left in the frontend.
## Tests
- Backend: `test_school_page_flag_fields.py` checks the detail payload carries **every field the header's flags read**, so this class of gap fails CI. 305 pass.
- Jest: Admissions-section notes (selective, non-selective, None, Does not apply, Church of England); `describeCutoffAbsence` with "Non-selective"; "Not applicable" as no faith. 72 suites, 629 pass; `tsc` clean.
- E2E: a non-selective, no-faith secondary's Admissions section makes neither claim. It **fails against staging today** on "Faith priority:". The failing Selective journey from #177 should pass once this deploys.
- Merges cleanly with #178.
Leftover, not changed: `components/SchoolCard.tsx` has the same "None" check but nothing imports it.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
The header's Selective flag read school_info.admissions_policy, which the
detail endpoint never sent, so no school page could flag Selective while
its search row did (staging E2E: The Grammar School at Leeds). The detail
payload now carries it, and a contract test checks it carries every field
the header's flags read.
Sending it would have switched on two older copies of the tag logic #176
fixed in the rows. The Admissions section and the cut-off note both tested
includes('selective'), so every non-selective secondary would have read
"entry is by selective examination". The section also counted "None" as a
faith: Burntwood reads "a faith-based admissions priority (None)" today.
All of them now share isSelective() and hasReligiousCharacter(), which also
treats "Not applicable" as no faith, as the place table already does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Admissions section of a non-selective secondary with no religious
character must say neither "Selective:" nor "Faith priority:". Run against
staging before the fix, it fails on "Faith priority" (Burntwood's "(None)").
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds admissions_policy to the school detail API payload so the header's Selective flag works on school pages. It also replaces substring checks with an exact isSelective() helper, so 'Non-selective' is no longer read as selective. The helper also treats 'Not applicable' as no faith. Tests are added at the backend, unit and e2e levels. The change looks healthy.
🟡 Minor
e2e/tests/journeys.spec.ts: The new e2e test checks s.religious_denomination !== 'None' on search results. It assumes the search API returns that field and that the value is exactly 'None'. It also only checks the first 8 results. If the field is missing or has other casing, the test silently skips instead of failing, so it may never run.
nextjs-app/components/school/SecondaryAdmissionsSection.tsx: Before this change, the fallback only excluded 'Does not apply'. It now also hides the Faith priority note for 'Not applicable', 'None' and blank values. This is intended, but it is a behaviour change that is only covered by tests on the section component.
## 🤖 AI Code Review (Claude Code)
Adds admissions_policy to the school detail API payload so the header's Selective flag works on school pages. It also replaces substring checks with an exact isSelective() helper, so 'Non-selective' is no longer read as selective. The helper also treats 'Not applicable' as no faith. Tests are added at the backend, unit and e2e levels. The change looks healthy.
### 🟡 Minor
- **e2e/tests/journeys.spec.ts**: The new e2e test checks `s.religious_denomination !== 'None'` on search results. It assumes the search API returns that field and that the value is exactly 'None'. It also only checks the first 8 results. If the field is missing or has other casing, the test silently skips instead of failing, so it may never run.
- **nextjs-app/components/school/SecondaryAdmissionsSection.tsx**: Before this change, the fallback only excluded 'Does not apply'. It now also hides the Faith priority note for 'Not applicable', 'None' and blank values. This is intended, but it is a behaviour change that is only covered by tests on the section component.
tudor
merged commit 423b27140c into main2026-10-02 22:56:36 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Fixes the staging E2E failure after #177 merged (
a selective school is flagged Selective, on its page and in search), and a live Admissions-section bug the fix would otherwise have made worse.Root cause
The header's Selective flag reads
school_info.admissions_policy. The detail endpoint (/api/schools/{urn}) never sent that field: on staging both Tiffin and The Grammar School at Leeds returnschool_infowithout it. The list API does send it, so the search row said Selective and the school page couldn't. My Jest fixtures set the field directly, which hid the gap. The E2E journey caught it.Why it is more than one line
Sending the field switches on two older copies of the tag logic #176 fixed in the search rows:
SecondaryAdmissionsSection):includes('selective'), so every non-selective secondary would have started reading "entry to this school is by selective examination". It also counted "None" as a faith, and that half is live now: Burntwood's Admissions section reads "Faith priority: this school has a faith-based admissions priority (None)."describeCutoffAbsence): the same substring test. The distance feature is off on staging, but it would have misread every comprehensive once the feature is on.All three callers now share
isSelective()(exact match) andhasReligiousCharacter(). The latter now also treats "Not applicable" as no faith, matching the rule the place table (PlaceView'sNO_FAITH) already documents. Noincludes('selective')is left in the frontend.Tests
test_school_page_flag_fields.pychecks the detail payload carries every field the header's flags read, so this class of gap fails CI. 305 pass.describeCutoffAbsencewith "Non-selective"; "Not applicable" as no faith. 72 suites, 629 pass;tscclean.Leftover, not changed:
components/SchoolCard.tsxhas the same "None" check but nothing imports it.🤖 Generated with Claude Code
🤖 AI Code Review (Claude Code)
Adds admissions_policy to the school detail API payload so the header's Selective flag works on school pages. It also replaces substring checks with an exact isSelective() helper, so 'Non-selective' is no longer read as selective. The helper also treats 'Not applicable' as no faith. Tests are added at the backend, unit and e2e levels. The change looks healthy.
🟡 Minor
s.religious_denomination !== 'None'on search results. It assumes the search API returns that field and that the value is exactly 'None'. It also only checks the first 8 results. If the field is missing or has other casing, the test silently skips instead of failing, so it may never run.