Compare commits
	
		
			6 commits
		
	
	
		
			f89d984623
			...
			2203677146
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
							
							
								
									
								
								 | 
						2203677146 | ||
| 
							
							
								
									
								
								 | 
						46091779f0 | ||
| 
							
							
								
									
								
								 | 
						29d5145b87 | ||
| 
							
							
								
									
								
								 | 
						0e2c95117c | ||
| 
							
							
								
									
								
								 | 
						f396d8a62f | ||
| 
							
							
								
									
								
								 | 
						e370049bcb | 
							
								
								
									
										467
									
								
								agenda/busy.py
									
									
									
									
									
								
							
							
						
						
									
										467
									
								
								agenda/busy.py
									
									
									
									
									
								
							| 
						 | 
				
			
			@ -11,20 +11,6 @@ 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."""
 | 
			
		||||
| 
						 | 
				
			
			@ -106,58 +92,20 @@ 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 | None]:
 | 
			
		||||
) -> tuple[str | None, pycountry.db.Country]:
 | 
			
		||||
    """Get location from accommodation data."""
 | 
			
		||||
    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")))
 | 
			
		||||
    c = get_country(acc["country"])
 | 
			
		||||
    assert c
 | 
			
		||||
    assert isinstance(acc["location"], str)
 | 
			
		||||
    return (acc["location"] if on_trip else None, c)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _find_most_recent_travel_within_trip(
 | 
			
		||||
    trip: Trip,
 | 
			
		||||
    target_date: date,
 | 
			
		||||
    bookings: list[StrDict],
 | 
			
		||||
    accommodations: list[StrDict],
 | 
			
		||||
    airports: StrDict,
 | 
			
		||||
) -> tuple[str | None, pycountry.db.Country | None] | None:
 | 
			
		||||
) -> tuple[str | None, pycountry.db.Country] | None:
 | 
			
		||||
    """Find the most recent travel location within a trip."""
 | 
			
		||||
    uk_airports = {"LHR", "LGW", "STN", "LTN", "BRS", "BHX", "MAN", "EDI", "GLA"}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -166,39 +114,54 @@ def _find_most_recent_travel_within_trip(
 | 
			
		|||
    trip_most_recent_datetime = None
 | 
			
		||||
 | 
			
		||||
    # Check flights within trip period
 | 
			
		||||
    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
 | 
			
		||||
    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"])
 | 
			
		||||
 | 
			
		||||
                # 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
 | 
			
		||||
                            )
 | 
			
		||||
            # 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")
 | 
			
		||||
                        )
 | 
			
		||||
                    ):
 | 
			
		||||
                        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
 | 
			
		||||
                        trip_most_recent_location = (
 | 
			
		||||
                            location_name,
 | 
			
		||||
                            get_country("gb"),
 | 
			
		||||
                        )
 | 
			
		||||
                    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 accommodations:
 | 
			
		||||
    for acc in trip.accommodation:
 | 
			
		||||
        if "from" in acc:
 | 
			
		||||
            try:
 | 
			
		||||
                _, acc_date = _parse_datetime_field(acc["from"])
 | 
			
		||||
| 
						 | 
				
			
			@ -219,6 +182,93 @@ 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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -250,9 +300,7 @@ def _get_trip_location_by_progression(
 | 
			
		|||
 | 
			
		||||
def _find_most_recent_travel_before_date(
 | 
			
		||||
    target_date: date,
 | 
			
		||||
    bookings: list[StrDict],
 | 
			
		||||
    accommodations: list[StrDict],
 | 
			
		||||
    airports: StrDict,
 | 
			
		||||
    trips: list[Trip],
 | 
			
		||||
) -> 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"}
 | 
			
		||||
| 
						 | 
				
			
			@ -261,13 +309,14 @@ def _find_most_recent_travel_before_date(
 | 
			
		|||
    most_recent_date = None
 | 
			
		||||
    most_recent_datetime = None
 | 
			
		||||
 | 
			
		||||
    # Check flights
 | 
			
		||||
    for booking in bookings:
 | 
			
		||||
        for flight in booking.get("flights", []):
 | 
			
		||||
            if "arrive" in flight:
 | 
			
		||||
    # 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:
 | 
			
		||||
                try:
 | 
			
		||||
                    arrive_datetime, arrive_date = _parse_datetime_field(
 | 
			
		||||
                        flight["arrive"]
 | 
			
		||||
                        travel_item["arrive"]
 | 
			
		||||
                    )
 | 
			
		||||
                except ValueError:
 | 
			
		||||
                    continue
 | 
			
		||||
| 
						 | 
				
			
			@ -287,26 +336,167 @@ def _find_most_recent_travel_before_date(
 | 
			
		|||
                    ):
 | 
			
		||||
                        most_recent_date = arrive_date
 | 
			
		||||
                        most_recent_datetime = arrive_datetime
 | 
			
		||||
                        destination_airport = flight["to"]
 | 
			
		||||
                        most_recent_location = _get_airport_location(
 | 
			
		||||
                            destination_airport, airports, uk_airports, on_trip=False
 | 
			
		||||
                        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
 | 
			
		||||
                        )
 | 
			
		||||
 | 
			
		||||
    # 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
 | 
			
		||||
                        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"),
 | 
			
		||||
                            )
 | 
			
		||||
 | 
			
		||||
            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
 | 
			
		||||
            # 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"]
 | 
			
		||||
                    )
 | 
			
		||||
                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
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -327,49 +517,33 @@ 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 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
 | 
			
		||||
        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
 | 
			
		||||
 | 
			
		||||
            # 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 flight or accommodation before this date
 | 
			
		||||
    recent_travel = _find_most_recent_travel_before_date(
 | 
			
		||||
        target_date, bookings, accommodations, airports
 | 
			
		||||
    )
 | 
			
		||||
    # Find most recent travel before this date
 | 
			
		||||
    recent_travel = _find_most_recent_travel_before_date(target_date, trips)
 | 
			
		||||
 | 
			
		||||
    # 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)
 | 
			
		||||
| 
						 | 
				
			
			@ -396,11 +570,6 @@ 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)
 | 
			
		||||
| 
						 | 
				
			
			@ -418,10 +587,12 @@ def weekends(
 | 
			
		|||
        ]
 | 
			
		||||
 | 
			
		||||
        saturday_location = get_location_for_date(
 | 
			
		||||
            saturday, trips, bookings, accommodations, airports
 | 
			
		||||
            saturday,
 | 
			
		||||
            trips,
 | 
			
		||||
        )
 | 
			
		||||
        sunday_location = get_location_for_date(
 | 
			
		||||
            sunday, trips, bookings, accommodations, airports
 | 
			
		||||
            sunday,
 | 
			
		||||
            trips,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        weekends_info.append(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										128
									
								
								templates/trip_debug.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								templates/trip_debug.html
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,128 @@
 | 
			
		|||
{% extends "base.html" %}
 | 
			
		||||
 | 
			
		||||
{% block title %}Debug: {{ trip.title }} ({{ trip.start }}) - Edward Betts{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block style %}
 | 
			
		||||
<style>
 | 
			
		||||
.json-display {
 | 
			
		||||
    background-color: #f8f9fa;
 | 
			
		||||
    border: 1px solid #dee2e6;
 | 
			
		||||
    border-radius: 0.375rem;
 | 
			
		||||
    padding: 1rem;
 | 
			
		||||
    font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
 | 
			
		||||
    font-size: 0.875rem;
 | 
			
		||||
    line-height: 1.4;
 | 
			
		||||
    white-space: pre-wrap;
 | 
			
		||||
    overflow-x: auto;
 | 
			
		||||
    max-height: 80vh;
 | 
			
		||||
    overflow-y: auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.debug-header {
 | 
			
		||||
    background-color: #fff3cd;
 | 
			
		||||
    border: 1px solid #ffeaa7;
 | 
			
		||||
    border-radius: 0.375rem;
 | 
			
		||||
    padding: 1rem;
 | 
			
		||||
    margin-bottom: 1rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.debug-header h1 {
 | 
			
		||||
    color: #856404;
 | 
			
		||||
    margin-bottom: 0.5rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.debug-header p {
 | 
			
		||||
    color: #856404;
 | 
			
		||||
    margin-bottom: 0.5rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.debug-header .btn {
 | 
			
		||||
    margin-right: 0.5rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Basic JSON syntax highlighting using CSS */
 | 
			
		||||
.json-display .json-key {
 | 
			
		||||
    color: #d73a49;
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.json-display .json-string {
 | 
			
		||||
    color: #032f62;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.json-display .json-number {
 | 
			
		||||
    color: #005cc5;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.json-display .json-boolean {
 | 
			
		||||
    color: #e36209;
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.json-display .json-null {
 | 
			
		||||
    color: #6f42c1;
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<div class="container-fluid">
 | 
			
		||||
    <div class="debug-header">
 | 
			
		||||
        <h1>🐛 Trip Debug Information</h1>
 | 
			
		||||
        <p>Raw trip object data for: <strong>{{ trip.title }}</strong></p>
 | 
			
		||||
        <a href="{{ url_for('trip_page', start=start) }}" class="btn btn-primary">← Back to Trip Page</a>
 | 
			
		||||
        <button onclick="copyToClipboard()" class="btn btn-secondary">📋 Copy JSON</button>
 | 
			
		||||
    </div>
 | 
			
		||||
    
 | 
			
		||||
    <div class="row">
 | 
			
		||||
        <div class="col-12">
 | 
			
		||||
            <h3>Trip Object (JSON)</h3>
 | 
			
		||||
            <div class="json-display" id="jsonDisplay">{{ trip_json }}</div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block scripts %}
 | 
			
		||||
<script>
 | 
			
		||||
function copyToClipboard() {
 | 
			
		||||
    const jsonText = document.getElementById('jsonDisplay').textContent;
 | 
			
		||||
    navigator.clipboard.writeText(jsonText).then(function() {
 | 
			
		||||
        // Show a temporary notification
 | 
			
		||||
        const btn = event.target;
 | 
			
		||||
        const originalText = btn.textContent;
 | 
			
		||||
        btn.textContent = '✅ Copied!';
 | 
			
		||||
        btn.classList.remove('btn-secondary');
 | 
			
		||||
        btn.classList.add('btn-success');
 | 
			
		||||
        
 | 
			
		||||
        setTimeout(function() {
 | 
			
		||||
            btn.textContent = originalText;
 | 
			
		||||
            btn.classList.remove('btn-success');
 | 
			
		||||
            btn.classList.add('btn-secondary');
 | 
			
		||||
        }, 2000);
 | 
			
		||||
    }).catch(function(err) {
 | 
			
		||||
        console.error('Failed to copy: ', err);
 | 
			
		||||
        alert('Failed to copy to clipboard');
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Simple JSON syntax highlighting
 | 
			
		||||
function highlightJSON() {
 | 
			
		||||
    const display = document.getElementById('jsonDisplay');
 | 
			
		||||
    let content = display.textContent;
 | 
			
		||||
    
 | 
			
		||||
    // Highlight different JSON elements
 | 
			
		||||
    content = content.replace(/"([^"]+)":/g, '<span class="json-key">"$1":</span>');
 | 
			
		||||
    content = content.replace(/"([^"]*)"(?=\s*[,\]\}])/g, '<span class="json-string">"$1"</span>');
 | 
			
		||||
    content = content.replace(/\b(\d+\.?\d*)\b/g, '<span class="json-number">$1</span>');
 | 
			
		||||
    content = content.replace(/\b(true|false)\b/g, '<span class="json-boolean">$1</span>');
 | 
			
		||||
    content = content.replace(/\bnull\b/g, '<span class="json-null">null</span>');
 | 
			
		||||
    
 | 
			
		||||
    display.innerHTML = content;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Apply highlighting when page loads
 | 
			
		||||
document.addEventListener('DOMContentLoaded', highlightJSON);
 | 
			
		||||
</script>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
| 
						 | 
				
			
			@ -73,9 +73,6 @@ 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
 | 
			
		||||
| 
						 | 
				
			
			@ -94,9 +91,6 @@ 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
 | 
			
		||||
| 
						 | 
				
			
			@ -111,9 +105,6 @@ 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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										78
									
								
								web_view.py
									
									
									
									
									
								
							
							
						
						
									
										78
									
								
								web_view.py
									
									
									
									
									
								
							| 
						 | 
				
			
			@ -4,6 +4,7 @@
 | 
			
		|||
 | 
			
		||||
import decimal
 | 
			
		||||
import inspect
 | 
			
		||||
import json
 | 
			
		||||
import operator
 | 
			
		||||
import os.path
 | 
			
		||||
import sys
 | 
			
		||||
| 
						 | 
				
			
			@ -258,7 +259,9 @@ 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,
 | 
			
		||||
| 
						 | 
				
			
			@ -586,6 +589,79 @@ def trip_page(start: str) -> str:
 | 
			
		|||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.route("/trip/<start>/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."""
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in a new issue