Fix trip map unbooked flight pins and timezone-safe busy date comparisons
This commit is contained in:
parent
f38c5327ea
commit
f9b79d5a51
3 changed files with 150 additions and 7 deletions
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import itertools
|
import itertools
|
||||||
import typing
|
import typing
|
||||||
from datetime import date, datetime, timedelta
|
from datetime import date, datetime, timedelta, timezone
|
||||||
|
|
||||||
import flask
|
import flask
|
||||||
import pycountry
|
import pycountry
|
||||||
|
|
@ -84,14 +84,21 @@ def get_busy_events(
|
||||||
def _parse_datetime_field(datetime_obj: datetime | date | str) -> tuple[datetime, date]:
|
def _parse_datetime_field(datetime_obj: datetime | date | str) -> tuple[datetime, date]:
|
||||||
"""Parse a datetime field that could be datetime object or string."""
|
"""Parse a datetime field that could be datetime object or string."""
|
||||||
if isinstance(datetime_obj, datetime):
|
if isinstance(datetime_obj, datetime):
|
||||||
return datetime_obj, datetime_obj.date()
|
dt = datetime_obj
|
||||||
if isinstance(datetime_obj, date):
|
elif isinstance(datetime_obj, date):
|
||||||
return datetime.combine(datetime_obj, datetime.min.time()), datetime_obj
|
dt = datetime.combine(datetime_obj, datetime.min.time(), tzinfo=timezone.utc)
|
||||||
if isinstance(datetime_obj, str):
|
elif isinstance(datetime_obj, str):
|
||||||
dt = datetime.fromisoformat(datetime_obj.replace("Z", "+00:00"))
|
dt = datetime.fromisoformat(datetime_obj.replace("Z", "+00:00"))
|
||||||
return dt, dt.date()
|
else:
|
||||||
raise ValueError(f"Invalid datetime format: {datetime_obj}")
|
raise ValueError(f"Invalid datetime format: {datetime_obj}")
|
||||||
|
|
||||||
|
if dt.tzinfo is None:
|
||||||
|
dt = dt.replace(tzinfo=timezone.utc)
|
||||||
|
else:
|
||||||
|
dt = dt.astimezone(timezone.utc)
|
||||||
|
|
||||||
|
return dt, dt.date()
|
||||||
|
|
||||||
|
|
||||||
def _get_accommodation_location(
|
def _get_accommodation_location(
|
||||||
acc: StrDict, on_trip: bool = False
|
acc: StrDict, on_trip: bool = False
|
||||||
|
|
|
||||||
35
tests/test_busy_timezone.py
Normal file
35
tests/test_busy_timezone.py
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
"""Regression tests for timezone handling in busy location logic."""
|
||||||
|
|
||||||
|
from datetime import date, datetime, timezone
|
||||||
|
|
||||||
|
import agenda.busy
|
||||||
|
from agenda.types import Trip
|
||||||
|
|
||||||
|
|
||||||
|
def test_mixed_naive_and_aware_arrivals_do_not_crash() -> None:
|
||||||
|
"""Most recent travel should compare mixed timezone styles safely."""
|
||||||
|
trips = [
|
||||||
|
Trip(
|
||||||
|
start=date(2099, 12, 30),
|
||||||
|
travel=[
|
||||||
|
{
|
||||||
|
"type": "flight",
|
||||||
|
"arrive": datetime(2100, 1, 1, 10, 0, 0),
|
||||||
|
"to": "CDG",
|
||||||
|
"to_airport": {"country": "fr", "city": "Paris"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "flight",
|
||||||
|
"arrive": datetime(2100, 1, 1, 12, 0, 0, tzinfo=timezone.utc),
|
||||||
|
"to": "AMS",
|
||||||
|
"to_airport": {"country": "nl", "city": "Amsterdam"},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
location = agenda.busy._find_most_recent_travel_before_date(date(2100, 1, 1), trips)
|
||||||
|
assert location is not None
|
||||||
|
assert location[0] == "Amsterdam"
|
||||||
|
assert location[1] is not None
|
||||||
|
assert location[1].alpha_2 == "NL"
|
||||||
101
tests/test_trip.py
Normal file
101
tests/test_trip.py
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
"""Tests for trip map coordinate assembly."""
|
||||||
|
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
import agenda.trip
|
||||||
|
from agenda.types import Trip
|
||||||
|
from web_view import app
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_coordinates_for_unbooked_flights_adds_missing_airports() -> None:
|
||||||
|
"""Unbooked routes should contribute missing airport pins."""
|
||||||
|
routes = [
|
||||||
|
{
|
||||||
|
"type": "unbooked_flight",
|
||||||
|
"key": "LHR_Paris_fr",
|
||||||
|
"from_iata": "LHR",
|
||||||
|
"to_iata": "CDG",
|
||||||
|
"from": (51.47, -0.45),
|
||||||
|
"to": (49.01, 2.55),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
coordinates = [
|
||||||
|
{
|
||||||
|
"name": "Heathrow Airport",
|
||||||
|
"type": "airport",
|
||||||
|
"latitude": 51.47,
|
||||||
|
"longitude": -0.45,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
airports = {
|
||||||
|
"LHR": {
|
||||||
|
"name": "Heathrow Airport",
|
||||||
|
"latitude": 51.47,
|
||||||
|
"longitude": -0.45,
|
||||||
|
},
|
||||||
|
"CDG": {
|
||||||
|
"name": "Paris Charles de Gaulle Airport",
|
||||||
|
"latitude": 49.01,
|
||||||
|
"longitude": 2.55,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
original_parse_yaml = agenda.trip.travel.parse_yaml
|
||||||
|
try:
|
||||||
|
agenda.trip.travel.parse_yaml = lambda _name, _data_dir: airports
|
||||||
|
agenda.trip.add_coordinates_for_unbooked_flights(routes, coordinates)
|
||||||
|
finally:
|
||||||
|
agenda.trip.travel.parse_yaml = original_parse_yaml
|
||||||
|
|
||||||
|
airport_names = {
|
||||||
|
coord["name"] for coord in coordinates if coord["type"] == "airport"
|
||||||
|
}
|
||||||
|
assert airport_names == {"Heathrow Airport", "Paris Charles de Gaulle Airport"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_coordinates_and_routes_adds_unbooked_flight_airports() -> None:
|
||||||
|
"""Trip list map data should include pins for unbooked flights."""
|
||||||
|
trips = [Trip(start=date(2026, 7, 20))]
|
||||||
|
unbooked_routes = [
|
||||||
|
{
|
||||||
|
"type": "unbooked_flight",
|
||||||
|
"key": "LHR_Paris_fr",
|
||||||
|
"from_iata": "LHR",
|
||||||
|
"to_iata": "CDG",
|
||||||
|
"from": (51.47, -0.45),
|
||||||
|
"to": (49.01, 2.55),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
airports = {
|
||||||
|
"LHR": {
|
||||||
|
"name": "Heathrow Airport",
|
||||||
|
"latitude": 51.47,
|
||||||
|
"longitude": -0.45,
|
||||||
|
},
|
||||||
|
"CDG": {
|
||||||
|
"name": "Paris Charles de Gaulle Airport",
|
||||||
|
"latitude": 49.01,
|
||||||
|
"longitude": 2.55,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
original_collect_trip_coordinates = agenda.trip.collect_trip_coordinates
|
||||||
|
original_get_trip_routes = agenda.trip.get_trip_routes
|
||||||
|
original_parse_yaml = agenda.trip.travel.parse_yaml
|
||||||
|
try:
|
||||||
|
agenda.trip.collect_trip_coordinates = lambda _trip: []
|
||||||
|
agenda.trip.get_trip_routes = lambda _trip, _data_dir: unbooked_routes
|
||||||
|
agenda.trip.travel.parse_yaml = lambda _name, _data_dir: airports
|
||||||
|
|
||||||
|
coordinates, _routes = agenda.trip.get_coordinates_and_routes(trips)
|
||||||
|
finally:
|
||||||
|
agenda.trip.collect_trip_coordinates = original_collect_trip_coordinates
|
||||||
|
agenda.trip.get_trip_routes = original_get_trip_routes
|
||||||
|
agenda.trip.travel.parse_yaml = original_parse_yaml
|
||||||
|
|
||||||
|
airport_names = {
|
||||||
|
coord["name"] for coord in coordinates if coord["type"] == "airport"
|
||||||
|
}
|
||||||
|
assert airport_names == {"Heathrow Airport", "Paris Charles de Gaulle Airport"}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue