diff --git a/agenda/__init__.py b/agenda/__init__.py index a969e5b..4fca2b0 100644 --- a/agenda/__init__.py +++ b/agenda/__init__.py @@ -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: diff --git a/agenda/busy.py b/agenda/busy.py index 711523a..91233ad 100644 --- a/agenda/busy.py +++ b/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 diff --git a/templates/weekends.html b/templates/weekends.html index 8878fdf..91f207c 100644 --- a/templates/weekends.html +++ b/templates/weekends.html @@ -11,7 +11,9 @@ Week Date Saturday + Saturday Location Sunday + Sunday Location @@ -43,6 +45,14 @@ free {% endif %} + {% if extra_class %}{% else %}{% endif %} + {% set city, country = weekend[day + '_location'] %} + {% if city == "Bristol" and country.alpha_2 | upper == "GB" %} + home + {% else %} + {{ city }}, {{ country.flag }} {{ country.name }} + {% endif %} + {% endfor %} {% endfor %} diff --git a/tests/test_busy.py b/tests/test_busy.py new file mode 100644 index 0000000..b267997 --- /dev/null +++ b/tests/test_busy.py @@ -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" diff --git a/web_view.py b/web_view.py index 1bdd9e3..2cffe28 100755 --- a/web_view.py +++ b/web_view.py @@ -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,