Link to Eurostar search page with pricing; add Bristol departures RTT link

Replace the Eurostar timetable link with the search URL
(eurostar.com/search/uk-en?adult=1&origin=…&destination=…&outbound=…)
so the footer links directly to the page that shows prices for the
specific date and destination.

Add a Bristol Temple Meads → Paddington departures link on RTT alongside
the existing Paddington arrivals link.

Also update "morning service unavailable" badge and tests to reflect the
removal of the morning-only cutoff filter from find_unreachable_morning_eurostars.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Edward Betts 2026-04-04 10:47:46 +01:00
parent 4e4202e220
commit 06afd57957
5 changed files with 27 additions and 8 deletions

10
app.py
View file

@ -16,6 +16,12 @@ RTT_PADDINGTON_URL = (
"?stp=WVS&show=pax-calls&order=wtt" "?stp=WVS&show=pax-calls&order=wtt"
) )
RTT_BRISTOL_URL = (
"https://www.realtimetrains.co.uk/search/detailed/"
"gb-nr:BRI/to/gb-nr:PAD/{date}/0000-2359"
"?stp=WVS&show=pax-calls&order=wtt"
)
app = Flask(__name__) app = Flask(__name__)
DESTINATIONS = { DESTINATIONS = {
@ -154,8 +160,9 @@ def results(slug, travel_date):
next_date = (dt + timedelta(days=1)).isoformat() next_date = (dt + timedelta(days=1)).isoformat()
travel_date_display = dt.strftime('%A %-d %B %Y') travel_date_display = dt.strftime('%A %-d %B %Y')
eurostar_url = eurostar_scraper.timetable_url(destination) + f"?date={travel_date}" eurostar_url = eurostar_scraper.search_url(destination, travel_date)
rtt_url = RTT_PADDINGTON_URL.format(date=travel_date) rtt_url = RTT_PADDINGTON_URL.format(date=travel_date)
rtt_bristol_url = RTT_BRISTOL_URL.format(date=travel_date)
return render_template( return render_template(
'results.html', 'results.html',
@ -175,6 +182,7 @@ def results(slug, travel_date):
error=error, error=error,
eurostar_url=eurostar_url, eurostar_url=eurostar_url,
rtt_url=rtt_url, rtt_url=rtt_url,
rtt_bristol_url=rtt_bristol_url,
min_connection=min_connection, min_connection=min_connection,
max_connection=max_connection, max_connection=max_connection,
valid_min_connections=sorted(VALID_MIN_CONNECTIONS), valid_min_connections=sorted(VALID_MIN_CONNECTIONS),

View file

@ -48,6 +48,14 @@ def _slugify_station_name(name: str) -> str:
return re.sub(r'[^a-z0-9]+', '-', name.lower()).strip('-') return re.sub(r'[^a-z0-9]+', '-', name.lower()).strip('-')
def search_url(destination: str, travel_date: str) -> str:
dest_id = DESTINATION_STATION_IDS[destination]
return (
f'https://www.eurostar.com/search/uk-en'
f'?adult=1&origin={ORIGIN_STATION_ID}&destination={dest_id}&outbound={travel_date}'
)
def timetable_url(destination: str) -> str: def timetable_url(destination: str) -> str:
dest_id = DESTINATION_STATION_IDS[destination] dest_id = DESTINATION_STATION_IDS[destination]
dest_slug = _slugify_station_name(destination) dest_slug = _slugify_station_name(destination)

View file

@ -83,7 +83,7 @@
{% if unreachable_morning_services %} {% if unreachable_morning_services %}
&nbsp;&middot;&nbsp; &nbsp;&middot;&nbsp;
<span style="color:#718096"> <span style="color:#718096">
{{ unreachable_morning_services | length }} morning service{{ 's' if unreachable_morning_services | length != 1 }} unavailable from Bristol {{ unreachable_morning_services | length }} Eurostar service{{ 's' if unreachable_morning_services | length != 1 }} unavailable from Bristol
</span> </span>
{% endif %} {% endif %}
{% if from_cache %} {% if from_cache %}
@ -209,6 +209,8 @@
Eurostar Standard prices are for 1 adult in GBP; always check Eurostar Standard prices are for 1 adult in GBP; always check
<a href="{{ eurostar_url }}" target="_blank" rel="noopener">eurostar.com</a> to book. <a href="{{ eurostar_url }}" target="_blank" rel="noopener">eurostar.com</a> to book.
&nbsp;&middot;&nbsp; &nbsp;&middot;&nbsp;
<a href="{{ rtt_bristol_url }}" target="_blank" rel="noopener">Bristol departures on RTT</a>
&nbsp;&middot;&nbsp;
<a href="{{ rtt_url }}" target="_blank" rel="noopener">Paddington arrivals on RTT</a> <a href="{{ rtt_url }}" target="_blank" rel="noopener">Paddington arrivals on RTT</a>
</p> </p>

View file

@ -211,11 +211,10 @@ def test_results_shows_unreachable_morning_eurostar_services(monkeypatch):
html = resp.get_data(as_text=True) html = resp.get_data(as_text=True)
assert resp.status_code == 200 assert resp.status_code == 200
assert '1 morning service unavailable from Bristol' in html assert '2 Eurostar services unavailable from Bristol' in html
assert '09:30' in html assert '09:30' in html
assert 'ES 9001' in html assert 'ES 9001' in html
assert 'Unavailable from Bristol' in html assert 'Unavailable from Bristol' in html
assert 'Morning means Eurostar departures before 12:00 from St&nbsp;Pancras.' in html
assert html.index('09:30') < html.index('10:15') assert html.index('09:30') < html.index('10:15')
@ -267,6 +266,6 @@ def test_results_can_show_only_unreachable_morning_services(monkeypatch):
assert resp.status_code == 200 assert resp.status_code == 200
assert 'No valid journeys found.' not in html assert 'No valid journeys found.' not in html
assert '1 morning service unavailable from Bristol' in html assert '1 Eurostar service unavailable from Bristol' in html
assert '09:30' in html assert '09:30' in html
assert 'Unavailable from Bristol' in html assert 'Unavailable from Bristol' in html

View file

@ -131,7 +131,9 @@ def test_connection_duration_in_trip():
assert trips[0]['connection_duration'] == '1h 16m' assert trips[0]['connection_duration'] == '1h 16m'
def test_unreachable_morning_eurostars_lists_only_unreachable_morning_services(): def test_find_unreachable_eurostars_excludes_connectable_services():
# GWR arrives 08:45; default min=50/max=110 → viable window 09:3510:35.
# 09:30 too early, 10:15 connectable, 12:30 beyond max connection.
gwr = [ gwr = [
{'depart_bristol': '07:00', 'arrive_paddington': '08:45'}, {'depart_bristol': '07:00', 'arrive_paddington': '08:45'},
] ]
@ -143,7 +145,7 @@ def test_unreachable_morning_eurostars_lists_only_unreachable_morning_services()
unreachable = find_unreachable_morning_eurostars(gwr, eurostar, DATE) unreachable = find_unreachable_morning_eurostars(gwr, eurostar, DATE)
assert [service['depart_st_pancras'] for service in unreachable] == ['09:30'] assert [s['depart_st_pancras'] for s in unreachable] == ['09:30', '12:30']
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -205,7 +207,7 @@ def test_combine_trips_includes_ticket_fields():
assert 'ticket_code' in t assert 'ticket_code' in t
def test_unreachable_morning_eurostars_returns_empty_when_morning_service_is_reachable(): def test_find_unreachable_eurostars_returns_empty_when_all_connectable():
gwr = [ gwr = [
{'depart_bristol': '07:00', 'arrive_paddington': '08:45'}, {'depart_bristol': '07:00', 'arrive_paddington': '08:45'},
] ]