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:
parent
f41930f811
commit
af492750cb
|
@ -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:
|
||||
|
|
196
agenda/busy.py
196
agenda/busy.py
|
@ -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
|
||||
|
|
|
@ -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
29
tests/test_busy.py
Normal 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"
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue