Use path segments for journey result URLs

This commit is contained in:
Edward Betts 2026-05-26 13:19:22 +01:00
parent ce6e8afcc8
commit 1c230ce0d6
3 changed files with 76 additions and 55 deletions

50
app.py
View file

@ -389,9 +389,15 @@ def _results_url(
**params, **params,
) )
if journey_type == "inbound": if journey_type == "inbound":
params["journey_type"] = "inbound" return url_for(
"back_results",
station_crs=station_crs,
slug=slug,
travel_date=travel_date,
**params,
)
return url_for( return url_for(
"results", "out_results",
station_crs=station_crs, station_crs=station_crs,
slug=slug, slug=slug,
travel_date=travel_date, travel_date=travel_date,
@ -463,18 +469,17 @@ def search() -> ResponseReturnValue:
return redirect(url_for("index")) return redirect(url_for("index"))
@app.route("/results/<station_crs>/<slug>/<travel_date>") @app.route("/results/<station_crs>/<slug>/out/<travel_date>")
def results(station_crs: str, slug: str, travel_date: str) -> ResponseReturnValue: def out_results(station_crs: str, slug: str, travel_date: str) -> ResponseReturnValue:
return _results( return _results(station_crs, slug, travel_date, "outbound", None)
station_crs,
slug,
travel_date,
request.args.get("journey_type", "outbound"),
request.args.get("return_date"),
)
@app.route("/results/<station_crs>/<slug>/<travel_date>/return/<return_date>") @app.route("/results/<station_crs>/<slug>/back/<travel_date>")
def back_results(station_crs: str, slug: str, travel_date: str) -> ResponseReturnValue:
return _results(station_crs, slug, travel_date, "inbound", None)
@app.route("/results/<station_crs>/<slug>/return/<travel_date>/<return_date>")
def return_results( def return_results(
station_crs: str, slug: str, travel_date: str, return_date: str station_crs: str, slug: str, travel_date: str, return_date: str
) -> ResponseReturnValue: ) -> ResponseReturnValue:
@ -1108,7 +1113,7 @@ def _results(
station_crs=station_crs, station_crs=station_crs,
slug=slug, slug=slug,
travel_date=travel_date, travel_date=travel_date,
journey_type=journey_type if journey_type == "inbound" else None, path_journey_type="back" if journey_type == "inbound" else "out",
) )
summary_html = _build_summary_html( summary_html = _build_summary_html(
@ -1331,7 +1336,7 @@ def _results(
station_crs=station_crs, station_crs=station_crs,
slug=slug, slug=slug,
travel_date=travel_date, travel_date=travel_date,
journey_type=journey_type if journey_type == "inbound" else None, path_journey_type="back" if journey_type == "inbound" else "out",
) )
return render_template( return render_template(
@ -1480,15 +1485,20 @@ def api_walkon_fares(
return jsonify({"error": str(e)}), 500 return jsonify({"error": str(e)}), 500
@app.route("/api/results_refresh/<station_crs>/<slug>/<travel_date>") @app.route(
def api_results_refresh(station_crs: str, slug: str, travel_date: str) -> Response: "/api/results_refresh/<station_crs>/<slug>/<path_journey_type>/<travel_date>"
return _api_results_refresh( )
station_crs, slug, travel_date, request.args.get("return_date") def api_results_refresh(
) station_crs: str, slug: str, path_journey_type: str, travel_date: str
) -> Response:
if path_journey_type not in {"out", "back"}:
abort(404)
journey_type = "inbound" if path_journey_type == "back" else "outbound"
return _api_results_refresh(station_crs, slug, travel_date, None, journey_type)
@app.route( @app.route(
"/api/results_refresh/<station_crs>/<slug>/<travel_date>/return/<return_date>" "/api/results_refresh/<station_crs>/<slug>/return/<travel_date>/<return_date>"
) )
def api_return_results_refresh( def api_return_results_refresh(
station_crs: str, slug: str, travel_date: str, return_date: str station_crs: str, slug: str, travel_date: str, return_date: str

View file

@ -78,7 +78,7 @@ def test_search_redirects_to_results_with_selected_params() -> None:
assert resp.status_code == 302 assert resp.status_code == 302
assert resp.headers["Location"].endswith( assert resp.headers["Location"].endswith(
"/results/BRI/rotterdam/2026-04-10?min_connection=60&max_connection=120" "/results/BRI/rotterdam/out/2026-04-10?min_connection=60&max_connection=120"
) )
@ -91,10 +91,21 @@ def test_search_redirects_return_with_return_date() -> None:
assert resp.status_code == 302 assert resp.status_code == 302
assert resp.headers["Location"].endswith( assert resp.headers["Location"].endswith(
"/results/BRI/paris/2026-04-10/return/2026-04-17" "/results/BRI/paris/return/2026-04-10/2026-04-17"
) )
def test_search_redirects_inbound_to_back_path() -> None:
client = _client()
resp = client.get(
"/search?journey_type=inbound&destination=paris&travel_date=2026-04-10&station_crs=BRI"
)
assert resp.status_code == 302
assert resp.headers["Location"].endswith("/results/BRI/paris/back/2026-04-10")
def test_nr_weekday_cache_key_includes_timetable_period() -> None: def test_nr_weekday_cache_key_includes_timetable_period() -> None:
key = app_module._nr_weekday_cache_key("to_paddington", "BRI", "2026-06-22") key = app_module._nr_weekday_cache_key("to_paddington", "BRI", "2026-06-22")
@ -106,7 +117,7 @@ def test_results_shows_same_day_destination_switcher(monkeypatch: Any) -> None:
client = _client() client = _client()
resp = client.get( resp = client.get(
"/results/BRI/paris/2026-04-10?min_connection=60&max_connection=120" "/results/BRI/paris/out/2026-04-10?min_connection=60&max_connection=120"
) )
html = resp.get_data(as_text=True) html = resp.get_data(as_text=True)
@ -114,11 +125,11 @@ def test_results_shows_same_day_destination_switcher(monkeypatch: Any) -> None:
assert "Switch destination for Friday 10 April 2026" in html assert "Switch destination for Friday 10 April 2026" in html
assert '<span class="chip-current">Paris Gare du Nord</span>' in html assert '<span class="chip-current">Paris Gare du Nord</span>' in html
assert ( assert (
"/results/BRI/brussels/2026-04-10?min_connection=60&amp;max_connection=120" "/results/BRI/brussels/out/2026-04-10?min_connection=60&amp;max_connection=120"
in html in html
) )
assert ( assert (
"/results/BRI/rotterdam/2026-04-10?min_connection=60&amp;max_connection=120" "/results/BRI/rotterdam/out/2026-04-10?min_connection=60&amp;max_connection=120"
in html in html
) )
assert "ES 9014" in html assert "ES 9014" in html
@ -171,7 +182,7 @@ def test_results_can_render_from_weekday_timetable_cache(monkeypatch: Any) -> No
client = _client() client = _client()
resp = client.get( resp = client.get(
"/results/BRI/paris/2026-06-22?min_connection=60&max_connection=120" "/results/BRI/paris/out/2026-06-22?min_connection=60&max_connection=120"
) )
html = resp.get_data(as_text=True) html = resp.get_data(as_text=True)
@ -180,7 +191,7 @@ def test_results_can_render_from_weekday_timetable_cache(monkeypatch: Any) -> No
assert "10:01 &rarr; 13:34" in html assert "10:01 &rarr; 13:34" in html
assert "ES 9014" in html assert "ES 9014" in html
assert "checking exact timetable" in html assert "checking exact timetable" in html
assert "/api/results_refresh/BRI/paris/2026-06-22" in html assert "/api/results_refresh/BRI/paris/out/2026-06-22" in html
assert "refreshFullResults()" in html assert "refreshFullResults()" in html
assert "window.location.reload()" not in html assert "window.location.reload()" not in html
assert "Checking Eurostar price" in html assert "Checking Eurostar price" in html
@ -268,7 +279,7 @@ def test_results_refresh_reloads_when_exact_timetable_differs(monkeypatch: Any)
) )
client = _client() client = _client()
resp = client.get("/api/results_refresh/BRI/paris/2026-06-22") resp = client.get("/api/results_refresh/BRI/paris/out/2026-06-22")
body = resp.get_data(as_text=True) body = resp.get_data(as_text=True)
assert resp.status_code == 200 assert resp.status_code == 200
@ -341,7 +352,7 @@ def test_results_refresh_streams_prices_when_timetable_matches(
) )
client = _client() client = _client()
resp = client.get("/api/results_refresh/BRI/paris/2026-06-22") resp = client.get("/api/results_refresh/BRI/paris/out/2026-06-22")
body = resp.get_data(as_text=True) body = resp.get_data(as_text=True)
assert resp.status_code == 200 assert resp.status_code == 200
@ -362,7 +373,7 @@ def test_results_progressive_shell_loads_without_scraping(monkeypatch: Any) -> N
monkeypatch.setattr(gwr_fares_scraper, "fetch", fail_fetch) monkeypatch.setattr(gwr_fares_scraper, "fetch", fail_fetch)
client = _client() client = _client()
resp = client.get("/results/BRI/paris/2026-04-10?progressive=1") resp = client.get("/results/BRI/paris/out/2026-04-10?progressive=1")
html = resp.get_data(as_text=True) html = resp.get_data(as_text=True)
assert resp.status_code == 200 assert resp.status_code == 200
@ -379,7 +390,7 @@ def test_return_progressive_shell_formats_return_date(monkeypatch: Any) -> None:
monkeypatch.setattr(gwr_fares_scraper, "fetch", fail_fetch) monkeypatch.setattr(gwr_fares_scraper, "fetch", fail_fetch)
client = _client() client = _client()
resp = client.get("/results/BRI/paris/2026-04-10/return/2026-04-17?progressive=1") resp = client.get("/results/BRI/paris/return/2026-04-10/2026-04-17?progressive=1")
html = resp.get_data(as_text=True) html = resp.get_data(as_text=True)
assert resp.status_code == 200 assert resp.status_code == 200
@ -392,7 +403,7 @@ def test_results_title_and_social_meta_include_destination(monkeypatch: Any) ->
client = _client() client = _client()
resp = client.get( resp = client.get(
"/results/BRI/lille/2026-04-10?min_connection=60&max_connection=120" "/results/BRI/lille/out/2026-04-10?min_connection=60&max_connection=120"
) )
html = resp.get_data(as_text=True) html = resp.get_data(as_text=True)
@ -407,7 +418,7 @@ def test_results_title_and_social_meta_include_destination(monkeypatch: Any) ->
'to Lille Europe on Friday 10 April 2026 via Paddington, St Pancras, and Eurostar.">' 'to Lille Europe on Friday 10 April 2026 via Paddington, St Pancras, and Eurostar.">'
) in html ) in html
assert ( assert (
'<meta property="og:url" content="http://localhost/results/BRI/lille/2026-04-10?min_connection=60&amp;max_connection=120">' '<meta property="og:url" content="http://localhost/results/BRI/lille/out/2026-04-10?min_connection=60&amp;max_connection=120">'
in html in html
) )
@ -498,7 +509,7 @@ def test_results_marks_trips_within_five_minutes_of_fastest_and_slowest(
client = _client() client = _client()
resp = client.get( resp = client.get(
"/results/BRI/paris/2026-04-10?min_connection=60&max_connection=120" "/results/BRI/paris/out/2026-04-10?min_connection=60&max_connection=120"
) )
html = resp.get_data(as_text=True) html = resp.get_data(as_text=True)
@ -567,7 +578,7 @@ def test_results_shows_only_pre_first_reachable_unreachable_services(
client = _client() client = _client()
resp = client.get( resp = client.get(
"/results/BRI/paris/2026-04-10?min_connection=60&max_connection=120" "/results/BRI/paris/out/2026-04-10?min_connection=60&max_connection=120"
) )
html = resp.get_data(as_text=True) html = resp.get_data(as_text=True)
@ -583,7 +594,7 @@ def test_results_shows_eurostar_price_and_total(monkeypatch: Any) -> None:
client = _client() client = _client()
resp = client.get( resp = client.get(
"/results/BRI/paris/2026-04-10?min_connection=60&max_connection=120" "/results/BRI/paris/out/2026-04-10?min_connection=60&max_connection=120"
) )
html = resp.get_data(as_text=True) html = resp.get_data(as_text=True)
@ -640,7 +651,7 @@ def test_results_uses_unique_row_keys_for_same_eurostar(monkeypatch: Any) -> Non
client = _client() client = _client()
resp = client.get( resp = client.get(
"/results/BRI/paris/2026-04-10?min_connection=60&max_connection=120" "/results/BRI/paris/out/2026-04-10?min_connection=60&max_connection=120"
) )
html = resp.get_data(as_text=True) html = resp.get_data(as_text=True)
@ -685,7 +696,7 @@ def test_results_shows_unreachable_service_when_no_trips(monkeypatch: Any) -> No
client = _client() client = _client()
resp = client.get( resp = client.get(
"/results/BRI/paris/2026-04-10?min_connection=60&max_connection=120" "/results/BRI/paris/out/2026-04-10?min_connection=60&max_connection=120"
) )
html = resp.get_data(as_text=True) html = resp.get_data(as_text=True)
@ -703,7 +714,7 @@ def test_results_shows_eurostar_plus_price(monkeypatch: Any) -> None:
client = _client() client = _client()
resp = client.get( resp = client.get(
"/results/BRI/paris/2026-04-10?min_connection=60&max_connection=120" "/results/BRI/paris/out/2026-04-10?min_connection=60&max_connection=120"
) )
html = resp.get_data(as_text=True) html = resp.get_data(as_text=True)
@ -718,7 +729,7 @@ def test_results_selectors_present(monkeypatch: Any) -> None:
client = _client() client = _client()
resp = client.get( resp = client.get(
"/results/BRI/paris/2026-04-10?min_connection=60&max_connection=120" "/results/BRI/paris/out/2026-04-10?min_connection=60&max_connection=120"
) )
html = resp.get_data(as_text=True) html = resp.get_data(as_text=True)
@ -775,7 +786,7 @@ def test_results_preloads_cached_advance_fares(monkeypatch: Any) -> None:
client = _client() client = _client()
resp = client.get( resp = client.get(
"/results/BRI/paris/2026-04-10?min_connection=60&max_connection=120" "/results/BRI/paris/out/2026-04-10?min_connection=60&max_connection=120"
) )
html = resp.get_data(as_text=True) html = resp.get_data(as_text=True)
@ -827,7 +838,7 @@ def test_results_inbound_uses_reverse_legs(monkeypatch: Any) -> None:
) )
client = _client() client = _client()
resp = client.get("/results/BRI/paris/2026-04-10?journey_type=inbound") resp = client.get("/results/BRI/paris/back/2026-04-10")
html = resp.get_data(as_text=True) html = resp.get_data(as_text=True)
assert resp.status_code == 200 assert resp.status_code == 200
@ -917,7 +928,7 @@ def test_results_return_renders_outbound_and_inbound_tables(monkeypatch: Any) ->
) )
client = _client() client = _client()
resp = client.get("/results/BRI/paris/2026-04-10/return/2026-04-17") resp = client.get("/results/BRI/paris/return/2026-04-10/2026-04-17")
html = resp.get_data(as_text=True) html = resp.get_data(as_text=True)
assert resp.status_code == 200 assert resp.status_code == 200
@ -925,11 +936,11 @@ def test_results_return_renders_outbound_and_inbound_tables(monkeypatch: Any) ->
assert "Return: Paris Gare du Nord &rarr; Bristol Temple Meads" in html assert "Return: Paris Gare du Nord &rarr; Bristol Temple Meads" in html
assert "Friday 10 April 2026" in html assert "Friday 10 April 2026" in html
assert "Friday 17 April 2026" in html assert "Friday 17 April 2026" in html
assert "/results/BRI/paris/2026-04-09/return/2026-04-17" in html assert "/results/BRI/paris/return/2026-04-09/2026-04-17" in html
assert "/results/BRI/paris/2026-04-11/return/2026-04-17" in html assert "/results/BRI/paris/return/2026-04-11/2026-04-17" in html
assert "/results/BRI/paris/2026-04-10/return/2026-04-16" in html assert "/results/BRI/paris/return/2026-04-10/2026-04-16" in html
assert "/results/BRI/paris/2026-04-10/return/2026-04-18" in html assert "/results/BRI/paris/return/2026-04-10/2026-04-18" in html
assert "/results/BRI/paris/2026-04-10/return/2026-04-17" in html assert "/results/BRI/paris/return/2026-04-10/2026-04-17" in html
assert "journey_type=return" not in html assert "journey_type=return" not in html
assert "return_date=2026-04-17" not in html assert "return_date=2026-04-17" not in html
assert "Circle 09:10 &rarr; KX 09:25" in html assert "Circle 09:10 &rarr; KX 09:25" in html

View file

@ -244,7 +244,7 @@ def test_single_advance_standard_totals_after_click(single_server: str) -> None:
browser = _launch_browser(p) browser = _launch_browser(p)
page = browser.new_page() page = browser.new_page()
page.goto( page.goto(
f"{single_server}/results/BRI/paris/2026-07-20", f"{single_server}/results/BRI/paris/out/2026-07-20",
wait_until="domcontentloaded", wait_until="domcontentloaded",
) )
@ -344,7 +344,7 @@ def test_single_next_date_advance_standard_labels_unreachable_rows(
page = browser.new_page() page = browser.new_page()
page.goto( page.goto(
f"http://127.0.0.1:{server.server_port}" f"http://127.0.0.1:{server.server_port}"
"/results/BRI/brussels/2026-06-16", "/results/BRI/brussels/out/2026-06-16",
wait_until="domcontentloaded", wait_until="domcontentloaded",
) )
page.get_by_role("link", name="Next →").click() page.get_by_role("link", name="Next →").click()
@ -372,7 +372,7 @@ def test_single_advance_standard_premier_totals_on_initial_url(
browser = _launch_browser(p) browser = _launch_browser(p)
page = browser.new_page() page = browser.new_page()
page.goto( page.goto(
f"{single_server}/results/BRI/paris/2026-07-20" f"{single_server}/results/BRI/paris/out/2026-07-20"
"?nr_class=advance_std&es_class=plus", "?nr_class=advance_std&es_class=plus",
wait_until="domcontentloaded", wait_until="domcontentloaded",
) )
@ -452,7 +452,7 @@ def test_single_advance_first_falls_back_to_walkon_when_unavailable(
page = browser.new_page() page = browser.new_page()
page.goto( page.goto(
f"http://127.0.0.1:{server.server_port}" f"http://127.0.0.1:{server.server_port}"
"/results/BRI/paris/2026-07-20?nr_class=advance_1st&es_class=standard", "/results/BRI/paris/out/2026-07-20?nr_class=advance_1st&es_class=standard",
wait_until="domcontentloaded", wait_until="domcontentloaded",
) )
@ -496,7 +496,7 @@ def test_return_advance_first_standard_premier_totals(local_server: str) -> None
timeout=10000, timeout=10000,
) )
assert "/results/BRI/paris/2026-07-20/return/2026-07-27" in page.url assert "/results/BRI/paris/return/2026-07-20/2026-07-27" in page.url
assert "journey_type=return" not in page.url assert "journey_type=return" not in page.url
assert "return_date=2026-07-27" not in page.url assert "return_date=2026-07-27" not in page.url
assert "nr_class=advance_1st" in page.url assert "nr_class=advance_1st" in page.url
@ -528,7 +528,7 @@ def test_return_calendar_selects_outbound_before_return(local_server: str) -> No
assert "Return: Wed 17 Jun" in page.locator("#cal-hint").inner_text() assert "Return: Wed 17 Jun" in page.locator("#cal-hint").inner_text()
page.locator('button[type="submit"]').click() page.locator('button[type="submit"]').click()
page.wait_for_url("**/results/BRI/paris/2026-06-10/return/2026-06-17", timeout=10000) page.wait_for_url("**/results/BRI/paris/return/2026-06-10/2026-06-17", timeout=10000)
browser.close() browser.close()
@ -539,7 +539,7 @@ def test_return_advance_first_standard_premier_totals_on_initial_url(
browser = _launch_browser(p) browser = _launch_browser(p)
page = browser.new_page() page = browser.new_page()
page.goto( page.goto(
f"{local_server}/results/BRI/paris/2026-07-20/return/2026-07-27" f"{local_server}/results/BRI/paris/return/2026-07-20/2026-07-27"
"?nr_class=advance_1st&es_class=plus", "?nr_class=advance_1st&es_class=plus",
wait_until="domcontentloaded", wait_until="domcontentloaded",
) )