diff --git a/agenda/busy.py b/agenda/busy.py index 8452903..4fcbeec 100644 --- a/agenda/busy.py +++ b/agenda/busy.py @@ -11,6 +11,20 @@ from . import events_yaml, get_country, travel from .event import Event from .types import StrDict, Trip +NEARBY_BALKAN_COUNTRIES = { + "GR", + "AL", + "XK", + "HR", + "SI", + "MK", + "BA", + "ME", + "RS", + "BG", + "RO", +} + def busy_event(e: Event) -> bool: """Busy.""" @@ -92,20 +106,58 @@ def _parse_datetime_field(datetime_obj: datetime | date) -> tuple[datetime, date raise ValueError(f"Invalid datetime format: {datetime_obj}") +def _get_airport_location( + airport_code: str, airports: StrDict, uk_airports: set[str], on_trip: bool = False +) -> tuple[str | None, pycountry.db.Country | None]: + """Get location from airport code.""" + if airport_code in uk_airports: + if on_trip: + # When on a trip, show the actual location even for UK airports + airport_info = airports.get(airport_code) + if airport_info: + location_name = airport_info.get( + "city", airport_info.get("name", "London") + ) + return (location_name, get_country("gb")) + else: + return ("London", get_country("gb")) + else: + # When not on a trip, UK airports mean home + return (None, get_country("gb")) + else: + # Non-UK airports + airport_info = airports.get(airport_code) + if airport_info: + location_name = airport_info.get( + "city", airport_info.get("name", airport_code) + ) + return (location_name, get_country(airport_info.get("country", "gb"))) + else: + return (airport_code, get_country("gb")) + + def _get_accommodation_location( acc: StrDict, on_trip: bool = False -) -> tuple[str | None, pycountry.db.Country]: +) -> tuple[str | None, pycountry.db.Country | None]: """Get location from accommodation data.""" - c = get_country(acc["country"]) - assert c - assert isinstance(acc["location"], str) - return (acc["location"] if on_trip else None, c) + if acc.get("country") == "gb": + if on_trip: + # When on a trip, show the actual location even for UK accommodations + return (acc.get("location", "London"), get_country("gb")) + else: + # When not on a trip, UK accommodation means home + return (None, get_country("gb")) + else: + return (acc.get("location", "Unknown"), get_country(acc.get("country", "gb"))) def _find_most_recent_travel_within_trip( trip: Trip, target_date: date, -) -> tuple[str | None, pycountry.db.Country] | None: + bookings: list[StrDict], + accommodations: list[StrDict], + airports: StrDict, +) -> tuple[str | None, pycountry.db.Country | None] | None: """Find the most recent travel location within a trip.""" uk_airports = {"LHR", "LGW", "STN", "LTN", "BRS", "BHX", "MAN", "EDI", "GLA"} @@ -114,54 +166,39 @@ def _find_most_recent_travel_within_trip( trip_most_recent_datetime = None # Check flights within trip period - for travel_item in trip.travel: - if travel_item["type"] == "flight" and "arrive" in travel_item: - arrive_datetime, arrive_date = _parse_datetime_field(travel_item["arrive"]) + for booking in bookings: + for flight in booking.get("flights", []): + if "arrive" in flight: + try: + arrive_datetime, arrive_date = _parse_datetime_field( + flight["arrive"] + ) + except ValueError: + continue - # Only consider flights within this trip and before target date - if not (trip.start <= arrive_date <= target_date): - continue - # Compare both date and time to handle same-day flights correctly - if ( - trip_most_recent_date is None - or arrive_date > trip_most_recent_date - or ( - arrive_date == trip_most_recent_date - and ( - trip_most_recent_datetime is None - or arrive_datetime > trip_most_recent_datetime - ) - ) - ): - trip_most_recent_date = arrive_date - trip_most_recent_datetime = arrive_datetime - destination_airport = travel_item["to"] - assert "to_airport" in travel_item - airport_info = travel_item["to_airport"] - airport_country = airport_info["country"] - if airport_country == "gb": - if destination_airport in uk_airports: - # UK airport while on trip - show actual location - location_name = airport_info.get( - "city", airport_info.get("name", "London") + # Only consider flights within this trip and before target date + if trip.start <= arrive_date <= target_date: + # Compare both date and time to handle same-day flights correctly + if ( + trip_most_recent_date is None + or arrive_date > trip_most_recent_date + or ( + arrive_date == trip_most_recent_date + and ( + trip_most_recent_datetime is None + or arrive_datetime > trip_most_recent_datetime + ) ) - trip_most_recent_location = ( - location_name, - get_country("gb"), + ): + trip_most_recent_date = arrive_date + trip_most_recent_datetime = arrive_datetime + destination_airport = flight["to"] + trip_most_recent_location = _get_airport_location( + destination_airport, airports, uk_airports, on_trip=True ) - else: - trip_most_recent_location = (None, get_country("gb")) - else: - location_name = airport_info.get( - "city", airport_info.get("name", destination_airport) - ) - trip_most_recent_location = ( - location_name, - get_country(airport_country), - ) # Check accommodations within trip period - for acc in trip.accommodation: + for acc in accommodations: if "from" in acc: try: _, acc_date = _parse_datetime_field(acc["from"]) @@ -182,93 +219,6 @@ def _find_most_recent_travel_within_trip( acc, on_trip=True ) - # Check trains within trip period - for travel_item in trip.travel: - if travel_item["type"] == "train": - for leg in travel_item.get("legs", []): - if "arrive" in leg: - try: - arrive_datetime, arrive_date = _parse_datetime_field( - leg["arrive"] - ) - except ValueError: - continue - - # Only consider trains within this trip and before target date - if trip.start <= arrive_date <= target_date: - # Compare both date and time to handle same-day arrivals correctly - if ( - trip_most_recent_date is None - or arrive_date > trip_most_recent_date - or ( - arrive_date == trip_most_recent_date - and ( - trip_most_recent_datetime is None - or arrive_datetime > trip_most_recent_datetime - ) - ) - ): - trip_most_recent_date = arrive_date - trip_most_recent_datetime = arrive_datetime - # For trains, we can get station info from to_station if available - destination = leg.get("to") - assert "to_station" in leg - station_info = leg["to_station"] - station_country = station_info["country"] - if station_country == "gb": - trip_most_recent_location = ( - destination, - get_country("gb"), - ) - else: - trip_most_recent_location = ( - destination, - get_country(station_country), - ) - - # Check ferries within trip period - for travel_item in trip.travel: - if travel_item["type"] == "ferry" and "arrive" in travel_item: - try: - arrive_datetime, arrive_date = _parse_datetime_field( - travel_item["arrive"] - ) - except ValueError: - continue - - # Only consider ferries within this trip and before target date - if trip.start <= arrive_date <= target_date: - # Compare both date and time to handle same-day arrivals correctly - if ( - trip_most_recent_date is None - or arrive_date > trip_most_recent_date - or ( - arrive_date == trip_most_recent_date - and ( - trip_most_recent_datetime is None - or arrive_datetime > trip_most_recent_datetime - ) - ) - ): - trip_most_recent_date = arrive_date - trip_most_recent_datetime = arrive_datetime - # For ferries, we can get terminal info from to_terminal if available - destination = travel_item.get("to") - assert "to_terminal" in travel_item - terminal_info = travel_item["to_terminal"] - terminal_country = terminal_info.get("country", "gb") - terminal_city = terminal_info.get("city", destination) - if terminal_country == "gb": - trip_most_recent_location = ( - terminal_city, - get_country("gb"), - ) - else: - trip_most_recent_location = ( - terminal_city, - get_country(terminal_country), - ) - return trip_most_recent_location @@ -300,7 +250,9 @@ def _get_trip_location_by_progression( def _find_most_recent_travel_before_date( target_date: date, - trips: list[Trip], + bookings: list[StrDict], + accommodations: list[StrDict], + airports: StrDict, ) -> tuple[str | None, pycountry.db.Country | None] | None: """Find the most recent travel location before a given date.""" uk_airports = {"LHR", "LGW", "STN", "LTN", "BRS", "BHX", "MAN", "EDI", "GLA"} @@ -309,14 +261,13 @@ def _find_most_recent_travel_before_date( most_recent_date = None most_recent_datetime = None - # Check all travel across all trips - for trip in trips: - # Check flights - for travel_item in trip.travel: - if travel_item["type"] == "flight" and "arrive" in travel_item: + # Check flights + for booking in bookings: + for flight in booking.get("flights", []): + if "arrive" in flight: try: arrive_datetime, arrive_date = _parse_datetime_field( - travel_item["arrive"] + flight["arrive"] ) except ValueError: continue @@ -336,167 +287,26 @@ def _find_most_recent_travel_before_date( ): most_recent_date = arrive_date most_recent_datetime = arrive_datetime - destination_airport = travel_item["to"] - # For flights, determine if we're "on trip" based on whether this is within any trip period - on_trip = any( - t.start <= arrive_date <= (t.end or t.start) for t in trips + destination_airport = flight["to"] + most_recent_location = _get_airport_location( + destination_airport, airports, uk_airports, on_trip=False ) - if "to_airport" in travel_item: - airport_info = travel_item["to_airport"] - airport_country = airport_info.get("country", "gb") - if airport_country == "gb": - if not on_trip: - # When not on a trip, UK airports mean home - most_recent_location = (None, get_country("gb")) - else: - # When on a trip, show the actual location even for UK airports - location_name = airport_info.get( - "city", airport_info.get("name", "London") - ) - most_recent_location = ( - location_name, - get_country("gb"), - ) - else: - location_name = airport_info.get( - "city", - airport_info.get("name", destination_airport), - ) - most_recent_location = ( - location_name, - get_country(airport_country), - ) - else: - most_recent_location = ( - destination_airport, - get_country("gb"), - ) + # Check accommodation - only override if accommodation is more recent + for acc in accommodations: + if "from" in acc: + try: + _, acc_date = _parse_datetime_field(acc["from"]) + except ValueError: + continue - # Check trains - elif travel_item["type"] == "train": - for leg in travel_item.get("legs", []): - if "arrive" in leg: - try: - arrive_datetime, arrive_date = _parse_datetime_field( - leg["arrive"] - ) - except ValueError: - continue - - if arrive_date <= target_date: - # Compare both date and time to handle same-day arrivals correctly - if ( - most_recent_date is None - or arrive_date > most_recent_date - or ( - arrive_date == most_recent_date - and ( - most_recent_datetime is None - or arrive_datetime > most_recent_datetime - ) - ) - ): - most_recent_date = arrive_date - most_recent_datetime = arrive_datetime - destination = leg.get("to") - on_trip = any( - t.start <= arrive_date <= (t.end or t.start) - for t in trips - ) - - if "to_station" in leg: - station_info = leg["to_station"] - station_country = station_info.get("country", "gb") - if station_country == "gb": - if not on_trip: - most_recent_location = ( - None, - get_country("gb"), - ) - else: - most_recent_location = ( - destination, - get_country("gb"), - ) - else: - most_recent_location = ( - destination, - get_country(station_country), - ) - else: - most_recent_location = ( - destination, - get_country("gb"), - ) - - # Check ferries - elif travel_item["type"] == "ferry" and "arrive" in travel_item: - try: - arrive_datetime, arrive_date = _parse_datetime_field( - travel_item["arrive"] + 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 + most_recent_location = _get_accommodation_location( + acc, on_trip=False ) - except ValueError: - continue - - if arrive_date <= target_date: - # Compare both date and time to handle same-day arrivals correctly - if ( - most_recent_date is None - or arrive_date > most_recent_date - or ( - arrive_date == most_recent_date - and ( - most_recent_datetime is None - or arrive_datetime > most_recent_datetime - ) - ) - ): - most_recent_date = arrive_date - most_recent_datetime = arrive_datetime - destination = travel_item.get("to") - on_trip = any( - t.start <= arrive_date <= (t.end or t.start) for t in trips - ) - - if "to_terminal" in travel_item: - terminal_info = travel_item["to_terminal"] - terminal_country = terminal_info.get("country", "gb") - terminal_city = terminal_info.get("city", destination) - if terminal_country == "gb": - if not on_trip: - most_recent_location = (None, get_country("gb")) - else: - most_recent_location = ( - terminal_city, - get_country("gb"), - ) - else: - most_recent_location = ( - terminal_city, - get_country(terminal_country), - ) - else: - most_recent_location = (destination, get_country("gb")) - - # Check accommodation - only override if accommodation is more recent - for acc in trip.accommodation: - if "from" in acc: - try: - _, acc_date = _parse_datetime_field(acc["from"]) - except ValueError: - continue - - 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 - on_trip = any( - t.start <= acc_date <= (t.end or t.start) for t in trips - ) - most_recent_location = _get_accommodation_location( - acc, on_trip=on_trip - ) return most_recent_location @@ -517,33 +327,49 @@ def _check_return_home_heuristic( if hasattr(final_country, "alpha_2") and final_country.alpha_2 == "GB": return (None, get_country("gb")) + # For short trips to nearby countries or international trips + # (ended >=1 day ago), assume returned home if no subsequent travel data + if days_since_trip >= 1 and ( + # European countries (close by rail/ferry) + final_alpha_2 in {"BE", "NL", "FR", "DE", "CH", "AT", "IT", "ES"} + # Nearby Balkan countries + or final_alpha_2 in NEARBY_BALKAN_COUNTRIES + # International trips (assume return home after trip ends) + or final_alpha_2 + in {"US", "CA", "IN", "JP", "CN", "AU", "NZ", "BR", "AR", "ZA"} + ): + return (None, get_country("gb")) + return None def get_location_for_date( target_date: date, trips: list[Trip], + bookings: list[StrDict], + accommodations: list[StrDict], + airports: StrDict, ) -> tuple[str | None, pycountry.db.Country | None]: """Get location (city, country) for a specific date using travel history.""" # First check if currently on a trip for trip in trips: - if not (trip.start <= target_date <= (trip.end or trip.start)): - continue - # For trips, find the most recent travel within the trip period - trip_location = _find_most_recent_travel_within_trip( - trip, - target_date, - ) - if trip_location: - return trip_location + if trip.start <= target_date <= (trip.end or trip.start): + # For trips, find the most recent flight or accommodation within the trip period + trip_location = _find_most_recent_travel_within_trip( + trip, target_date, bookings, accommodations, airports + ) + if trip_location: + return trip_location - # Fallback: determine location based on trip progression and date - progression_location = _get_trip_location_by_progression(trip, target_date) - if progression_location: - return progression_location + # Fallback: determine location based on trip progression and date + progression_location = _get_trip_location_by_progression(trip, target_date) + if progression_location: + return progression_location - # Find most recent travel before this date - recent_travel = _find_most_recent_travel_before_date(target_date, trips) + # Find most recent flight or accommodation before this date + recent_travel = _find_most_recent_travel_before_date( + target_date, bookings, accommodations, airports + ) # Check for recent trips that have ended - prioritize this over individual travel data # This handles cases where you're traveling home after a trip (e.g. stopovers, connections) @@ -570,6 +396,11 @@ def weekends( 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) @@ -587,12 +418,10 @@ def weekends( ] saturday_location = get_location_for_date( - saturday, - trips, + saturday, trips, bookings, accommodations, airports ) sunday_location = get_location_for_date( - sunday, - trips, + sunday, trips, bookings, accommodations, airports ) weekends_info.append( diff --git a/templates/trip_debug.html b/templates/trip_debug.html deleted file mode 100644 index 7ccc06e..0000000 --- a/templates/trip_debug.html +++ /dev/null @@ -1,128 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Debug: {{ trip.title }} ({{ trip.start }}) - Edward Betts{% endblock %} - -{% block style %} - -{% endblock %} - -{% block content %} -
-
-

🐛 Trip Debug Information

-

Raw trip object data for: {{ trip.title }}

- ← Back to Trip Page - -
- -
-
-

Trip Object (JSON)

-
{{ trip_json }}
-
-
-
-{% endblock %} - -{% block scripts %} - -{% endblock %} \ No newline at end of file diff --git a/tests/test_busy.py b/tests/test_busy.py index 8a12d8c..150dc8f 100644 --- a/tests/test_busy.py +++ b/tests/test_busy.py @@ -73,6 +73,9 @@ def test_specific_home_dates(travel_data): location = agenda.busy.get_location_for_date( test_date, trips, + travel_data["bookings"], + travel_data["accommodations"], + travel_data["airports"], ) assert not location[ 0 @@ -91,6 +94,9 @@ def test_specific_away_dates(travel_data): location = agenda.busy.get_location_for_date( test_date, trips, + travel_data["bookings"], + travel_data["accommodations"], + travel_data["airports"], ) assert ( location[0] == expected_city @@ -105,6 +111,9 @@ def test_get_location_for_date_basic(travel_data): location = agenda.busy.get_location_for_date( test_date, trips, + travel_data["bookings"], + travel_data["accommodations"], + travel_data["airports"], ) # Should return a tuple with (city|None, country) diff --git a/web_view.py b/web_view.py index 0f137b8..2cffe28 100755 --- a/web_view.py +++ b/web_view.py @@ -4,7 +4,6 @@ import decimal import inspect -import json import operator import os.path import sys @@ -259,9 +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, trip_list, app.config["PERSONAL_DATA"] - ) + weekends = agenda.busy.weekends(start, busy_events, trip_list, app.config["PERSONAL_DATA"]) return flask.render_template( "weekends.html", items=weekends, @@ -589,79 +586,6 @@ def trip_page(start: str) -> str: ) -@app.route("/trip//debug") -def trip_debug_page(start: str) -> str: - """Trip debug page showing raw trip object data.""" - - if not flask.g.user.is_authenticated: - flask.abort(401) - - route_distances = agenda.travel.load_route_distances(app.config["DATA_DIR"]) - trip_list = get_trip_list(route_distances) - - prev_trip, trip, next_trip = get_prev_current_and_next_trip(start, trip_list) - if not trip: - flask.abort(404) - - # Add Schengen compliance information - trip = agenda.trip_schengen.add_schengen_compliance_to_trip(trip) - - # Convert trip object to dictionary for display - trip_dict = { - "start": trip.start.isoformat(), - "name": trip.name, - "private": trip.private, - "travel": trip.travel, - "accommodation": trip.accommodation, - "conferences": trip.conferences, - "events": trip.events, - "flight_bookings": trip.flight_bookings, - "computed_properties": { - "title": trip.title, - "end": trip.end.isoformat() if trip.end else None, - "countries": [ - {"name": c.name, "alpha_2": c.alpha_2, "flag": c.flag} - for c in trip.countries - ], - "locations": [ - { - "location": loc, - "country": {"name": country.name, "alpha_2": country.alpha_2}, - } - for loc, country in trip.locations() - ], - "total_distance": trip.total_distance(), - "total_co2_kg": trip.total_co2_kg(), - "distances_by_transport_type": trip.distances_by_transport_type(), - "co2_by_transport_type": trip.co2_by_transport_type(), - }, - "schengen_compliance": ( - { - "total_days_used": trip.schengen_compliance.total_days_used, - "days_remaining": trip.schengen_compliance.days_remaining, - "is_compliant": trip.schengen_compliance.is_compliant, - "current_180_day_period": [ - trip.schengen_compliance.current_180_day_period[0].isoformat(), - trip.schengen_compliance.current_180_day_period[1].isoformat(), - ], - "days_over_limit": trip.schengen_compliance.days_over_limit, - } - if trip.schengen_compliance - else None - ), - } - - # Convert to JSON for pretty printing - trip_json = json.dumps(trip_dict, indent=2, default=str) - - return flask.render_template( - "trip_debug.html", - trip=trip, - trip_json=trip_json, - start=start, - ) - - @app.route("/holidays") def holiday_list() -> str: """List of holidays."""