Add location tracking to weekend page

Weekend page now shows specific location (city and country) for each
Saturday and Sunday based on travel history:

- Analyzes flight arrivals and accommodation check-ins to determine exact location
- Shows "home" when at Bristol, UK
- Shows "City, 🏴 Country" format when traveling
- Handles multi-location trips by finding most recent travel within trip period
- Optimized to parse YAML files once instead of per-date lookup

Closes #191

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Edward Betts 2025-07-16 03:59:35 +02:00
parent f41930f811
commit af492750cb
5 changed files with 235 additions and 6 deletions

View file

@ -22,8 +22,10 @@ def format_list_with_ampersand(items: list[str]) -> str:
return ""
def get_country(alpha_2: str) -> pycountry.db.Country | None:
def get_country(alpha_2: str|None) -> pycountry.db.Country | None:
"""Lookup country by alpha-2 country code."""
if not alpha_2:
return None
if alpha_2.count(",") > 3: # ESA
return pycountry.db.Country(flag="🇪🇺", name="ESA")
if not alpha_2:

View file

@ -2,11 +2,12 @@
import itertools
import typing
from datetime import date, timedelta
from datetime import date, datetime, timedelta
import flask
import pycountry
from . import events_yaml
from . import events_yaml, get_country, travel
from .event import Event
from .types import StrDict, Trip
@ -80,7 +81,180 @@ def get_busy_events(
return busy_events
def weekends(start: date, busy_events: list[Event]) -> typing.Sequence[StrDict]:
def get_location_for_date(
target_date: date,
trips: list[Trip],
bookings: list[dict],
accommodations: list[dict],
airports: dict
) -> tuple[str, pycountry.db.Country | None]:
"""Get location (city, country) for a specific date using travel history."""
# UK airports that indicate being home
uk_airports = {"LHR", "LGW", "STN", "LTN", "BRS", "BHX", "MAN", "EDI", "GLA"}
# First check if currently on a trip
for trip in trips:
if trip.start <= target_date <= (trip.end or trip.start):
# For trips, find the most recent flight or accommodation within the trip period
# to determine exact location on the target date
trip_most_recent_date = None
trip_most_recent_location = None
# Check flights within trip period
for booking in bookings:
for flight in booking.get("flights", []):
if "arrive" in flight:
arrive_date = flight["arrive"]
if hasattr(arrive_date, "date"):
arrive_date = arrive_date.date()
elif isinstance(arrive_date, str):
arrive_date = datetime.fromisoformat(
arrive_date.replace("Z", "+00:00")
).date()
# Only consider flights within this trip and before target date
if trip.start <= arrive_date <= target_date:
if (
trip_most_recent_date is None
or arrive_date > trip_most_recent_date
):
trip_most_recent_date = arrive_date
destination_airport = flight["to"]
if destination_airport in uk_airports:
trip_most_recent_location = (
"Bristol",
get_country("gb"),
)
else:
airport_info = airports.get(destination_airport)
if airport_info:
location_name = airport_info.get(
"city",
airport_info.get(
"name", destination_airport
),
)
trip_most_recent_location = (
location_name,
get_country(
airport_info.get("country", "gb")
),
)
# Check accommodations within trip period
for acc in accommodations:
if "from" in acc:
acc_date = acc["from"]
if hasattr(acc_date, "date"):
acc_date = acc_date.date()
elif isinstance(acc_date, str):
acc_date = datetime.fromisoformat(
acc_date.replace("Z", "+00:00")
).date()
# Only consider accommodations within this trip and before/on target date
if trip.start <= acc_date <= target_date:
# Accommodation takes precedence over flights on the same date
# or if it's genuinely more recent
if (
trip_most_recent_date is None
or acc_date > trip_most_recent_date
or acc_date == trip_most_recent_date
):
trip_most_recent_date = acc_date
if acc.get("country") == "gb":
trip_most_recent_location = (
"Bristol",
get_country("gb"),
)
else:
trip_most_recent_location = (
acc.get("location", "Unknown"),
get_country(acc.get("country", "gb")),
)
# Return the most recent location within the trip, or fallback to first trip location
if trip_most_recent_location:
return trip_most_recent_location
# Fallback to first location if no specific location found
locations = trip.locations()
if locations:
city, country = locations[0]
return (city, country)
# Find most recent flight or accommodation before this date
most_recent_location = None
most_recent_date = None
# Check flights
for booking in bookings:
for flight in booking.get("flights", []):
if "arrive" in flight:
arrive_date = flight["arrive"]
if hasattr(arrive_date, "date"):
arrive_date = arrive_date.date()
elif isinstance(arrive_date, str):
arrive_date = datetime.fromisoformat(
arrive_date.replace("Z", "+00:00")
).date()
if arrive_date <= target_date:
if most_recent_date is None or arrive_date > most_recent_date:
most_recent_date = arrive_date
destination_airport = flight["to"]
# If arriving at UK airport, assume back home in Bristol
if destination_airport in uk_airports:
most_recent_location = ("Bristol", get_country("gb"))
else:
# Get destination airport location for non-UK arrivals
airport_info = airports.get(destination_airport)
if airport_info:
location_name = airport_info.get(
"city",
airport_info.get("name", destination_airport),
)
most_recent_location = (
location_name,
get_country(airport_info.get("country", "gb")),
)
# Check accommodation - only override if accommodation is more recent
for acc in accommodations:
if "from" in acc:
acc_date = acc["from"]
if hasattr(acc_date, "date"):
acc_date = acc_date.date()
elif isinstance(acc_date, str):
acc_date = datetime.fromisoformat(
acc_date.replace("Z", "+00:00")
).date()
if acc_date <= target_date:
# Only update if this accommodation is more recent than existing result
if most_recent_date is None or acc_date > most_recent_date:
most_recent_date = acc_date
# For UK accommodation, use Bristol as location
if acc.get("country") == "gb":
most_recent_location = ("Bristol", get_country("gb"))
else:
most_recent_location = (
acc.get("location", "Unknown"),
get_country(acc.get("country", "gb")),
)
# Return most recent location or default to Bristol
if most_recent_location:
return most_recent_location
return ("Bristol", get_country("gb"))
def weekends(
start: date, busy_events: list[Event], trips: list[Trip], data_dir: str
) -> typing.Sequence[StrDict]:
"""Next ten weekends."""
weekday = start.weekday()
@ -90,6 +264,11 @@ def weekends(start: date, busy_events: list[Event]) -> typing.Sequence[StrDict]:
else:
start_date = start + timedelta(days=(5 - weekday))
# Parse YAML files once for all location lookups
bookings = travel.parse_yaml("flights", data_dir)
accommodations = travel.parse_yaml("accommodation", data_dir)
airports = travel.parse_yaml("airports", data_dir)
weekends_info = []
for i in range(52):
saturday = start_date + timedelta(weeks=i)
@ -106,8 +285,17 @@ def weekends(start: date, busy_events: list[Event]) -> typing.Sequence[StrDict]:
if event.end_date and event.as_date <= sunday <= event.end_as_date
]
saturday_location = get_location_for_date(saturday, trips, bookings, accommodations, airports)
sunday_location = get_location_for_date(sunday, trips, bookings, accommodations, airports)
weekends_info.append(
{"date": saturday, "saturday": saturday_events, "sunday": sunday_events}
{
"date": saturday,
"saturday": saturday_events,
"sunday": sunday_events,
"saturday_location": saturday_location,
"sunday_location": sunday_location,
}
)
return weekends_info

View file

@ -11,7 +11,9 @@
<th class="text-end">Week</th>
<th class="text-end">Date</th>
<th>Saturday</th>
<th>Saturday Location</th>
<th>Sunday</th>
<th>Sunday Location</th>
</tr>
</thead>
<tbody>
@ -43,6 +45,14 @@
<strong>free</strong>
{% endif %}
</td>
{% if extra_class %}<td class="{{ extra_class|trim }}">{% else %}<td>{% endif %}
{% set city, country = weekend[day + '_location'] %}
{% if city == "Bristol" and country.alpha_2 | upper == "GB" %}
<strong>home</strong>
{% else %}
{{ city }}, {{ country.flag }} {{ country.name }}
{% endif %}
</td>
{% endfor %}
</tr>
{% endfor %}

29
tests/test_busy.py Normal file
View file

@ -0,0 +1,29 @@
from datetime import date, datetime
import agenda.trip
from web_view import app
def test_get_location_for_date() -> None:
app.config["SERVER_NAME"] = "test"
with app.app_context():
today = datetime.now().date()
start = date(today.year, 1, 1)
trips = [
t for t in agenda.trip.build_trip_list() if t.start == date(2025, 2, 9)
]
assert len(trips) == 1
data_dir = app.config["PERSONAL_DATA"]
# Parse YAML files once for the test
import agenda.travel as travel
bookings = travel.parse_yaml("flights", data_dir)
accommodations = travel.parse_yaml("accommodation", data_dir)
airports = travel.parse_yaml("airports", data_dir)
l1 = agenda.busy.get_location_for_date(date(2025, 2, 15), trips, bookings, accommodations, airports)
assert l1[0] == "Hackettstown"
l2 = agenda.busy.get_location_for_date(date(2025, 7, 1), trips, bookings, accommodations, airports)
assert l2[0] == "Bristol"

View file

@ -258,7 +258,7 @@ async def weekends() -> str:
trip_list = agenda.trip.build_trip_list()
busy_events = agenda.busy.get_busy_events(start, app.config, trip_list)
weekends = agenda.busy.weekends(start, busy_events)
weekends = agenda.busy.weekends(start, busy_events, trip_list, app.config["PERSONAL_DATA"])
return flask.render_template(
"weekends.html",
items=weekends,