The location tables carried one column: a percentage. A parent shortlisting from a town page asks a different question first — does it take my child's age, is it a faith school, does it have a nursery — and the page could not answer any of it.
The columns
Primary
Secondary
Ages
✓
✓
Religious character
✓
✓
Nursery
✓
—
Constituency
✓
✓
Nursery is a primary question, so it appears in the primary table only. An all-through school renders in both groups and its nursery shows under primary alone — under "Secondary schools" the column would be a fact about a different intake.
The measure moves to the second column
Six columns overflow a phone, and .tableWrap already turns that into a horizontal swipe. With the measure last, the one number the page exists for is the one scrolled off the screen. It now sits immediately after the school name, which costs nothing and keeps it visible. An e2e test asserts it at 390px.
Cell rules are the school page's rules
So the two surfaces cannot disagree about the same school:
Does not apply, None and Not applicable all read as no religious character — GIAS spells the absence of a faith three ways, none of them English.
The en-dash age normalisation moves into formatAgeSpan, which formatAgeRange now delegates to. The column heading carries the word "Ages", so the cell says 4–11, not Ages 4–11.
A missing attribute is —. The measure keeps its "Not published", which stays meaningful precisely because four of them per row would drown the row they qualify.
Backend
nursery_provision and parliamentary_constituency were not in the place response; age_range and religious_denomination already rode in on SCHOOL_COLUMNS. Both new ones are optional GIAS mart columns that data_loader degrades to NULL, and the in rows.columns guard keeps a mart the pipeline has not rebuilt working — a test covers that path.
Also fixes a live bug on the same line.SCHOOL_COLUMNS already ends with latitude and longitude, and the endpoint concatenated them again, so rows[cols] selected each twice: pandas dropped one of every duplicated pair and emitted UserWarning: DataFrame columns are not unique, some columns will be omitted on every request. Ordered de-duplication removes both the warning and the silent drop.
Verification
Frontend 319 passed (was 305), tsc clean, next build green. Backend 160 passed, and the places suite no longer emits the uniqueness warning. 105 E2E collected (was 102).
The three new journeys assert column headings, not values, plus one age value — age_range predates the optional columns, so it is the one attribute safe to assert anywhere. Asserting nursery or constituency values would fail on an environment whose pipeline has not rebuilt the mart, which is a data reason, not a code one.
The location tables carried one column: a percentage. A parent shortlisting from a town page asks a different question first — does it take my child's age, is it a faith school, does it have a nursery — and the page could not answer any of it.
## The columns
| | Primary | Secondary |
|---|---|---|
| Ages | ✓ | ✓ |
| Religious character | ✓ | ✓ |
| Nursery | ✓ | — |
| Constituency | ✓ | ✓ |
Nursery is a primary question, so it appears in the primary table only. An all-through school renders in both groups and its nursery shows under primary alone — under "Secondary schools" the column would be a fact about a different intake.
## The measure moves to the second column
Six columns overflow a phone, and `.tableWrap` already turns that into a horizontal swipe. With the measure last, the one number the page exists for is the one scrolled off the screen. It now sits immediately after the school name, which costs nothing and keeps it visible. An e2e test asserts it at 390px.
## Cell rules are the school page's rules
So the two surfaces cannot disagree about the same school:
- `Does not apply`, `None` and `Not applicable` all read as no religious character — GIAS spells the absence of a faith three ways, none of them English.
- The en-dash age normalisation moves into `formatAgeSpan`, which `formatAgeRange` now delegates to. The column heading carries the word "Ages", so the cell says `4–11`, not `Ages 4–11`.
- A missing attribute is `—`. The measure keeps its "Not published", which stays meaningful precisely because four of them per row would drown the row they qualify.
## Backend
`nursery_provision` and `parliamentary_constituency` were not in the place response; `age_range` and `religious_denomination` already rode in on `SCHOOL_COLUMNS`. Both new ones are optional GIAS mart columns that `data_loader` degrades to NULL, and the `in rows.columns` guard keeps a mart the pipeline has not rebuilt working — a test covers that path.
**Also fixes a live bug on the same line.** `SCHOOL_COLUMNS` already ends with `latitude` and `longitude`, and the endpoint concatenated them again, so `rows[cols]` selected each twice: pandas dropped one of every duplicated pair and emitted `UserWarning: DataFrame columns are not unique, some columns will be omitted` on every request. Ordered de-duplication removes both the warning and the silent drop.
## Verification
Frontend **319 passed** (was 305), `tsc` clean, `next build` green. Backend **160 passed**, and the places suite no longer emits the uniqueness warning. **105 E2E collected** (was 102).
The three new journeys assert column *headings*, not values, plus one age value — `age_range` predates the optional columns, so it is the one attribute safe to assert anywhere. Asserting nursery or constituency values would fail on an environment whose pipeline has not rebuilt the mart, which is a data reason, not a code one.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01FuPUioHpxtaiDNagQvjxyM
The location tables carried one column: a percentage. A parent
shortlisting from a town page is asking a different question first —
does it take my child's age, is it a faith school, does it have a
nursery — and the page could not answer any of it.
Primary tables gain Ages, Religious character, Nursery and
Constituency; secondary tables the same minus Nursery, which is a
question about a different intake. An all-through school renders in
both groups, so its nursery shows under primary alone.
The measure moves to the second column rather than the last. Six
columns overflow a phone and .tableWrap turns that into a horizontal
swipe; with the measure last, the one number the page exists for is
the one scrolled off the screen.
Cell rules are the ones the school page already uses, so the two
surfaces cannot disagree about the same school: "Does not apply",
"None" and "Not applicable" all read as no religious character, and
the en-dash age normalisation moves into formatAgeSpan, which
formatAgeRange now delegates to.
Backend: nursery_provision and parliamentary_constituency were not in
the place response. Both are optional GIAS mart columns that
data_loader degrades to NULL, and the `in rows.columns` guard keeps a
mart the pipeline has not rebuilt working.
Also fixes a live bug on the same line: SCHOOL_COLUMNS already ends
with latitude and longitude, and the endpoint concatenated them again,
so pandas dropped one of every duplicated pair and warned "columns are
not unique" on each request. Ordered de-duplication removes the
warning and the silent drop.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FuPUioHpxtaiDNagQvjxyM
Adds four attribute columns (age range, religious character, nursery provision, parliamentary constituency) to the place-page school tables, fixing a pre-existing pandas column-duplication bug in the /api/places/{kind}/{slug} endpoint along the way. The change is well-tested across backend, component, and e2e suites, and the graceful-degradation behavior for marts missing the two optional GIAS columns is correctly implemented and verified.
✅ No issues found.
## 🤖 AI Code Review (Claude Code)
Adds four attribute columns (age range, religious character, nursery provision, parliamentary constituency) to the place-page school tables, fixing a pre-existing pandas column-duplication bug in the /api/places/{kind}/{slug} endpoint along the way. The change is well-tested across backend, component, and e2e suites, and the graceful-degradation behavior for marts missing the two optional GIAS columns is correctly implemented and verified.
✅ No issues found.
tudor
merged commit 4fdeb70a93 into main2026-08-27 07:59:11 +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.
The location tables carried one column: a percentage. A parent shortlisting from a town page asks a different question first — does it take my child's age, is it a faith school, does it have a nursery — and the page could not answer any of it.
The columns
Nursery is a primary question, so it appears in the primary table only. An all-through school renders in both groups and its nursery shows under primary alone — under "Secondary schools" the column would be a fact about a different intake.
The measure moves to the second column
Six columns overflow a phone, and
.tableWrapalready turns that into a horizontal swipe. With the measure last, the one number the page exists for is the one scrolled off the screen. It now sits immediately after the school name, which costs nothing and keeps it visible. An e2e test asserts it at 390px.Cell rules are the school page's rules
So the two surfaces cannot disagree about the same school:
Does not apply,NoneandNot applicableall read as no religious character — GIAS spells the absence of a faith three ways, none of them English.formatAgeSpan, whichformatAgeRangenow delegates to. The column heading carries the word "Ages", so the cell says4–11, notAges 4–11.—. The measure keeps its "Not published", which stays meaningful precisely because four of them per row would drown the row they qualify.Backend
nursery_provisionandparliamentary_constituencywere not in the place response;age_rangeandreligious_denominationalready rode in onSCHOOL_COLUMNS. Both new ones are optional GIAS mart columns thatdata_loaderdegrades to NULL, and thein rows.columnsguard keeps a mart the pipeline has not rebuilt working — a test covers that path.Also fixes a live bug on the same line.
SCHOOL_COLUMNSalready ends withlatitudeandlongitude, and the endpoint concatenated them again, sorows[cols]selected each twice: pandas dropped one of every duplicated pair and emittedUserWarning: DataFrame columns are not unique, some columns will be omittedon every request. Ordered de-duplication removes both the warning and the silent drop.Verification
Frontend 319 passed (was 305),
tscclean,next buildgreen. Backend 160 passed, and the places suite no longer emits the uniqueness warning. 105 E2E collected (was 102).The three new journeys assert column headings, not values, plus one age value —
age_rangepredates the optional columns, so it is the one attribute safe to assert anywhere. Asserting nursery or constituency values would fail on an environment whose pipeline has not rebuilt the mart, which is a data reason, not a code one.🤖 Generated with Claude Code
https://claude.ai/code/session_01FuPUioHpxtaiDNagQvjxyM
🤖 AI Code Review (Claude Code)
Adds four attribute columns (age range, religious character, nursery provision, parliamentary constituency) to the place-page school tables, fixing a pre-existing pandas column-duplication bug in the /api/places/{kind}/{slug} endpoint along the way. The change is well-tested across backend, component, and e2e suites, and the graceful-degradation behavior for marts missing the two optional GIAS columns is correctly implemented and verified.
✅ No issues found.