Split return date nav into separate outbound/return rows; show earlier tube option on inbound
For return journeys, replace the single combined date navigation row with two separate rows so outbound and return dates can be adjusted independently. For inbound underground options, show one service before the earliest catchable (as an "aim for this" option) rather than the next service after it, which often arrived too late to connect with the GWR train. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
1407cb8246
commit
a859b96a23
6 changed files with 82 additions and 22 deletions
30
app.py
30
app.py
|
|
@ -681,6 +681,32 @@ def _results(station_crs, slug, travel_date, journey_type, return_date):
|
|||
next_date,
|
||||
**{**common_url_args, "return_date": next_return_date},
|
||||
)
|
||||
prev_outbound_url = _results_url(
|
||||
station_crs, slug, prev_date, **common_url_args
|
||||
)
|
||||
next_outbound_url = _results_url(
|
||||
station_crs, slug, next_date, **common_url_args
|
||||
)
|
||||
prev_return_url = (
|
||||
_results_url(
|
||||
station_crs,
|
||||
slug,
|
||||
travel_date,
|
||||
**{**common_url_args, "return_date": prev_return_date},
|
||||
)
|
||||
if return_date
|
||||
else None
|
||||
)
|
||||
next_return_url = (
|
||||
_results_url(
|
||||
station_crs,
|
||||
slug,
|
||||
travel_date,
|
||||
**{**common_url_args, "return_date": next_return_date},
|
||||
)
|
||||
if return_date
|
||||
else None
|
||||
)
|
||||
destination_links = [
|
||||
(
|
||||
destination_slug,
|
||||
|
|
@ -775,6 +801,10 @@ def _results(station_crs, slug, travel_date, journey_type, return_date):
|
|||
next_date=next_date,
|
||||
prev_results_url=prev_results_url,
|
||||
next_results_url=next_results_url,
|
||||
prev_outbound_url=prev_outbound_url,
|
||||
next_outbound_url=next_outbound_url,
|
||||
prev_return_url=prev_return_url,
|
||||
next_return_url=next_return_url,
|
||||
destination_links=destination_links,
|
||||
results_base_url=results_base_url,
|
||||
travel_date_display=travel_date_display,
|
||||
|
|
|
|||
|
|
@ -165,13 +165,17 @@ def next_service(
|
|||
|
||||
|
||||
def upcoming_services(
|
||||
earliest_board: datetime, count: int = 2, direction: str = 'pad_to_kx'
|
||||
earliest_board: datetime,
|
||||
count: int = 2,
|
||||
direction: str = 'pad_to_kx',
|
||||
preceding: int = 0,
|
||||
) -> list[tuple[datetime, datetime]]:
|
||||
"""
|
||||
Return up to *count* Circle line services for *direction*, starting from
|
||||
*earliest_board*.
|
||||
Return Circle line services for *direction* around *earliest_board*.
|
||||
|
||||
Each element is (depart_origin, arrive_destination) as datetimes.
|
||||
Returns up to *preceding* services before earliest_board followed by up to
|
||||
*count* services at or after earliest_board. Each element is
|
||||
(depart_origin, arrive_destination) as datetimes.
|
||||
"""
|
||||
timetable = _get_timetable().get(direction, {})[_day_type(earliest_board.weekday())]
|
||||
board_secs = (
|
||||
|
|
@ -180,13 +184,17 @@ def upcoming_services(
|
|||
+ earliest_board.second
|
||||
)
|
||||
midnight = earliest_board.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
pre_results = []
|
||||
results = []
|
||||
for pad_secs, kxp_secs in timetable:
|
||||
if pad_secs >= board_secs:
|
||||
results.append((
|
||||
midnight + timedelta(seconds=pad_secs),
|
||||
midnight + timedelta(seconds=kxp_secs),
|
||||
))
|
||||
entry = (
|
||||
midnight + timedelta(seconds=pad_secs),
|
||||
midnight + timedelta(seconds=kxp_secs),
|
||||
)
|
||||
if pad_secs < board_secs:
|
||||
pre_results.append(entry)
|
||||
else:
|
||||
results.append(entry)
|
||||
if len(results) == count:
|
||||
break
|
||||
return results
|
||||
return pre_results[-preceding:] + results if preceding else results
|
||||
|
|
|
|||
|
|
@ -246,6 +246,7 @@
|
|||
/* Results page layout */
|
||||
.back-link { margin-bottom: 1rem; }
|
||||
.date-nav { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.5rem; }
|
||||
.date-nav-label { min-width: 6rem; font-weight: 600; font-size: 0.9rem; }
|
||||
.switcher-section { margin: 0.9rem 0 1rem; }
|
||||
.section-label { font-size: 0.9rem; font-weight: 600; margin-bottom: 0.45rem; }
|
||||
.filter-row { margin-top: 0.75rem; display: flex; gap: 1.5rem; align-items: center; flex-wrap: wrap; }
|
||||
|
|
|
|||
|
|
@ -20,13 +20,28 @@
|
|||
{{ departure_station_name }} → {{ destination }}
|
||||
{% endif %}
|
||||
</h2>
|
||||
{% if journey_type == 'return' %}
|
||||
<div class="date-nav">
|
||||
<span class="date-nav-label">Outbound:</span>
|
||||
<a href="{{ prev_outbound_url }}" class="btn-nav">← Prev</a>
|
||||
<strong>{{ travel_date_display }}</strong>
|
||||
<a href="{{ next_outbound_url }}" class="btn-nav">Next →</a>
|
||||
</div>
|
||||
<div class="date-nav">
|
||||
<span class="date-nav-label">Return:</span>
|
||||
<a href="{{ prev_return_url }}" class="btn-nav">← Prev</a>
|
||||
<strong>{{ return_date_display }}</strong>
|
||||
<a href="{{ next_return_url }}" class="btn-nav">Next →</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="date-nav">
|
||||
<a href="{{ prev_results_url }}"
|
||||
class="btn-nav">← Prev</a>
|
||||
<strong>{{ travel_date_display }}{% if return_date_display %} to {{ return_date_display }}{% endif %}</strong>
|
||||
<strong>{{ travel_date_display }}</strong>
|
||||
<a href="{{ next_results_url }}"
|
||||
class="btn-nav">Next →</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="switcher-section">
|
||||
<div class="section-label">Switch destination for {{ travel_date_display }}</div>
|
||||
<div class="chip-row">
|
||||
|
|
@ -496,11 +511,14 @@
|
|||
<td class="col-transfer" style="color:#4a5568">
|
||||
<span class="nowrap">{{ row.connection_duration }}{% if row.connection_minutes < 45 %} <span title="Tight connection">⚠️</span>{% endif %}</span>
|
||||
{% if row.circle_services %}
|
||||
{% set c = row.circle_services[0] %}
|
||||
<br><span class="text-xs text-muted nowrap">Circle {{ c.depart }} → PAD {{ c.arrive_pad }} · £{{ "%.2f"|format(c.fare) }}</span>
|
||||
{% if row.circle_services | length > 1 %}
|
||||
{% set c2 = row.circle_services[1] %}
|
||||
<br><span class="text-xs text-muted nowrap" style="opacity:0.7">next {{ c2.depart }} → PAD {{ c2.arrive_pad }} · £{{ "%.2f"|format(c2.fare) }}</span>
|
||||
{% set c_early = row.circle_services[0] %}
|
||||
{% set c = row.circle_services[1] %}
|
||||
<br><span class="text-xs text-muted nowrap" style="opacity:0.7">earlier {{ c_early.depart }} → PAD {{ c_early.arrive_pad }} · £{{ "%.2f"|format(c_early.fare) }}</span>
|
||||
<br><span class="text-xs text-muted nowrap">Circle {{ c.depart }} → PAD {{ c.arrive_pad }} · £{{ "%.2f"|format(c.fare) }}</span>
|
||||
{% else %}
|
||||
{% set c = row.circle_services[0] %}
|
||||
<br><span class="text-xs text-muted nowrap">Circle {{ c.depart }} → PAD {{ c.arrive_pad }} · £{{ "%.2f"|format(c.fare) }}</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
|
|
|
|||
|
|
@ -599,7 +599,7 @@ def test_results_return_renders_outbound_and_inbound_tables(monkeypatch):
|
|||
monkeypatch.setattr(
|
||||
trip_planner_module.circle_line,
|
||||
'upcoming_services',
|
||||
lambda earliest_board, count=2, direction='pad_to_kx': (
|
||||
lambda earliest_board, count=2, direction='pad_to_kx', preceding=0: (
|
||||
[
|
||||
(datetime(2026, 4, 10, 9, 10), datetime(2026, 4, 10, 9, 25)),
|
||||
(datetime(2026, 4, 10, 9, 15), datetime(2026, 4, 10, 9, 30)),
|
||||
|
|
@ -619,16 +619,19 @@ def test_results_return_renders_outbound_and_inbound_tables(monkeypatch):
|
|||
assert resp.status_code == 200
|
||||
assert 'Outbound: Bristol Temple Meads → Paris Gare du Nord' in html
|
||||
assert 'Return: Paris Gare du Nord → Bristol Temple Meads' in html
|
||||
assert 'Friday 10 April 2026 to Friday 17 April 2026' in html
|
||||
assert '/results/BRI/paris/2026-04-09/return/2026-04-16' in html
|
||||
assert '/results/BRI/paris/2026-04-11/return/2026-04-18' in html
|
||||
assert 'Friday 10 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/2026-04-11/return/2026-04-17' in html
|
||||
assert '/results/BRI/paris/2026-04-10/return/2026-04-16' in html
|
||||
assert '/results/BRI/paris/2026-04-10/return/2026-04-18' in html
|
||||
assert "/results/BRI/paris/2026-04-10/return/2026-04-17" in html
|
||||
assert 'journey_type=return' not in html
|
||||
assert 'return_date=2026-04-17' not in html
|
||||
assert 'Circle 09:10 → KX 09:25' in html
|
||||
assert 'next 09:15 → KX 09:30' in html
|
||||
assert 'Circle 16:40 → PAD 16:55' in html
|
||||
assert 'next 16:45 → PAD 17:00' in html
|
||||
assert 'earlier 16:40 → PAD 16:55' in html
|
||||
assert 'Circle 16:45 → PAD 17:00' in html
|
||||
assert 'title="Tight connection">⚠️</span>' in html
|
||||
assert 'ES 9014' in html
|
||||
assert 'ES 9035' in html
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ def _circle_line_services_to_paddington(arrive_st_pancras: datetime) -> list[dic
|
|||
earliest_board = arrive_st_pancras + timedelta(
|
||||
minutes=KX_WALK_TO_UNDERGROUND_MINUTES
|
||||
)
|
||||
services = circle_line.upcoming_services(earliest_board, count=2, direction='kx_to_pad')
|
||||
services = circle_line.upcoming_services(earliest_board, count=1, direction='kx_to_pad', preceding=1)
|
||||
return [
|
||||
{
|
||||
"depart": dep.strftime(TIME_FMT),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue