refactor: rename similar → nearby, so the code says what the section does
The section ranks on distance and is headed "Other schools nearby", but every identifier still called it "similar" — the exact drift that leaves a later reader trusting a name over the behaviour. Mechanical: files, the module, the payload key, the type, the components, the prop. No behaviour change; the suites are unchanged in count and still green. Free to do now because #150 has not merged, so the payload key rename needs no lockstep deploy. Uses of "similar" that are ordinary English — progress measures compared to similar pupils, and unrelated comments — are untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5c0ccc693d
commit
cd6a45bf7d
15 files changed
+102
-102
No files matched your search
+6
-6
@@ -41,7 +41,7 @@ from .data_loader import get_data_info as get_db_info
|
|||||||
from . import flags
|
from . import flags
|
||||||
from .places import build_place_index, build_place_registry, places_for_urn
|
from .places import build_place_index, build_place_registry, places_for_urn
|
||||||
from .schemas import METRIC_DEFINITIONS, PHASE_GROUPS, RANKING_COLUMNS, SCHOOL_COLUMNS
|
from .schemas import METRIC_DEFINITIONS, PHASE_GROUPS, RANKING_COLUMNS, SCHOOL_COLUMNS
|
||||||
from .similar_schools import select_similar
|
from .nearby_schools import select_nearby
|
||||||
from .utils import clean_for_json, convert_to_native
|
from .utils import clean_for_json, convert_to_native
|
||||||
|
|
||||||
# Values to exclude from filter dropdowns (empty strings, non-applicable labels)
|
# Values to exclude from filter dropdowns (empty strings, non-applicable labels)
|
||||||
@@ -266,10 +266,10 @@ def _places_payload(urn: int) -> list[dict]:
|
|||||||
return payload
|
return payload
|
||||||
|
|
||||||
|
|
||||||
def _similar_schools_payload(urn: int) -> list[dict]:
|
def _nearby_schools_payload(urn: int) -> list[dict]:
|
||||||
"""The nearest eligible schools this page may offer, closest first.
|
"""The nearest eligible schools this page may offer, closest first.
|
||||||
|
|
||||||
Phase and reach are read from the school's own row inside select_similar,
|
Phase and reach are read from the school's own row inside select_nearby,
|
||||||
so nothing here can hand it a phase that disagrees with the data.
|
so nothing here can hand it a phase that disagrees with the data.
|
||||||
|
|
||||||
Wrapped: a failure in selection must never 500 a page that is otherwise
|
Wrapped: a failure in selection must never 500 a page that is otherwise
|
||||||
@@ -277,12 +277,12 @@ def _similar_schools_payload(urn: int) -> list[dict]:
|
|||||||
section simply does not render.
|
section simply does not render.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
return select_similar(load_latest_school_data(), int(urn))
|
return select_nearby(load_latest_school_data(), int(urn))
|
||||||
except Exception:
|
except Exception:
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logging.getLogger(__name__).exception(
|
logging.getLogger(__name__).exception(
|
||||||
"Similar schools selection failed for urn=%s", urn
|
"Nearby schools selection failed for urn=%s", urn
|
||||||
)
|
)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@@ -1000,7 +1000,7 @@ async def get_school_details(request: Request, urn: int):
|
|||||||
# The nearest eligible schools, closest first. Always present on a
|
# The nearest eligible schools, closest first. Always present on a
|
||||||
# build with this code; the frontend treats absent and empty
|
# build with this code; the frontend treats absent and empty
|
||||||
# identically, which is what lets the two images deploy independently.
|
# identically, which is what lets the two images deploy independently.
|
||||||
"similar_schools": _similar_schools_payload(urn),
|
"nearby_schools": _nearby_schools_payload(urn),
|
||||||
"yearly_data": clean_for_json(school_data),
|
"yearly_data": clean_for_json(school_data),
|
||||||
# Supplementary data (null if not yet populated by Kestra)
|
# Supplementary data (null if not yet populated by Kestra)
|
||||||
"ofsted": supplementary.get("ofsted"),
|
"ofsted": supplementary.get("ofsted"),
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ def _shared(subject: pd.Series, candidate: pd.Series, is_secondary: bool) -> lis
|
|||||||
return shared
|
return shared
|
||||||
|
|
||||||
|
|
||||||
def select_similar(frame: pd.DataFrame, urn: int) -> list[dict]:
|
def select_nearby(frame: pd.DataFrame, urn: int) -> list[dict]:
|
||||||
"""The nearest eligible schools, closest first — at most MAX_SCHOOLS, and
|
"""The nearest eligible schools, closest first — at most MAX_SCHOOLS, and
|
||||||
none at all below MINIMUM.
|
none at all below MINIMUM.
|
||||||
|
|
||||||
+1
-1
@@ -536,7 +536,7 @@ RANKING_COLUMNS = [
|
|||||||
# include. All-through schools appear in both primary and secondary results,
|
# include. All-through schools appear in both primary and secondary results,
|
||||||
# which is why this is a set per phase rather than a single string comparison.
|
# which is why this is a set per phase rather than a single string comparison.
|
||||||
#
|
#
|
||||||
# Lives here rather than in app.py because similar_schools.py needs it too, and
|
# Lives here rather than in app.py because nearby_schools.py needs it too, and
|
||||||
# importing app from there would be a cycle.
|
# importing app from there would be a cycle.
|
||||||
PHASE_GROUPS: dict[str, set[str]] = {
|
PHASE_GROUPS: dict[str, set[str]] = {
|
||||||
"primary": {"primary", "middle deemed primary", "all-through"},
|
"primary": {"primary", "middle deemed primary", "all-through"},
|
||||||
|
|||||||
@@ -15,10 +15,10 @@ card and never reorders the row.
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from backend.similar_schools import (
|
from backend.nearby_schools import (
|
||||||
is_secondary_phase,
|
is_secondary_phase,
|
||||||
radius_miles,
|
radius_miles,
|
||||||
select_similar,
|
select_nearby,
|
||||||
)
|
)
|
||||||
|
|
||||||
BASE_LAT, BASE_LON = 51.5000, -0.1000
|
BASE_LAT, BASE_LON = 51.5000, -0.1000
|
||||||
@@ -66,7 +66,7 @@ def test_returns_nearest_first():
|
|||||||
_row(100003, "Near", latitude=_at(0.4)),
|
_row(100003, "Near", latitude=_at(0.4)),
|
||||||
_row(100004, "Far", latitude=_at(1.8)),
|
_row(100004, "Far", latitude=_at(1.8)),
|
||||||
)
|
)
|
||||||
result = select_similar(frame, 100001)
|
result = select_nearby(frame, 100001)
|
||||||
assert [s["urn"] for s in result] == [100003, 100002, 100004]
|
assert [s["urn"] for s in result] == [100003, 100002, 100004]
|
||||||
assert result[0]["distance_miles"] == 0.4
|
assert result[0]["distance_miles"] == 0.4
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ def test_a_faith_match_never_outranks_a_closer_school():
|
|||||||
_row(100004, "Sacred Heart RC", religious_denomination="Roman Catholic", latitude=_at(1.2)),
|
_row(100004, "Sacred Heart RC", religious_denomination="Roman Catholic", latitude=_at(1.2)),
|
||||||
_row(100005, "St Peter's RC", religious_denomination="Roman Catholic", latitude=_at(1.6)),
|
_row(100005, "St Peter's RC", religious_denomination="Roman Catholic", latitude=_at(1.6)),
|
||||||
)
|
)
|
||||||
result = select_similar(frame, 100001)
|
result = select_nearby(frame, 100001)
|
||||||
assert result[0]["urn"] == 100002, "the nearest school leads, whatever its intake"
|
assert result[0]["urn"] == 100002, "the nearest school leads, whatever its intake"
|
||||||
assert [s["distance_miles"] for s in result] == sorted(s["distance_miles"] for s in result)
|
assert [s["distance_miles"] for s in result] == sorted(s["distance_miles"] for s in result)
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ def test_the_nearest_eligible_school_is_always_shown():
|
|||||||
for n in range(6)
|
for n in range(6)
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
assert select_similar(frame, 100001)[0]["urn"] == 100002
|
assert select_nearby(frame, 100001)[0]["urn"] == 100002
|
||||||
|
|
||||||
|
|
||||||
def test_caps_at_six_taking_the_nearest():
|
def test_caps_at_six_taking_the_nearest():
|
||||||
@@ -106,7 +106,7 @@ def test_caps_at_six_taking_the_nearest():
|
|||||||
_row(100001, "Subject"),
|
_row(100001, "Subject"),
|
||||||
*[_row(100010 + n, f"Peer {n}", latitude=_at(0.1 * (n + 1))) for n in range(7)],
|
*[_row(100010 + n, f"Peer {n}", latitude=_at(0.1 * (n + 1))) for n in range(7)],
|
||||||
)
|
)
|
||||||
result = select_similar(frame, 100001)
|
result = select_nearby(frame, 100001)
|
||||||
assert len(result) == 6
|
assert len(result) == 6
|
||||||
assert 100016 not in {s["urn"] for s in result}, "the seventh-nearest is the one dropped"
|
assert 100016 not in {s["urn"] for s in result}, "the seventh-nearest is the one dropped"
|
||||||
|
|
||||||
@@ -116,7 +116,7 @@ def test_fewer_than_two_matches_returns_empty():
|
|||||||
_row(100001, "Subject"),
|
_row(100001, "Subject"),
|
||||||
_row(100002, "Only neighbour", latitude=_at(0.5)),
|
_row(100002, "Only neighbour", latitude=_at(0.5)),
|
||||||
)
|
)
|
||||||
assert select_similar(frame, 100001) == []
|
assert select_nearby(frame, 100001) == []
|
||||||
|
|
||||||
|
|
||||||
def test_excludes_the_subject_school():
|
def test_excludes_the_subject_school():
|
||||||
@@ -125,7 +125,7 @@ def test_excludes_the_subject_school():
|
|||||||
_row(100002, "A", latitude=_at(0.5)),
|
_row(100002, "A", latitude=_at(0.5)),
|
||||||
_row(100003, "B", latitude=_at(0.6)),
|
_row(100003, "B", latitude=_at(0.6)),
|
||||||
)
|
)
|
||||||
assert 100001 not in {s["urn"] for s in select_similar(frame, 100001)}
|
assert 100001 not in {s["urn"] for s in select_nearby(frame, 100001)}
|
||||||
|
|
||||||
|
|
||||||
def test_a_school_is_never_listed_twice():
|
def test_a_school_is_never_listed_twice():
|
||||||
@@ -134,7 +134,7 @@ def test_a_school_is_never_listed_twice():
|
|||||||
_row(100002, "A", latitude=_at(0.5)),
|
_row(100002, "A", latitude=_at(0.5)),
|
||||||
_row(100003, "B", latitude=_at(0.6)),
|
_row(100003, "B", latitude=_at(0.6)),
|
||||||
)
|
)
|
||||||
result = select_similar(frame, 100001)
|
result = select_nearby(frame, 100001)
|
||||||
assert len(result) == len({s["urn"] for s in result})
|
assert len(result) == len({s["urn"] for s in result})
|
||||||
|
|
||||||
|
|
||||||
@@ -151,7 +151,7 @@ def test_primary_does_not_reach_past_two_miles():
|
|||||||
)
|
)
|
||||||
# One inside the cap is below the minimum, so nothing renders at all —
|
# One inside the cap is below the minimum, so nothing renders at all —
|
||||||
# a primary with nothing within two miles has no nearby schools.
|
# a primary with nothing within two miles has no nearby schools.
|
||||||
assert select_similar(frame, 100001) == []
|
assert select_nearby(frame, 100001) == []
|
||||||
|
|
||||||
|
|
||||||
def test_secondary_reaches_further_than_primary():
|
def test_secondary_reaches_further_than_primary():
|
||||||
@@ -160,7 +160,7 @@ def test_secondary_reaches_further_than_primary():
|
|||||||
_row(100002, "A", phase="Secondary", latitude=_at(3.0)),
|
_row(100002, "A", phase="Secondary", latitude=_at(3.0)),
|
||||||
_row(100003, "B", phase="Secondary", latitude=_at(5.5)),
|
_row(100003, "B", phase="Secondary", latitude=_at(5.5)),
|
||||||
)
|
)
|
||||||
assert {s["urn"] for s in select_similar(frame, 100001)} == {100002, 100003}
|
assert {s["urn"] for s in select_nearby(frame, 100001)} == {100002, 100003}
|
||||||
|
|
||||||
|
|
||||||
def test_the_cap_follows_the_phase():
|
def test_the_cap_follows_the_phase():
|
||||||
@@ -183,8 +183,8 @@ def test_selective_never_meets_non_selective():
|
|||||||
_row(100002, "Comp A", phase="Secondary", admissions_policy="Non-selective", latitude=_at(0.5)),
|
_row(100002, "Comp A", phase="Secondary", admissions_policy="Non-selective", latitude=_at(0.5)),
|
||||||
_row(100003, "Comp B", phase="Secondary", admissions_policy="Non-selective", latitude=_at(0.6)),
|
_row(100003, "Comp B", phase="Secondary", admissions_policy="Non-selective", latitude=_at(0.6)),
|
||||||
)
|
)
|
||||||
assert select_similar(frame, 100001) == []
|
assert select_nearby(frame, 100001) == []
|
||||||
assert 100001 not in {s["urn"] for s in select_similar(frame, 100002)}
|
assert 100001 not in {s["urn"] for s in select_nearby(frame, 100002)}
|
||||||
|
|
||||||
|
|
||||||
def test_special_schools_match_only_each_other():
|
def test_special_schools_match_only_each_other():
|
||||||
@@ -193,8 +193,8 @@ def test_special_schools_match_only_each_other():
|
|||||||
_row(100002, "Mainstream A", latitude=_at(0.5)),
|
_row(100002, "Mainstream A", latitude=_at(0.5)),
|
||||||
_row(100003, "Mainstream B", latitude=_at(0.6)),
|
_row(100003, "Mainstream B", latitude=_at(0.6)),
|
||||||
)
|
)
|
||||||
assert select_similar(frame, 100001) == []
|
assert select_nearby(frame, 100001) == []
|
||||||
assert select_similar(frame, 100002) == []
|
assert select_nearby(frame, 100002) == []
|
||||||
|
|
||||||
|
|
||||||
def test_boys_never_meets_girls():
|
def test_boys_never_meets_girls():
|
||||||
@@ -204,7 +204,7 @@ def test_boys_never_meets_girls():
|
|||||||
_row(100003, "Mixed School", gender="Mixed", latitude=_at(0.6)),
|
_row(100003, "Mixed School", gender="Mixed", latitude=_at(0.6)),
|
||||||
_row(100004, "Another Mixed", gender="Mixed", latitude=_at(0.7)),
|
_row(100004, "Another Mixed", gender="Mixed", latitude=_at(0.7)),
|
||||||
)
|
)
|
||||||
urns = {s["urn"] for s in select_similar(frame, 100001)}
|
urns = {s["urn"] for s in select_nearby(frame, 100001)}
|
||||||
assert 100002 not in urns
|
assert 100002 not in urns
|
||||||
assert urns == {100003, 100004}
|
assert urns == {100003, 100004}
|
||||||
|
|
||||||
@@ -217,7 +217,7 @@ def test_closed_schools_and_missing_coordinates_are_dropped():
|
|||||||
_row(100004, "Good A", latitude=_at(0.6)),
|
_row(100004, "Good A", latitude=_at(0.6)),
|
||||||
_row(100005, "Good B", latitude=_at(0.7)),
|
_row(100005, "Good B", latitude=_at(0.7)),
|
||||||
)
|
)
|
||||||
assert {s["urn"] for s in select_similar(frame, 100001)} == {100004, 100005}
|
assert {s["urn"] for s in select_nearby(frame, 100001)} == {100004, 100005}
|
||||||
|
|
||||||
|
|
||||||
def test_all_through_is_offered_on_both_phase_sides():
|
def test_all_through_is_offered_on_both_phase_sides():
|
||||||
@@ -226,14 +226,14 @@ def test_all_through_is_offered_on_both_phase_sides():
|
|||||||
_row(100002, "All through", phase="All-through", latitude=_at(0.5)),
|
_row(100002, "All through", phase="All-through", latitude=_at(0.5)),
|
||||||
_row(100003, "Primary peer", phase="Primary", latitude=_at(0.6)),
|
_row(100003, "Primary peer", phase="Primary", latitude=_at(0.6)),
|
||||||
)
|
)
|
||||||
assert 100002 in {s["urn"] for s in select_similar(frame, 100001)}
|
assert 100002 in {s["urn"] for s in select_nearby(frame, 100001)}
|
||||||
|
|
||||||
secondary = _frame(
|
secondary = _frame(
|
||||||
_row(100010, "Secondary subject", phase="Secondary"),
|
_row(100010, "Secondary subject", phase="Secondary"),
|
||||||
_row(100002, "All through", phase="All-through", latitude=_at(0.5)),
|
_row(100002, "All through", phase="All-through", latitude=_at(0.5)),
|
||||||
_row(100011, "Secondary peer", phase="Secondary", latitude=_at(0.6)),
|
_row(100011, "Secondary peer", phase="Secondary", latitude=_at(0.6)),
|
||||||
)
|
)
|
||||||
assert 100002 in {s["urn"] for s in select_similar(secondary, 100010)}
|
assert 100002 in {s["urn"] for s in select_nearby(secondary, 100010)}
|
||||||
|
|
||||||
|
|
||||||
def test_sixteen_plus_is_matched_against_secondary_not_primary():
|
def test_sixteen_plus_is_matched_against_secondary_not_primary():
|
||||||
@@ -248,7 +248,7 @@ def test_sixteen_plus_is_matched_against_secondary_not_primary():
|
|||||||
_row(100003, "Nearby College", phase="16 plus", latitude=_at(0.6)),
|
_row(100003, "Nearby College", phase="16 plus", latitude=_at(0.6)),
|
||||||
_row(100004, "Nearby Primary", phase="Primary", latitude=_at(0.1)),
|
_row(100004, "Nearby Primary", phase="Primary", latitude=_at(0.1)),
|
||||||
)
|
)
|
||||||
result = select_similar(frame, 100001)
|
result = select_nearby(frame, 100001)
|
||||||
urns = {s["urn"] for s in result}
|
urns = {s["urn"] for s in result}
|
||||||
assert 100004 not in urns, "a primary school is not a peer for a sixth form"
|
assert 100004 not in urns, "a primary school is not a peer for a sixth form"
|
||||||
assert urns == {100002, 100003}
|
assert urns == {100002, 100003}
|
||||||
@@ -278,7 +278,7 @@ def test_shared_lists_only_what_is_actually_shared():
|
|||||||
_row(100003, "Faith differs", phase="Secondary", gender="Mixed",
|
_row(100003, "Faith differs", phase="Secondary", gender="Mixed",
|
||||||
religious_denomination="Church of England", admissions_policy="Non-selective", latitude=_at(0.6)),
|
religious_denomination="Church of England", admissions_policy="Non-selective", latitude=_at(0.6)),
|
||||||
)
|
)
|
||||||
by_urn = {s["urn"]: s for s in select_similar(frame, 100001)}
|
by_urn = {s["urn"]: s for s in select_nearby(frame, 100001)}
|
||||||
assert by_urn[100002]["shared"] == ["Mixed", "Non-selective", "No religious character"]
|
assert by_urn[100002]["shared"] == ["Mixed", "Non-selective", "No religious character"]
|
||||||
assert by_urn[100003]["shared"] == ["Mixed", "Non-selective"]
|
assert by_urn[100003]["shared"] == ["Mixed", "Non-selective"]
|
||||||
|
|
||||||
@@ -289,7 +289,7 @@ def test_a_shared_faith_is_named():
|
|||||||
_row(100002, "Also RC", religious_denomination="Roman Catholic", latitude=_at(0.4)),
|
_row(100002, "Also RC", religious_denomination="Roman Catholic", latitude=_at(0.4)),
|
||||||
_row(100003, "Secular", religious_denomination="None", latitude=_at(0.5)),
|
_row(100003, "Secular", religious_denomination="None", latitude=_at(0.5)),
|
||||||
)
|
)
|
||||||
by_urn = {s["urn"]: s for s in select_similar(frame, 100001)}
|
by_urn = {s["urn"]: s for s in select_nearby(frame, 100001)}
|
||||||
assert "Roman Catholic" in by_urn[100002]["shared"]
|
assert "Roman Catholic" in by_urn[100002]["shared"]
|
||||||
assert by_urn[100003]["shared"] == ["Mixed"]
|
assert by_urn[100003]["shared"] == ["Mixed"]
|
||||||
|
|
||||||
@@ -300,7 +300,7 @@ def test_shared_is_empty_when_nothing_is_shared():
|
|||||||
_row(100002, "A", gender="Mixed", religious_denomination="None", latitude=_at(0.4)),
|
_row(100002, "A", gender="Mixed", religious_denomination="None", latitude=_at(0.4)),
|
||||||
_row(100003, "B", gender="Mixed", religious_denomination="Church of England", latitude=_at(0.5)),
|
_row(100003, "B", gender="Mixed", religious_denomination="Church of England", latitude=_at(0.5)),
|
||||||
)
|
)
|
||||||
assert all(s["shared"] == [] for s in select_similar(frame, 100001))
|
assert all(s["shared"] == [] for s in select_nearby(frame, 100001))
|
||||||
|
|
||||||
|
|
||||||
def test_no_tier_is_reported_because_there_are_no_tiers():
|
def test_no_tier_is_reported_because_there_are_no_tiers():
|
||||||
@@ -309,7 +309,7 @@ def test_no_tier_is_reported_because_there_are_no_tiers():
|
|||||||
_row(100002, "A", latitude=_at(0.4)),
|
_row(100002, "A", latitude=_at(0.4)),
|
||||||
_row(100003, "B", latitude=_at(0.5)),
|
_row(100003, "B", latitude=_at(0.5)),
|
||||||
)
|
)
|
||||||
assert all("tier" not in s for s in select_similar(frame, 100001))
|
assert all("tier" not in s for s in select_nearby(frame, 100001))
|
||||||
|
|
||||||
|
|
||||||
def test_metric_follows_the_phase_side_not_the_neighbour():
|
def test_metric_follows_the_phase_side_not_the_neighbour():
|
||||||
@@ -318,7 +318,7 @@ def test_metric_follows_the_phase_side_not_the_neighbour():
|
|||||||
_row(100002, "A", phase="Secondary", attainment_8_score=52.8, latitude=_at(0.5)),
|
_row(100002, "A", phase="Secondary", attainment_8_score=52.8, latitude=_at(0.5)),
|
||||||
_row(100003, "B", phase="Secondary", attainment_8_score=np.nan, latitude=_at(0.6)),
|
_row(100003, "B", phase="Secondary", attainment_8_score=np.nan, latitude=_at(0.6)),
|
||||||
)
|
)
|
||||||
by_urn = {s["urn"]: s for s in select_similar(frame, 100001)}
|
by_urn = {s["urn"]: s for s in select_nearby(frame, 100001)}
|
||||||
assert by_urn[100002]["metric_key"] == "attainment_8_score"
|
assert by_urn[100002]["metric_key"] == "attainment_8_score"
|
||||||
assert by_urn[100002]["metric_value"] == 52.8
|
assert by_urn[100002]["metric_value"] == 52.8
|
||||||
assert by_urn[100002]["metric_year"] == 202425
|
assert by_urn[100002]["metric_year"] == 202425
|
||||||
@@ -331,7 +331,7 @@ def test_values_are_json_safe_native_types():
|
|||||||
_row(100002, "A", latitude=_at(0.5)),
|
_row(100002, "A", latitude=_at(0.5)),
|
||||||
_row(100003, "B", latitude=_at(0.6)),
|
_row(100003, "B", latitude=_at(0.6)),
|
||||||
)
|
)
|
||||||
for school in select_similar(frame, 100001):
|
for school in select_nearby(frame, 100001):
|
||||||
assert isinstance(school["urn"], int)
|
assert isinstance(school["urn"], int)
|
||||||
assert isinstance(school["distance_miles"], float)
|
assert isinstance(school["distance_miles"], float)
|
||||||
assert not isinstance(school["metric_value"], np.generic)
|
assert not isinstance(school["metric_value"], np.generic)
|
||||||
@@ -366,7 +366,7 @@ def client(monkeypatch):
|
|||||||
def test_detail_payload_carries_nearby_schools(client):
|
def test_detail_payload_carries_nearby_schools(client):
|
||||||
resp = client.get("/api/schools/100001")
|
resp = client.get("/api/schools/100001")
|
||||||
assert resp.status_code == 200, resp.text
|
assert resp.status_code == 200, resp.text
|
||||||
similar = resp.json()["similar_schools"]
|
similar = resp.json()["nearby_schools"]
|
||||||
assert [s["school_name"] for s in similar] == ["Neighbour A", "Neighbour B"]
|
assert [s["school_name"] for s in similar] == ["Neighbour A", "Neighbour B"]
|
||||||
assert similar[0]["metric_key"] == "rwm_expected_pct"
|
assert similar[0]["metric_key"] == "rwm_expected_pct"
|
||||||
|
|
||||||
@@ -377,7 +377,7 @@ def test_a_failure_in_selection_does_not_break_the_page(client, monkeypatch):
|
|||||||
def _explode(*args, **kwargs):
|
def _explode(*args, **kwargs):
|
||||||
raise ValueError("selection blew up")
|
raise ValueError("selection blew up")
|
||||||
|
|
||||||
monkeypatch.setattr(app_module, "select_similar", _explode)
|
monkeypatch.setattr(app_module, "select_nearby", _explode)
|
||||||
resp = client.get("/api/schools/100001")
|
resp = client.get("/api/schools/100001")
|
||||||
assert resp.status_code == 200, resp.text
|
assert resp.status_code == 200, resp.text
|
||||||
assert resp.json()["similar_schools"] == []
|
assert resp.json()["nearby_schools"] == []
|
||||||
@@ -73,12 +73,12 @@ There is no SWR dependency. Leaflet maps are loaded through dynamic wrappers;
|
|||||||
Chart.js renders performance and comparison charts.
|
Chart.js renders performance and comparison charts.
|
||||||
|
|
||||||
`components/school/` contains detail sections, with section decisions and data
|
`components/school/` contains detail sections, with section decisions and data
|
||||||
preparation in `lib/schoolSections.ts`. The similar-schools section is selected
|
preparation in `lib/schoolSections.ts`. The nearby-schools section is selected in
|
||||||
in `backend/similar_schools.py` — hard filters that never relax (phase,
|
`backend/nearby_schools.py` — hard filters decide eligibility (phase, provision,
|
||||||
provision, selectivity, gender) and soft preferences that do (religious
|
selectivity, gender) and distance alone decides the order, capped per phase —
|
||||||
character, then gender exactness) — and served on `/api/schools/{urn}`. Its
|
and served on `/api/schools/{urn}`. Its rules are presentation logic,
|
||||||
rules are presentation logic, deliberately kept out of `marts.*` so they can be
|
deliberately kept out of `marts.*` so they can be tuned by deploy rather than by
|
||||||
tuned by deploy rather than by pipeline run. `lib/types.ts` contains manually maintained
|
pipeline run. `lib/types.ts` contains manually maintained
|
||||||
API types. `payload-types.ts` and the Payload import map are generated artifacts.
|
API types. `payload-types.ts` and the Payload import map are generated artifacts.
|
||||||
|
|
||||||
## Publication and caching today
|
## Publication and caching today
|
||||||
|
|||||||
+17
-17
@@ -9,24 +9,24 @@ import { render, screen } from '@testing-library/react';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
nearbyNoun,
|
nearbyNoun,
|
||||||
SimilarSchoolsSection,
|
NearbySchoolsSection,
|
||||||
shouldRenderSimilar,
|
shouldRenderNearby,
|
||||||
} from '@/components/school/SimilarSchoolsSection';
|
} from '@/components/school/NearbySchoolsSection';
|
||||||
import type { SimilarSchool } from '@/lib/types';
|
import type { NearbySchool } from '@/lib/types';
|
||||||
|
|
||||||
jest.mock('@/components/school/AddToCompareButton', () => ({
|
jest.mock('@/components/school/AddToCompareButton', () => ({
|
||||||
AddToCompareButton: ({ school }: { school: SimilarSchool }) => (
|
AddToCompareButton: ({ school }: { school: NearbySchool }) => (
|
||||||
<button type="button">Add {school.school_name} to compare</button>
|
<button type="button">Add {school.school_name} to compare</button>
|
||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('@/components/school/SimilarSchoolsCompareBar', () => ({
|
jest.mock('@/components/school/NearbySchoolsCompareBar', () => ({
|
||||||
SimilarSchoolsCompareBar: ({ thisUrn }: { thisUrn: number }) => (
|
NearbySchoolsCompareBar: ({ thisUrn }: { thisUrn: number }) => (
|
||||||
<div data-testid="compare-bar">bar for {thisUrn}</div>
|
<div data-testid="compare-bar">bar for {thisUrn}</div>
|
||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
function school(overrides: Partial<SimilarSchool> = {}): SimilarSchool {
|
function school(overrides: Partial<NearbySchool> = {}): NearbySchool {
|
||||||
return {
|
return {
|
||||||
urn: 100002,
|
urn: 100002,
|
||||||
school_name: 'Willow Lane Primary School',
|
school_name: 'Willow Lane Primary School',
|
||||||
@@ -41,14 +41,14 @@ function school(overrides: Partial<SimilarSchool> = {}): SimilarSchool {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderSection(similar: SimilarSchool[]) {
|
function renderSection(nearby: NearbySchool[]) {
|
||||||
return render(
|
return render(
|
||||||
<SimilarSchoolsSection
|
<NearbySchoolsSection
|
||||||
urn={100001}
|
urn={100001}
|
||||||
schoolName="Meadowbrook Primary School"
|
schoolName="Meadowbrook Primary School"
|
||||||
phase="Primary"
|
phase="Primary"
|
||||||
thisMetricValue={72}
|
thisMetricValue={72}
|
||||||
similar={similar}
|
nearby={nearby}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -60,11 +60,11 @@ describe('render gates', () => {
|
|||||||
['empty', []],
|
['empty', []],
|
||||||
['a single school', [school()]],
|
['a single school', [school()]],
|
||||||
])('renders nothing for %s', (_label, value) => {
|
])('renders nothing for %s', (_label, value) => {
|
||||||
expect(shouldRenderSimilar(value as SimilarSchool[] | null | undefined)).toBe(false);
|
expect(shouldRenderNearby(value as NearbySchool[] | null | undefined)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders for two or more schools', () => {
|
it('renders for two or more schools', () => {
|
||||||
expect(shouldRenderSimilar([school(), school({ urn: 100003 })])).toBe(true);
|
expect(shouldRenderNearby([school(), school({ urn: 100003 })])).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns null rather than an empty shell below the minimum', () => {
|
it('returns null rather than an empty shell below the minimum', () => {
|
||||||
@@ -91,12 +91,12 @@ describe('what the section claims', () => {
|
|||||||
|
|
||||||
it('shows no chips at all when nothing is shared, rather than inventing one', () => {
|
it('shows no chips at all when nothing is shared, rather than inventing one', () => {
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<SimilarSchoolsSection
|
<NearbySchoolsSection
|
||||||
urn={100001}
|
urn={100001}
|
||||||
schoolName="Meadowbrook Primary School"
|
schoolName="Meadowbrook Primary School"
|
||||||
phase="Primary"
|
phase="Primary"
|
||||||
thisMetricValue={72}
|
thisMetricValue={72}
|
||||||
similar={[school({ shared: [] }), school({ urn: 100003, shared: [] })]}
|
nearby={[school({ shared: [] }), school({ urn: 100003, shared: [] })]}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
// The card still carries its distance, name, type and figure — just no
|
// The card still carries its distance, name, type and figure — just no
|
||||||
@@ -124,12 +124,12 @@ describe('what the lede calls the set', () => {
|
|||||||
|
|
||||||
it('never calls a sixth form college\'s neighbours primary schools', () => {
|
it('never calls a sixth form college\'s neighbours primary schools', () => {
|
||||||
render(
|
render(
|
||||||
<SimilarSchoolsSection
|
<NearbySchoolsSection
|
||||||
urn={100001}
|
urn={100001}
|
||||||
schoolName="Barnet Sixth Form College"
|
schoolName="Barnet Sixth Form College"
|
||||||
phase="16 plus"
|
phase="16 plus"
|
||||||
thisMetricValue={null}
|
thisMetricValue={null}
|
||||||
similar={[school(), school({ urn: 100003 })]}
|
nearby={[school(), school({ urn: 100003 })]}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
expect(screen.getByText(/Other schools and colleges near Barnet Sixth Form College/)).toBeInTheDocument();
|
expect(screen.getByText(/Other schools and colleges near Barnet Sixth Form College/)).toBeInTheDocument();
|
||||||
@@ -8,7 +8,7 @@ import { APIFetchError, fetchSchoolDetails, fetchSchools, fetchNationalAverages
|
|||||||
import { notFound, redirect } from 'next/navigation';
|
import { notFound, redirect } from 'next/navigation';
|
||||||
import { SchoolDetailShell } from '@/components/school/SchoolDetailShell';
|
import { SchoolDetailShell } from '@/components/school/SchoolDetailShell';
|
||||||
import { NearbyPlaces } from '@/components/school/NearbyPlaces';
|
import { NearbyPlaces } from '@/components/school/NearbyPlaces';
|
||||||
import { shouldRenderSimilar } from '@/components/school/SimilarSchoolsSection';
|
import { shouldRenderNearby } from '@/components/school/NearbySchoolsSection';
|
||||||
import { schoolBreadcrumbJsonLd, type SchoolPlace } from '@/lib/jsonld';
|
import { schoolBreadcrumbJsonLd, type SchoolPlace } from '@/lib/jsonld';
|
||||||
import { PrimarySchoolSections } from '@/components/school/PrimarySchoolSections';
|
import { PrimarySchoolSections } from '@/components/school/PrimarySchoolSections';
|
||||||
import { SecondarySchoolSections } from '@/components/school/SecondarySchoolSections';
|
import { SecondarySchoolSections } from '@/components/school/SecondarySchoolSections';
|
||||||
@@ -157,7 +157,7 @@ export default async function SchoolPage({ params }: SchoolPageProps) {
|
|||||||
// lockstep deploy of the two images.
|
// lockstep deploy of the two images.
|
||||||
const places: SchoolPlace[] = data.places ?? [];
|
const places: SchoolPlace[] = data.places ?? [];
|
||||||
// Absent on an older API build, exactly like `places` above.
|
// Absent on an older API build, exactly like `places` above.
|
||||||
const similarSchools = data.similar_schools ?? [];
|
const nearbySchools = data.nearby_schools ?? [];
|
||||||
|
|
||||||
// Redirect bare URN to canonical slug URL
|
// Redirect bare URN to canonical slug URL
|
||||||
const canonicalSlug = schoolUrl(urn, school_info.school_name).replace('/school/', '');
|
const canonicalSlug = schoolUrl(urn, school_info.school_name).replace('/school/', '');
|
||||||
@@ -189,7 +189,7 @@ export default async function SchoolPage({ params }: SchoolPageProps) {
|
|||||||
admissions: admissions ?? null,
|
admissions: admissions ?? null,
|
||||||
admissionDistance: admission_distance ?? null,
|
admissionDistance: admission_distance ?? null,
|
||||||
hasLocation: school_info.latitude != null && school_info.longitude != null,
|
hasLocation: school_info.latitude != null && school_info.longitude != null,
|
||||||
hasNearbySchools: shouldRenderSimilar(similarSchools),
|
hasNearbySchools: shouldRenderNearby(nearbySchools),
|
||||||
yearlyDataLength: yearly_data.length,
|
yearlyDataLength: yearly_data.length,
|
||||||
};
|
};
|
||||||
const primaryNavItems = buildNavItems(primaryFlags, navInput);
|
const primaryNavItems = buildNavItems(primaryFlags, navInput);
|
||||||
@@ -266,7 +266,7 @@ export default async function SchoolPage({ params }: SchoolPageProps) {
|
|||||||
finance={finance ?? null}
|
finance={finance ?? null}
|
||||||
nationalAvg={nationalAvg}
|
nationalAvg={nationalAvg}
|
||||||
destinations={destinations ?? null}
|
destinations={destinations ?? null}
|
||||||
similarSchools={similarSchools}
|
nearbySchools={nearbySchools}
|
||||||
flags={secondaryFlags}
|
flags={secondaryFlags}
|
||||||
/>
|
/>
|
||||||
</SchoolDetailShell>
|
</SchoolDetailShell>
|
||||||
@@ -289,7 +289,7 @@ export default async function SchoolPage({ params }: SchoolPageProps) {
|
|||||||
deprivation={deprivation ?? null}
|
deprivation={deprivation ?? null}
|
||||||
finance={finance ?? null}
|
finance={finance ?? null}
|
||||||
nationalAvg={nationalAvg}
|
nationalAvg={nationalAvg}
|
||||||
similarSchools={similarSchools}
|
nearbySchools={nearbySchools}
|
||||||
flags={primaryFlags}
|
flags={primaryFlags}
|
||||||
/>
|
/>
|
||||||
</SchoolDetailShell>
|
</SchoolDetailShell>
|
||||||
|
|||||||
@@ -9,10 +9,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { useComparisonContext } from '@/context/ComparisonContext';
|
import { useComparisonContext } from '@/context/ComparisonContext';
|
||||||
import type { School, SimilarSchool } from '@/lib/types';
|
import type { School, NearbySchool } from '@/lib/types';
|
||||||
import styles from './SimilarSchools.module.css';
|
import styles from './NearbySchools.module.css';
|
||||||
|
|
||||||
export function AddToCompareButton({ school }: { school: SimilarSchool }) {
|
export function AddToCompareButton({ school }: { school: NearbySchool }) {
|
||||||
const { addSchool, removeSchool, selectedSchools } = useComparisonContext();
|
const { addSchool, removeSchool, selectedSchools } = useComparisonContext();
|
||||||
const selected = selectedSchools.some((s) => s.urn === school.urn);
|
const selected = selectedSchools.some((s) => s.urn === school.urn);
|
||||||
|
|
||||||
|
|||||||
File renamed without changes.
+2
-2
@@ -14,7 +14,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { useCallback, useEffect, useRef, useState, type ReactNode } from 'react';
|
import { useCallback, useEffect, useRef, useState, type ReactNode } from 'react';
|
||||||
import styles from './SimilarSchools.module.css';
|
import styles from './NearbySchools.module.css';
|
||||||
|
|
||||||
/** Three cards fit the row, so fewer than four has nowhere to scroll to.
|
/** Three cards fit the row, so fewer than four has nowhere to scroll to.
|
||||||
* Below 640px the arrows are not rendered at all — see the stylesheet. */
|
* Below 640px the arrows are not rendered at all — see the stylesheet. */
|
||||||
@@ -31,7 +31,7 @@ const VISIBLE = 3;
|
|||||||
*/
|
*/
|
||||||
const EDGE = 8;
|
const EDGE = 8;
|
||||||
|
|
||||||
export function SimilarSchoolsCarousel({
|
export function NearbySchoolsCarousel({
|
||||||
count,
|
count,
|
||||||
labelledBy,
|
labelledBy,
|
||||||
header,
|
header,
|
||||||
+4
-4
@@ -11,15 +11,15 @@
|
|||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useComparisonContext } from '@/context/ComparisonContext';
|
import { useComparisonContext } from '@/context/ComparisonContext';
|
||||||
import type { SimilarSchool } from '@/lib/types';
|
import type { NearbySchool } from '@/lib/types';
|
||||||
import styles from './SimilarSchools.module.css';
|
import styles from './NearbySchools.module.css';
|
||||||
|
|
||||||
export function SimilarSchoolsCompareBar({
|
export function NearbySchoolsCompareBar({
|
||||||
thisUrn,
|
thisUrn,
|
||||||
candidates,
|
candidates,
|
||||||
}: {
|
}: {
|
||||||
thisUrn: number;
|
thisUrn: number;
|
||||||
candidates: SimilarSchool[];
|
candidates: NearbySchool[];
|
||||||
}) {
|
}) {
|
||||||
const { selectedSchools } = useComparisonContext();
|
const { selectedSchools } = useComparisonContext();
|
||||||
|
|
||||||
+14
-14
@@ -18,18 +18,18 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import type { SimilarSchool } from '@/lib/types';
|
import type { NearbySchool } from '@/lib/types';
|
||||||
import { schoolUrl } from '@/lib/utils';
|
import { schoolUrl } from '@/lib/utils';
|
||||||
import { AddToCompareButton } from './AddToCompareButton';
|
import { AddToCompareButton } from './AddToCompareButton';
|
||||||
import { SimilarSchoolsCarousel } from './SimilarSchoolsCarousel';
|
import { NearbySchoolsCarousel } from './NearbySchoolsCarousel';
|
||||||
import { SimilarSchoolsCompareBar } from './SimilarSchoolsCompareBar';
|
import { NearbySchoolsCompareBar } from './NearbySchoolsCompareBar';
|
||||||
import { Section } from './sectionShared';
|
import { Section } from './sectionShared';
|
||||||
import styles from './SimilarSchools.module.css';
|
import styles from './NearbySchools.module.css';
|
||||||
|
|
||||||
const MINIMUM = 2;
|
const MINIMUM = 2;
|
||||||
|
|
||||||
export function shouldRenderSimilar(similar?: SimilarSchool[] | null): boolean {
|
export function shouldRenderNearby(nearby?: NearbySchool[] | null): boolean {
|
||||||
return (similar?.length ?? 0) >= MINIMUM;
|
return (nearby?.length ?? 0) >= MINIMUM;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -63,22 +63,22 @@ function formatMetric(value: number | null, key: string): string {
|
|||||||
return key === 'attainment_8_score' ? value.toFixed(1) : `${Math.round(value)}%`;
|
return key === 'attainment_8_score' ? value.toFixed(1) : `${Math.round(value)}%`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SimilarSchoolsSection({
|
export function NearbySchoolsSection({
|
||||||
urn,
|
urn,
|
||||||
schoolName,
|
schoolName,
|
||||||
phase,
|
phase,
|
||||||
thisMetricValue,
|
thisMetricValue,
|
||||||
similar,
|
nearby,
|
||||||
}: {
|
}: {
|
||||||
urn: number;
|
urn: number;
|
||||||
schoolName: string;
|
schoolName: string;
|
||||||
/** The school's own GIAS phase, not the template it renders with. */
|
/** The school's own GIAS phase, not the template it renders with. */
|
||||||
phase: string | null | undefined;
|
phase: string | null | undefined;
|
||||||
thisMetricValue: number | null;
|
thisMetricValue: number | null;
|
||||||
similar?: SimilarSchool[] | null;
|
nearby?: NearbySchool[] | null;
|
||||||
}) {
|
}) {
|
||||||
if (!shouldRenderSimilar(similar)) return null;
|
if (!shouldRenderNearby(nearby)) return null;
|
||||||
const schools = similar as SimilarSchool[];
|
const schools = nearby as NearbySchool[];
|
||||||
|
|
||||||
// One card matched on phase alone, so the section may not claim the set
|
// One card matched on phase alone, so the section may not claim the set
|
||||||
// shares an intake with this school.
|
// shares an intake with this school.
|
||||||
@@ -87,7 +87,7 @@ export function SimilarSchoolsSection({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Section id="nearby">
|
<Section id="nearby">
|
||||||
<SimilarSchoolsCarousel
|
<NearbySchoolsCarousel
|
||||||
count={schools.length}
|
count={schools.length}
|
||||||
labelledBy="nearby-schools-heading"
|
labelledBy="nearby-schools-heading"
|
||||||
header={
|
header={
|
||||||
@@ -137,9 +137,9 @@ export function SimilarSchoolsSection({
|
|||||||
<AddToCompareButton school={school} />
|
<AddToCompareButton school={school} />
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</SimilarSchoolsCarousel>
|
</NearbySchoolsCarousel>
|
||||||
|
|
||||||
<SimilarSchoolsCompareBar thisUrn={urn} candidates={schools} />
|
<NearbySchoolsCompareBar thisUrn={urn} candidates={schools} />
|
||||||
|
|
||||||
{/* The one caveat the cards cannot make on their own: a reader who takes
|
{/* The one caveat the cards cannot make on their own: a reader who takes
|
||||||
"0.6 miles away" for the walk has been misled, and nothing else here
|
"0.6 miles away" for the walk has been misled, and nothing else here
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
import type {
|
import type {
|
||||||
School, SchoolResult, AbsenceData, OfstedInspection, SchoolCensus,
|
School, SchoolResult, AbsenceData, OfstedInspection, SchoolCensus,
|
||||||
SchoolAdmissions, SchoolAdmissionDistance, SchoolDeprivation, SchoolFinance, NationalAverages,
|
SchoolAdmissions, SchoolAdmissionDistance, SchoolDeprivation, SchoolFinance, NationalAverages,
|
||||||
SimilarSchool,
|
NearbySchool,
|
||||||
} from '@/lib/types';
|
} from '@/lib/types';
|
||||||
import { ofstedLegacyAreas } from '@/lib/utils';
|
import { ofstedLegacyAreas } from '@/lib/utils';
|
||||||
import type { SchoolFlags } from '@/lib/schoolSections';
|
import type { SchoolFlags } from '@/lib/schoolSections';
|
||||||
@@ -27,7 +27,7 @@ import { HistorySection } from './HistorySection';
|
|||||||
import { SchoolLifeSection } from './SchoolLifeSection';
|
import { SchoolLifeSection } from './SchoolLifeSection';
|
||||||
import { LocalAreaSection } from './LocalAreaSection';
|
import { LocalAreaSection } from './LocalAreaSection';
|
||||||
import { FinancesSection } from './FinancesSection';
|
import { FinancesSection } from './FinancesSection';
|
||||||
import { SimilarSchoolsSection } from './SimilarSchoolsSection';
|
import { NearbySchoolsSection } from './NearbySchoolsSection';
|
||||||
|
|
||||||
export interface PrimarySchoolSectionsProps {
|
export interface PrimarySchoolSectionsProps {
|
||||||
schoolInfo: School;
|
schoolInfo: School;
|
||||||
@@ -42,14 +42,14 @@ export interface PrimarySchoolSectionsProps {
|
|||||||
finance: SchoolFinance | null;
|
finance: SchoolFinance | null;
|
||||||
nationalAvg: NationalAverages | null;
|
nationalAvg: NationalAverages | null;
|
||||||
/** Nearby schools of a comparable intake. Absent on an older API build. */
|
/** Nearby schools of a comparable intake. Absent on an older API build. */
|
||||||
similarSchools?: SimilarSchool[];
|
nearbySchools?: NearbySchool[];
|
||||||
flags: SchoolFlags;
|
flags: SchoolFlags;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PrimarySchoolSections({
|
export function PrimarySchoolSections({
|
||||||
schoolInfo, yearlyData, absenceData, ofsted, census,
|
schoolInfo, yearlyData, absenceData, ofsted, census,
|
||||||
admissions, admissionsHistory, admissionDistance,
|
admissions, admissionsHistory, admissionDistance,
|
||||||
deprivation, finance, nationalAvg, similarSchools, flags,
|
deprivation, finance, nationalAvg, nearbySchools, flags,
|
||||||
}: PrimarySchoolSectionsProps) {
|
}: PrimarySchoolSectionsProps) {
|
||||||
const primaryAvg = nationalAvg?.primary ?? {};
|
const primaryAvg = nationalAvg?.primary ?? {};
|
||||||
const secondaryAvg = nationalAvg?.secondary ?? {};
|
const secondaryAvg = nationalAvg?.secondary ?? {};
|
||||||
@@ -152,12 +152,12 @@ export function PrimarySchoolSections({
|
|||||||
{flags.hasFinance && finance && <FinancesSection finance={finance} />}
|
{flags.hasFinance && finance && <FinancesSection finance={finance} />}
|
||||||
|
|
||||||
{/* Last: it is where the reader goes next, not part of this school. */}
|
{/* Last: it is where the reader goes next, not part of this school. */}
|
||||||
<SimilarSchoolsSection
|
<NearbySchoolsSection
|
||||||
urn={schoolInfo.urn}
|
urn={schoolInfo.urn}
|
||||||
schoolName={schoolInfo.school_name}
|
schoolName={schoolInfo.school_name}
|
||||||
phase={schoolInfo.phase}
|
phase={schoolInfo.phase}
|
||||||
thisMetricValue={flags.latestResults?.rwm_expected_pct ?? null}
|
thisMetricValue={flags.latestResults?.rwm_expected_pct ?? null}
|
||||||
similar={similarSchools}
|
nearby={nearbySchools}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
import type {
|
import type {
|
||||||
School, SchoolResult, AbsenceData, OfstedInspection, SchoolCensus,
|
School, SchoolResult, AbsenceData, OfstedInspection, SchoolCensus,
|
||||||
SchoolAdmissions, SchoolAdmissionDistance, SchoolDeprivation, SchoolFinance, NationalAverages,
|
SchoolAdmissions, SchoolAdmissionDistance, SchoolDeprivation, SchoolFinance, NationalAverages,
|
||||||
SchoolDestinations, SimilarSchool,
|
SchoolDestinations, NearbySchool,
|
||||||
} from '@/lib/types';
|
} from '@/lib/types';
|
||||||
import { ofstedLegacyAreas } from '@/lib/utils';
|
import { ofstedLegacyAreas } from '@/lib/utils';
|
||||||
import type { SecondaryFlags } from '@/lib/schoolSections';
|
import type { SecondaryFlags } from '@/lib/schoolSections';
|
||||||
@@ -27,7 +27,7 @@ import { DistanceSection } from './DistanceSection';
|
|||||||
import { SecondaryHistorySection } from './SecondaryHistorySection';
|
import { SecondaryHistorySection } from './SecondaryHistorySection';
|
||||||
import { WellbeingSection } from './WellbeingSection';
|
import { WellbeingSection } from './WellbeingSection';
|
||||||
import { FinancesSection } from './FinancesSection';
|
import { FinancesSection } from './FinancesSection';
|
||||||
import { SimilarSchoolsSection } from './SimilarSchoolsSection';
|
import { NearbySchoolsSection } from './NearbySchoolsSection';
|
||||||
import styles from './schoolSections.module.css';
|
import styles from './schoolSections.module.css';
|
||||||
|
|
||||||
export interface SecondarySchoolSectionsProps {
|
export interface SecondarySchoolSectionsProps {
|
||||||
@@ -49,14 +49,14 @@ export interface SecondarySchoolSectionsProps {
|
|||||||
nationalAvg: NationalAverages | null;
|
nationalAvg: NationalAverages | null;
|
||||||
destinations: SchoolDestinations | null;
|
destinations: SchoolDestinations | null;
|
||||||
/** Nearby schools of a comparable intake. Absent on an older API build. */
|
/** Nearby schools of a comparable intake. Absent on an older API build. */
|
||||||
similarSchools?: SimilarSchool[];
|
nearbySchools?: NearbySchool[];
|
||||||
flags: SecondaryFlags;
|
flags: SecondaryFlags;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SecondarySchoolSections({
|
export function SecondarySchoolSections({
|
||||||
schoolInfo, yearlyData, ofsted, census,
|
schoolInfo, yearlyData, ofsted, census,
|
||||||
admissions, admissionsHistory, admissionDistance,
|
admissions, admissionsHistory, admissionDistance,
|
||||||
deprivation, finance, nationalAvg, destinations, similarSchools, flags,
|
deprivation, finance, nationalAvg, destinations, nearbySchools, flags,
|
||||||
}: SecondarySchoolSectionsProps) {
|
}: SecondarySchoolSectionsProps) {
|
||||||
const secondaryAvg = nationalAvg?.secondary ?? {};
|
const secondaryAvg = nationalAvg?.secondary ?? {};
|
||||||
|
|
||||||
@@ -146,12 +146,12 @@ export function SecondarySchoolSections({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Last: it is where the reader goes next, not part of this school. */}
|
{/* Last: it is where the reader goes next, not part of this school. */}
|
||||||
<SimilarSchoolsSection
|
<NearbySchoolsSection
|
||||||
urn={schoolInfo.urn}
|
urn={schoolInfo.urn}
|
||||||
schoolName={schoolInfo.school_name}
|
schoolName={schoolInfo.school_name}
|
||||||
phase={schoolInfo.phase}
|
phase={schoolInfo.phase}
|
||||||
thisMetricValue={flags.latestResults?.attainment_8_score ?? null}
|
thisMetricValue={flags.latestResults?.attainment_8_score ?? null}
|
||||||
similar={similarSchools}
|
nearby={nearbySchools}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -354,7 +354,7 @@ export interface SchoolsResponse {
|
|||||||
* version ordered by it and buried the school down the road under faith
|
* version ordered by it and buried the school down the road under faith
|
||||||
* matches three times further away.
|
* matches three times further away.
|
||||||
*/
|
*/
|
||||||
export interface SimilarSchool {
|
export interface NearbySchool {
|
||||||
urn: number;
|
urn: number;
|
||||||
school_name: string;
|
school_name: string;
|
||||||
distance_miles: number;
|
distance_miles: number;
|
||||||
@@ -384,7 +384,7 @@ export interface SchoolDetailsResponse {
|
|||||||
* API that serves this must render without it. Absent and empty mean the
|
* API that serves this must render without it. Absent and empty mean the
|
||||||
* same thing here — no section.
|
* same thing here — no section.
|
||||||
*/
|
*/
|
||||||
similar_schools?: SimilarSchool[];
|
nearby_schools?: NearbySchool[];
|
||||||
yearly_data: SchoolResult[];
|
yearly_data: SchoolResult[];
|
||||||
absence_data: AbsenceData | null;
|
absence_data: AbsenceData | null;
|
||||||
// Supplementary data (null until Kestra populates)
|
// Supplementary data (null until Kestra populates)
|
||||||
|
|||||||
Reference in new issue
Block a user