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 ""
|
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."""
|
"""Lookup country by alpha-2 country code."""
|
||||||
|
if not alpha_2:
|
||||||
|
return None
|
||||||
if alpha_2.count(",") > 3: # ESA
|
if alpha_2.count(",") > 3: # ESA
|
||||||
return pycountry.db.Country(flag="🇪🇺", name="ESA")
|
return pycountry.db.Country(flag="🇪🇺", name="ESA")
|
||||||
if not alpha_2:
|
if not alpha_2:
|
||||||
|
|
196
agenda/busy.py
196
agenda/busy.py
|
@ -2,11 +2,12 @@
|
||||||
|
|
||||||
import itertools
|
import itertools
|
||||||
import typing
|
import typing
|
||||||
from datetime import date, timedelta
|
from datetime import date, datetime, timedelta
|
||||||
|
|
||||||
import flask
|
import flask
|
||||||
|
import pycountry
|
||||||
|
|
||||||
from . import events_yaml
|
from . import events_yaml, get_country, travel
|
||||||
from .event import Event
|
from .event import Event
|
||||||
from .types import StrDict, Trip
|
from .types import StrDict, Trip
|
||||||
|
|
||||||
|
@ -80,7 +81,180 @@ def get_busy_events(
|
||||||
return 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."""
|
"""Next ten weekends."""
|
||||||
weekday = start.weekday()
|
weekday = start.weekday()
|
||||||
|
|
||||||
|
@ -90,6 +264,11 @@ def weekends(start: date, busy_events: list[Event]) -> typing.Sequence[StrDict]:
|
||||||
else:
|
else:
|
||||||
start_date = start + timedelta(days=(5 - weekday))
|
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 = []
|
weekends_info = []
|
||||||
for i in range(52):
|
for i in range(52):
|
||||||
saturday = start_date + timedelta(weeks=i)
|
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
|
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(
|
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
|
return weekends_info
|
||||||
|
|
|
@ -11,7 +11,9 @@
|
||||||
<th class="text-end">Week</th>
|
<th class="text-end">Week</th>
|
||||||
<th class="text-end">Date</th>
|
<th class="text-end">Date</th>
|
||||||
<th>Saturday</th>
|
<th>Saturday</th>
|
||||||
|
<th>Saturday Location</th>
|
||||||
<th>Sunday</th>
|
<th>Sunday</th>
|
||||||
|
<th>Sunday Location</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
@ -43,6 +45,14 @@
|
||||||
<strong>free</strong>
|
<strong>free</strong>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</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 %}
|
{% endfor %}
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% 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()
|
trip_list = agenda.trip.build_trip_list()
|
||||||
busy_events = agenda.busy.get_busy_events(start, app.config, 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(
|
return flask.render_template(
|
||||||
"weekends.html",
|
"weekends.html",
|
||||||
items=weekends,
|
items=weekends,
|
||||||
|
|
Loading…
Reference in a new issue