feat: similar schools nearby on the detail page #150

Merged
tudor merged 12 commits from feat/similar-schools-nearby into main 2026-09-22 05:53:07 +00:00
2 changed files with 74 additions and 0 deletions
Showing only changes of commit 52b00ac752 - Show all commits

No files matched your search

+28
View File
@@ -41,6 +41,7 @@ from .data_loader import get_data_info as get_db_info
from . import flags
from .places import build_place_index, build_place_registry, places_for_urn
from .schemas import METRIC_DEFINITIONS, PHASE_GROUPS, RANKING_COLUMNS, SCHOOL_COLUMNS
from .similar_schools import select_similar
from .utils import clean_for_json, convert_to_native
# Values to exclude from filter dropdowns (empty strings, non-applicable labels)
@@ -265,6 +266,28 @@ def _places_payload(urn: int) -> list[dict]:
return payload
def _similar_schools_payload(urn: int, phase: str | None) -> list[dict]:
"""Nearby schools this page may offer as alternatives.
Wrapped: a failure in selection must never 500 a page that is otherwise
complete, which is the posture get_supplementary_data already takes. The
section simply does not render.
"""
try:
phase_text = (phase or "").lower()
# All-through schools render with the primary template, which is what
# decides the metric, so they are not secondary here.
is_secondary = phase_text != "all-through" and "secondary" in phase_text
return select_similar(load_latest_school_data(), int(urn), is_secondary)
except Exception:
import logging
logging.getLogger(__name__).exception(
"Similar schools selection failed for urn=%s", urn
)
return []
def _place_sitemap_rows(kinds: tuple[str, ...], registry=None) -> list[str]:
"""A <url> per place, plus a phase variant wherever that phase clears the
threshold on its own.
@@ -975,6 +998,11 @@ async def get_school_details(request: Request, urn: int):
# and authority both fall below the publish threshold has nowhere to
# point, and the page renders without the module.
"places": _places_payload(urn),
# Nearby schools of the same phase and a comparable intake. Always
# present on a build with this code; the frontend treats absent and
# empty identically, which is what lets the two images deploy
# independently.
"similar_schools": _similar_schools_payload(urn, latest.get("phase")),
"yearly_data": clean_for_json(school_data),
# Supplementary data (null if not yet populated by Kestra)
"ofsted": supplementary.get("ofsted"),
+46
View File
@@ -250,3 +250,49 @@ def test_values_are_json_safe_native_types():
assert isinstance(school["urn"], int)
assert isinstance(school["distance_miles"], float)
assert not isinstance(school["metric_value"], np.generic)
# ---------------------------------------------------------------------------
# The endpoint
# ---------------------------------------------------------------------------
import pytest
from fastapi.testclient import TestClient
def _endpoint_frame():
return _frame(
_row(100001, "Subject Primary"),
_row(100002, "Neighbour A", latitude=_at(0.5)),
_row(100003, "Neighbour B", latitude=_at(0.6)),
)
@pytest.fixture()
def client(monkeypatch):
from backend import app as app_module
monkeypatch.setattr(app_module, "load_latest_school_data", _endpoint_frame)
monkeypatch.setattr(app_module, "load_school_data", _endpoint_frame)
monkeypatch.setattr(app_module, "get_supplementary_data", lambda db, urn: {})
return TestClient(app_module.app, raise_server_exceptions=False)
def test_detail_payload_carries_similar_schools(client):
resp = client.get("/api/schools/100001")
assert resp.status_code == 200, resp.text
similar = resp.json()["similar_schools"]
assert [s["school_name"] for s in similar] == ["Neighbour A", "Neighbour B"]
assert similar[0]["metric_key"] == "rwm_expected_pct"
def test_a_failure_in_selection_does_not_break_the_page(client, monkeypatch):
from backend import app as app_module
def _explode(*args, **kwargs):
raise ValueError("selection blew up")
monkeypatch.setattr(app_module, "select_similar", _explode)
resp = client.get("/api/schools/100001")
assert resp.status_code == 200, resp.text
assert resp.json()["similar_schools"] == []