From fd46f0a40504dc45e0d7c22caddef248a389ba34 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Wed, 3 Jan 2024 11:33:24 +0000 Subject: [PATCH 001/449] Show country names and flags on accommodation page --- templates/accommodation.html | 15 +++++++++++++-- web_view.py | 6 ++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/templates/accommodation.html b/templates/accommodation.html index ee98ef4..a7f9b21 100644 --- a/templates/accommodation.html +++ b/templates/accommodation.html @@ -1,9 +1,10 @@ {% extends "base.html" %} {% block style %} +{% set column_count = 7 %} {% endblock %} {% macro row(item, badge) %} +{% set country = get_country(item.country) %}
{{ item.from.strftime("%a, %d %b %Y") }}
{{ item.to.strftime("%a, %d %b") }}
{{ (item.to.date() - item.from.date()).days }}
{{ item.name }}
{{ item.operator }}
{{ item.location }}
+
+ {% if country %} + {{ country.flag }} {{ country.name }} + {% else %} + + country code {{ item.country }} not found + + {% endif %} +
{% endmacro %} {% macro section(heading, item_list, badge) %} diff --git a/web_view.py b/web_view.py index 0a31d2e..041d9c2 100755 --- a/web_view.py +++ b/web_view.py @@ -7,9 +7,11 @@ import operator import os.path import sys import traceback +import typing from datetime import date, datetime import flask +import pycountry import werkzeug import werkzeug.debug.tbtools import yaml @@ -140,11 +142,15 @@ def accommodation_list() -> str: if stay["country"] != "gb" ) + def get_country(alpha_2: str) -> str | None: + return typing.cast(str | None, pycountry.countries.get(alpha_2=alpha_2.upper())) + return flask.render_template( "accommodation.html", items=items, total_nights_2024=total_nights_2024, nights_abroad_2024=nights_abroad_2024, + get_country=get_country, ) From 17036d849f7b442b4417231d1d365f17fad03045 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Wed, 3 Jan 2024 15:52:24 +0000 Subject: [PATCH 002/449] Show country names and flags on conference page --- agenda/conference.py | 1 + templates/conference_list.html | 17 +++++++++++++++-- web_view.py | 17 +++++++++++++---- 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/agenda/conference.py b/agenda/conference.py index da697fe..c1939ea 100644 --- a/agenda/conference.py +++ b/agenda/conference.py @@ -18,6 +18,7 @@ class Conference: location: str start: date | datetime end: date | datetime + country: str | None = None venue: str | None = None address: str | None = None url: str | None = None diff --git a/templates/conference_list.html b/templates/conference_list.html index 5d490c1..b3422da 100644 --- a/templates/conference_list.html +++ b/templates/conference_list.html @@ -1,10 +1,11 @@ {% extends "base.html" %} {% block style %} +{% set column_count = 7 %} {% endblock %} {% macro row(item, badge) %} +{% set country = get_country(item.country) if item.country else None %}
{{ item.start.strftime("%a, %d %b %Y") }}
{{ item.end.strftime("%a, %d %b") }}
{{ item.name }} @@ -37,6 +39,17 @@
{{ item.topic }}
{{ item.location }}
+
+ {% if country %} + {{ country.flag }} {{ country.name }} + {% elif item.online %} + 💻 Online + {% else %} + + country code {{ item.country }} not found + + {% endif %} +
{% endmacro %} diff --git a/web_view.py b/web_view.py index 041d9c2..4f07b43 100755 --- a/web_view.py +++ b/web_view.py @@ -98,6 +98,13 @@ def as_date(d: date | datetime) -> date: return d.date() if isinstance(d, datetime) else d +def get_country(alpha_2: str) -> str | None: + """Lookup country by alpha-2 country code.""" + if not alpha_2: + return None + return typing.cast(str, pycountry.countries.get(alpha_2=alpha_2.upper())) + + @app.route("/conference") def conference_list() -> str: """Page showing a list of conferences.""" @@ -121,7 +128,12 @@ def conference_list() -> str: future = [conf for conf in item_list if conf["start_date"] > today] return flask.render_template( - "conference_list.html", current=current, past=past, future=future, today=today + "conference_list.html", + current=current, + past=past, + future=future, + today=today, + get_country=get_country, ) @@ -142,9 +154,6 @@ def accommodation_list() -> str: if stay["country"] != "gb" ) - def get_country(alpha_2: str) -> str | None: - return typing.cast(str | None, pycountry.countries.get(alpha_2=alpha_2.upper())) - return flask.render_template( "accommodation.html", items=items, From 824285a4cf42711fa3e03500a10233cf01169ea4 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Wed, 3 Jan 2024 17:22:04 +0000 Subject: [PATCH 003/449] Add links to accommodation --- templates/accommodation.html | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/templates/accommodation.html b/templates/accommodation.html index a7f9b21..29e5bfa 100644 --- a/templates/accommodation.html +++ b/templates/accommodation.html @@ -21,10 +21,10 @@ {% macro row(item, badge) %} {% set country = get_country(item.country) %} +{% set nights = (item.to.date() - item.from.date()).days %}
{{ item.from.strftime("%a, %d %b %Y") }}
{{ item.to.strftime("%a, %d %b") }}
-
{{ (item.to.date() - item.from.date()).days }}
-
{{ item.name }}
+
{% if nights == 1 %}1 night{% else %}{{ nights }} nights{% endif %}
{{ item.operator }}
{{ item.location }}
@@ -36,6 +36,13 @@ {% endif %}
+
+ {% if item.url %} + {{ item.name }} + {% else %} + {{ item.name }} + {% endif %} +
{% endmacro %} {% macro section(heading, item_list, badge) %} From b1139b79d2cfb7bb4241356a8052e261a55fbcb4 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Thu, 4 Jan 2024 07:40:39 +0000 Subject: [PATCH 004/449] Make name a link to conference web site --- templates/conference_list.html | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/templates/conference_list.html b/templates/conference_list.html index b3422da..74fa7eb 100644 --- a/templates/conference_list.html +++ b/templates/conference_list.html @@ -1,7 +1,7 @@ {% extends "base.html" %} {% block style %} -{% set column_count = 7 %} +{% set column_count = 6 %} {% endblock %} -{% macro row(item, badge) %} -{% set country = get_country(item.country) %} -{% set nights = (item.to.date() - item.from.date()).days %} -
{{ item.from.strftime("%a, %d %b %Y") }}
-
{{ item.to.strftime("%a, %d %b") }}
-
{% if nights == 1 %}1 night{% else %}{{ nights }} nights{% endif %}
-
{{ item.operator }}
-
{{ item.location }}
-
- {% if country %} - {{ country.flag }} {{ country.name }} - {% else %} - - country code {{ item.country }} not found - - {% endif %} -
-
- {% if item.url %} - {{ item.name }} - {% else %} - {{ item.name }} - {% endif %} -
-{% endmacro %} - {% macro section(heading, item_list, badge) %} {% if item_list %}

{{heading}}

-{% for item in item_list %}{{ row(item, badge) }}{% endfor %} +{% for item in item_list %}{{ accommodation_row(item, badge) }}{% endfor %} {% endif %} {% endmacro %} diff --git a/templates/conference_list.html b/templates/conference_list.html index 74fa7eb..3465f01 100644 --- a/templates/conference_list.html +++ b/templates/conference_list.html @@ -1,5 +1,7 @@ {% extends "base.html" %} +{% from "macros.html" import conference_row with context %} + {% block style %} {% set column_count = 6 %} {% endblock %} -{% macro row(item, badge) %} -{% set country = get_country(item.country) if item.country else None %} -
{{ item.start.strftime("%a, %d %b %Y") }}
-
{{ item.end.strftime("%a, %d %b") }}
-
- {% if item.url %} - {{ item.name }} - {% else %} - {{ item.name }} - {% endif %} - {% if item.going and not (item.accommodation_booked or item.travel_booked) %} - - {{ badge }} - - {% endif %} - {% if item.accommodation_booked %} - accommodation - {% endif %} - {% if item.transport_booked %} - transport - {% endif %} -
-
{{ item.topic }}
-
{{ item.location }}
-
- {% if country %} - {{ country.flag }} {{ country.name }} - {% elif item.online %} - 💻 Online - {% else %} - - country code {{ item.country }} not found - - {% endif %} -
-{% endmacro %} - {% macro section(heading, item_list, badge) %} -{% if item_list %} -

{{heading}}

-{% for item in item_list %}{{ row(item, badge) }}{% endfor %} -{% endif %} + {% if item_list %} +

{{ heading }}

+ {% for item in item_list %}{{ conference_row(item, badge) }}{% endfor %} + {% endif %} {% endmacro %} {% block content %}
-

Conferences

-
{{ section("Current", current, "attending") }} {{ section("Future", future, "going") }} diff --git a/templates/macros.html b/templates/macros.html new file mode 100644 index 0000000..b279470 --- /dev/null +++ b/templates/macros.html @@ -0,0 +1,96 @@ +{% macro display_datetime(dt) %}{{ dt.strftime("%a, %d, %b %Y %H:%M %z") }}{% endmacro %} +{% macro display_time(dt) %}{{ dt.strftime("%H:%M %z") }}{% endmacro %} + +{% macro conference_row(item, badge) %} + {% set country = get_country(item.country) if item.country else None %} +
{{ item.start.strftime("%a, %d %b %Y") }}
+
{{ item.end.strftime("%a, %d %b") }}
+
+ {% if item.url %} + {{ item.name }} + {% else %} + {{ item.name }} + {% endif %} + {% if item.going and not (item.accommodation_booked or item.travel_booked) %} + + {{ badge }} + + {% endif %} + {% if item.accommodation_booked %} + accommodation + {% endif %} + {% if item.transport_booked %} + transport + {% endif %} +
+
{{ item.topic }}
+
{{ item.location }}
+
+ {% if country %} + {{ country.flag }} {{ country.name }} + {% elif item.online %} + 💻 Online + {% else %} + + country code {{ item.country }} not found + + {% endif %} +
+{% endmacro %} + +{% macro accommodation_row(item, badge) %} + {% set country = get_country(item.country) %} + + {% set nights = (item.to.date() - item.from.date()).days %} +
{{ item.from.strftime("%a, %d %b %Y") }}
+
{{ item.to.strftime("%a, %d %b") }}
+
{% if nights == 1 %}1 night{% else %}{{ nights }} nights{% endif %}
+
{{ item.operator }}
+
{{ item.location }}
+
+ {% if country %} + {{ country.flag }} {{ country.name }} + {% else %} + + country code {{ item.country }} not found + + {% endif %} +
+
+ {% if item.url %} + {{ item.name }} + {% else %} + {{ item.name }} + {% endif %} +
+{% endmacro %} + +{% macro flight_row(item) %} +
{{ item.depart.strftime("%a, %d %b %Y") }}
+
{{ item.from }} → {{ item.to }}
+
{{ item.depart.strftime("%H:%M") }}
+
+ {% if item.arrive %} + {{ item.arrive.strftime("%H:%M") }} + {% if item.arrive.date() != item.depart.date() %}+1 day{% endif %} + {% endif %} +
+
{{ item.duration }}
+
{{ item.airline }}{{ item.flight_number }}
+
{{ item.booking_reference }}
+{% endmacro %} + +{% macro train_row(item) %} +
{{ item.depart.strftime("%a, %d %b %Y") }}
+
{{ item.from }} → {{ item.to }}
+
{{ item.depart.strftime("%H:%M") }}
+
+ {% if item.arrive %} + {{ item.arrive.strftime("%H:%M") }} + {% if item.arrive.date() != item.depart.date() %}+1 day{% endif %} + {% endif %} +
+
{{ ((item.arrive - item.depart).total_seconds() // 60) | int }} mins
+
{{ item.operator }}
+
{{ item.booking_reference }}
+{% endmacro %} diff --git a/templates/navbar.html b/templates/navbar.html index 444fa14..198b666 100644 --- a/templates/navbar.html +++ b/templates/navbar.html @@ -2,6 +2,7 @@ {% set pages = [ {"endpoint": "index", "label": "Home" }, + {"endpoint": "trip_list", "label": "Trips" }, {"endpoint": "conference_list", "label": "Conference" }, {"endpoint": "travel_list", "label": "Travel" }, {"endpoint": "accommodation_list", "label": "Accommodation" }, diff --git a/templates/travel.html b/templates/travel.html index 86ae8bc..a762ebb 100644 --- a/templates/travel.html +++ b/templates/travel.html @@ -1,10 +1,5 @@ {% extends "base.html" %} - -{% block travel %} -{% endblock %} - -{% macro display_datetime(dt) %}{{ dt.strftime("%a, %d, %b %Y %H:%M %z") }}{% endmacro %} -{% macro display_time(dt) %}{{ dt.strftime("%H:%M %z") }}{% endmacro %} +{% from "macros.html" import flight_row, train_row with context %} {% block style %} +{% endblock %} + +{% block content %} +
+ + {% set row = { "flight": flight_row, "train": train_row } %} + +

Trips

+ {% for trip in trips %} +
+

{{ trip.title }}

+

Countries: {{ trip.countries_str }}

+
+ {% for conf in trip.conferences %} + {{ conference_row(conf, "going") }} + {% endfor %} +
+ +
+ {% for conf in trip.accommodation %} + {{ accommodation_row(conf, "going") }} + {% endfor %} +
+ +
+ {% for item in trip.travel %} + {{ row[item.type](item) }} + {% endfor %} +
+
+ {% endfor %} +
+ +{% endblock %} + From 21b67bdc64d1ce62e3caf9d75d68f4855f4a353e Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Fri, 5 Jan 2024 09:35:56 +0000 Subject: [PATCH 009/449] Show end date for trips --- agenda/types.py | 90 +++++++++++++++++++++++++------------------ templates/macros.html | 2 + templates/trips.html | 12 ++++-- web_view.py | 8 ++-- 4 files changed, 67 insertions(+), 45 deletions(-) diff --git a/agenda/types.py b/agenda/types.py index e3d5801..bf69cb7 100644 --- a/agenda/types.py +++ b/agenda/types.py @@ -1,8 +1,8 @@ """Types.""" -import dataclasses -import datetime import typing +from dataclasses import dataclass, field +from datetime import date, datetime, timezone from pycountry.db import Country @@ -12,23 +12,47 @@ from agenda import format_list_with_ampersand StrDict = dict[str, typing.Any] -@dataclasses.dataclass +def as_date(d: datetime | date) -> date: + """Convert datetime to date.""" + if isinstance(d, datetime): + return d.date() + assert isinstance(d, date) + return d + + +@dataclass class Trip: """Trip.""" - date: datetime.date - travel: list[StrDict] = dataclasses.field(default_factory=list) - accommodation: list[StrDict] = dataclasses.field(default_factory=list) - conferences: list[StrDict] = dataclasses.field(default_factory=list) + start: date + travel: list[StrDict] = field(default_factory=list) + accommodation: list[StrDict] = field(default_factory=list) + conferences: list[StrDict] = field(default_factory=list) @property def title(self) -> str: """Trip title.""" - names = ( + return ( format_list_with_ampersand([conf["name"] for conf in self.conferences]) - or "[no conferences in trip]" + or "[no conference]" ) - return f'{names} ({self.date.strftime("%b %Y")})' + + @property + def end(self) -> date | None: + """End date for trip.""" + max_conference_end = ( + max(as_date(item["end"]) for item in self.conferences) + if self.conferences + else date.min + ) + assert isinstance(max_conference_end, date) + + arrive = [item["arrive"].date() for item in self.travel if "arrive" in item] + travel_end = max(arrive) if arrive else date.min + assert isinstance(travel_end, date) + + max_date = max(max_conference_end, travel_end) + return max_date if max_date != date.min else None @property def countries(self) -> set[Country]: @@ -51,56 +75,54 @@ class Trip: ) -@dataclasses.dataclass +@dataclass class Holiday: """Holiay.""" name: str country: str - date: datetime.date + date: date -@dataclasses.dataclass +@dataclass class Event: """Event.""" name: str - date: datetime.date | datetime.datetime - end_date: datetime.date | datetime.datetime | None = None + date: date | datetime + end_date: date | datetime | None = None title: str | None = None url: str | None = None going: bool | None = None @property - def as_datetime(self) -> datetime.datetime: + def as_datetime(self) -> datetime: """Date/time of event.""" d = self.date - t0 = datetime.datetime.min.time() + t0 = datetime.min.time() return ( d - if isinstance(d, datetime.datetime) - else datetime.datetime.combine(d, t0).replace(tzinfo=datetime.timezone.utc) + if isinstance(d, datetime) + else datetime.combine(d, t0).replace(tzinfo=timezone.utc) ) @property def has_time(self) -> bool: """Event has a time associated with it.""" - return isinstance(self.date, datetime.datetime) + return isinstance(self.date, datetime) @property - def as_date(self) -> datetime.date: + def as_date(self) -> date: """Date of event.""" - return ( - self.date.date() if isinstance(self.date, datetime.datetime) else self.date - ) + return self.date.date() if isinstance(self.date, datetime) else self.date @property - def end_as_date(self) -> datetime.date: + def end_as_date(self) -> date: """Date of event.""" return ( ( self.end_date.date() - if isinstance(self.end_date, datetime.datetime) + if isinstance(self.end_date, datetime) else self.end_date ) if self.end_date @@ -110,22 +132,14 @@ class Event: @property def display_time(self) -> str | None: """Time for display on web page.""" - return ( - self.date.strftime("%H:%M") - if isinstance(self.date, datetime.datetime) - else None - ) + return self.date.strftime("%H:%M") if isinstance(self.date, datetime) else None @property def display_timezone(self) -> str | None: """Timezone for display on web page.""" - return ( - self.date.strftime("%z") - if isinstance(self.date, datetime.datetime) - else None - ) + return self.date.strftime("%z") if isinstance(self.date, datetime) else None - def delta_days(self, today: datetime.date) -> str: + def delta_days(self, today: date) -> str: """Return number of days from today as a string.""" delta = (self.as_date - today).days @@ -140,7 +154,7 @@ class Event: @property def display_date(self) -> str: """Date for display on web page.""" - if isinstance(self.date, datetime.datetime): + if isinstance(self.date, datetime): return self.date.strftime("%a, %d, %b %Y %H:%M %z") else: return self.date.strftime("%a, %d, %b %Y") diff --git a/templates/macros.html b/templates/macros.html index b279470..96651a9 100644 --- a/templates/macros.html +++ b/templates/macros.html @@ -1,5 +1,7 @@ {% macro display_datetime(dt) %}{{ dt.strftime("%a, %d, %b %Y %H:%M %z") }}{% endmacro %} {% macro display_time(dt) %}{{ dt.strftime("%H:%M %z") }}{% endmacro %} +{% macro display_date(dt) %}{{ dt.strftime("%a, %d, %b %Y") }}{% endmacro %} +{% macro display_date_no_year(dt) %}{{ dt.strftime("%a, %d, %b") }}{% endmacro %} {% macro conference_row(item, badge) %} {% set country = get_country(item.country) if item.country else None %} diff --git a/templates/trips.html b/templates/trips.html index 48005d3..7b4e2f4 100644 --- a/templates/trips.html +++ b/templates/trips.html @@ -1,6 +1,6 @@ {% extends "base.html" %} -{% from "macros.html" import conference_row, accommodation_row, flight_row, train_row with context %} +{% from "macros.html" import display_date_no_year, conference_row, accommodation_row, flight_row, train_row with context %} {% block style %} {% set conference_column_count = 6 %} @@ -41,9 +41,15 @@

Trips

{% for trip in trips %} + {% set end = trip.end %}
-

{{ trip.title }}

-

Countries: {{ trip.countries_str }}

+

{{ trip.title }} ({{ trip.start.strftime("%b %Y") }})

+
Countries: {{ trip.countries_str }}
+ {% if end %} +
Dates: {{ display_date_no_year(trip.start) }} to {{ display_date_no_year(end) }}
+ {% else %} +
Start: {{ display_date_no_year(trip.start) }} (end date missing)
+ {% endif %}
{% for conf in trip.conferences %} {{ conference_row(conf, "going") }} diff --git a/web_view.py b/web_view.py index eedb29f..758ba3b 100755 --- a/web_view.py +++ b/web_view.py @@ -184,11 +184,11 @@ def trip_list() -> str: for key, item_list in data.items(): assert isinstance(item_list, list) for item in item_list: - if not (trip_id := item.get("trip")): + if not (start := item.get("trip")): continue - if trip_id not in trips: - trips[trip_id] = Trip(date=trip_id) - getattr(trips[trip_id], key).append(item) + if start not in trips: + trips[start] = Trip(start=start) + getattr(trips[start], key).append(item) trip_list = [trip for _, trip in sorted(trips.items(), reverse=True)] From fd6d3b674bcf649a30cf1bc7aec65480c54b73e6 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sat, 6 Jan 2024 09:17:34 +0000 Subject: [PATCH 010/449] Split up trips page and sort like conference page Closes: #94 --- templates/trips.html | 72 +++++++++++++++++++++++++------------------- web_view.py | 28 ++++++++++++++--- 2 files changed, 64 insertions(+), 36 deletions(-) diff --git a/templates/trips.html b/templates/trips.html index 7b4e2f4..664c49e 100644 --- a/templates/trips.html +++ b/templates/trips.html @@ -2,6 +2,8 @@ {% from "macros.html" import display_date_no_year, conference_row, accommodation_row, flight_row, train_row with context %} +{% set row = { "flight": flight_row, "train": train_row } %} + {% block style %} {% set conference_column_count = 6 %} {% set accommodation_column_count = 7 %} @@ -34,42 +36,50 @@ {% endblock %} +{% macro section(heading, item_list, badge) %} + {% if item_list %} +

{{ heading }}

+ {% for trip in item_list %} + {% set end = trip.end %} +
+

{{ trip.title }} ({{ trip.start.strftime("%b %Y") }})

+
Countries: {{ trip.countries_str }}
+ {% if end %} +
Dates: {{ display_date_no_year(trip.start) }} to {{ display_date_no_year(end) }}
+ {% else %} +
Start: {{ display_date_no_year(trip.start) }} (end date missing)
+ {% endif %} +
+ {% for conf in trip.conferences %} + {{ conference_row(conf, "going") }} + {% endfor %} +
+ +
+ {% for conf in trip.accommodation %} + {{ accommodation_row(conf, "going") }} + {% endfor %} +
+ +
+ {% for item in trip.travel %} + {{ row[item.type](item) }} + {% endfor %} +
+
+ {% endfor %} + {% endif %} +{% endmacro %} + + {% block content %}
- {% set row = { "flight": flight_row, "train": train_row } %}

Trips

- {% for trip in trips %} - {% set end = trip.end %} -
-

{{ trip.title }} ({{ trip.start.strftime("%b %Y") }})

-
Countries: {{ trip.countries_str }}
- {% if end %} -
Dates: {{ display_date_no_year(trip.start) }} to {{ display_date_no_year(end) }}
- {% else %} -
Start: {{ display_date_no_year(trip.start) }} (end date missing)
- {% endif %} -
- {% for conf in trip.conferences %} - {{ conference_row(conf, "going") }} - {% endfor %} -
- -
- {% for conf in trip.accommodation %} - {{ accommodation_row(conf, "going") }} - {% endfor %} -
- -
- {% for item in trip.travel %} - {{ row[item.type](item) }} - {% endfor %} -
-
- {% endfor %} + {{ section("Current", current, "attending") }} + {{ section("Future", future, "going") }} + {{ section("Past", past|reverse, "went") }}
{% endblock %} - diff --git a/web_view.py b/web_view.py index 758ba3b..5cc1d89 100755 --- a/web_view.py +++ b/web_view.py @@ -164,9 +164,8 @@ def load_travel(travel_type: str) -> list[StrDict]: return items -@app.route("/trip") -def trip_list() -> str: - """Page showing a list of trips.""" +def build_trip_list() -> list[Trip]: + """Generate list of trips.""" trips: dict[date, Trip] = {} data_dir = app.config["PERSONAL_DATA"] @@ -190,11 +189,30 @@ def trip_list() -> str: trips[start] = Trip(start=start) getattr(trips[start], key).append(item) - trip_list = [trip for _, trip in sorted(trips.items(), reverse=True)] + return [trip for _, trip in sorted(trips.items())] + + +@app.route("/trip") +def trip_list() -> str: + """Page showing a list of trips.""" + trip_list = build_trip_list() + + today = date.today() + current = [ + item + for item in trip_list + if item.start <= today and (item.end or item.start) >= today + ] + + past = [item for item in trip_list if (item.end or item.start) < today] + future = [item for item in trip_list if item.start > today] return flask.render_template( "trips.html", - trips=trip_list, + current=current, + past=past, + future=future, + today=today, get_country=agenda.get_country, format_list_with_ampersand=format_list_with_ampersand, ) From 50127417f0b1c696269f0e70584879d38f2bf9cc Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sat, 6 Jan 2024 09:21:54 +0000 Subject: [PATCH 011/449] Show trip country list in order visited --- agenda/types.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/agenda/types.py b/agenda/types.py index bf69cb7..1e0ba44 100644 --- a/agenda/types.py +++ b/agenda/types.py @@ -55,17 +55,21 @@ class Trip: return max_date if max_date != date.min else None @property - def countries(self) -> set[Country]: - """Trip countries.""" - found: set[Country] = set() + def countries(self) -> list[Country]: + """Countries visited as part of trip, in order.""" + seen: set[str] = set() + items: list[Country] = [] for item in self.conferences + self.accommodation: if "country" not in item: continue + if item["country"] in seen: + continue + seen.add(item["country"]) country = agenda.get_country(item["country"]) assert country - found.add(country) + items.append(country) - return found + return items @property def countries_str(self) -> str: From 7a5319aa8356fd79306885260d59b6f818eab65f Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sat, 6 Jan 2024 15:39:46 +0000 Subject: [PATCH 012/449] Improve trip template layout --- templates/macros.html | 4 ++-- templates/trips.html | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/templates/macros.html b/templates/macros.html index 96651a9..a9d7364 100644 --- a/templates/macros.html +++ b/templates/macros.html @@ -1,7 +1,7 @@ {% macro display_datetime(dt) %}{{ dt.strftime("%a, %d, %b %Y %H:%M %z") }}{% endmacro %} {% macro display_time(dt) %}{{ dt.strftime("%H:%M %z") }}{% endmacro %} -{% macro display_date(dt) %}{{ dt.strftime("%a, %d, %b %Y") }}{% endmacro %} -{% macro display_date_no_year(dt) %}{{ dt.strftime("%a, %d, %b") }}{% endmacro %} +{% macro display_date(dt) %}{{ dt.strftime("%a %-d %b %Y") }}{% endmacro %} +{% macro display_date_no_year(dt) %}{{ dt.strftime("%a %-d %b") }}{% endmacro %} {% macro conference_row(item, badge) %} {% set country = get_country(item.country) if item.country else None %} diff --git a/templates/trips.html b/templates/trips.html index 664c49e..e2c45bd 100644 --- a/templates/trips.html +++ b/templates/trips.html @@ -1,6 +1,6 @@ {% extends "base.html" %} -{% from "macros.html" import display_date_no_year, conference_row, accommodation_row, flight_row, train_row with context %} +{% from "macros.html" import display_date_no_year, display_date, conference_row, accommodation_row, flight_row, train_row with context %} {% set row = { "flight": flight_row, "train": train_row } %} @@ -38,11 +38,13 @@ {% macro section(heading, item_list, badge) %} {% if item_list %} + {% set items = item_list | list %}

{{ heading }}

- {% for trip in item_list %} +

{{ items | count }} trips

+ {% for trip in items %} {% set end = trip.end %}
-

{{ trip.title }} ({{ trip.start.strftime("%b %Y") }})

+

{{ trip.title }} ({{ display_date(trip.start) }})

Countries: {{ trip.countries_str }}
{% if end %}
Dates: {{ display_date_no_year(trip.start) }} to {{ display_date_no_year(end) }}
From acbad39df7a3c8c60dab2616b7479193eccdb258 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Mon, 8 Jan 2024 15:18:28 +0000 Subject: [PATCH 013/449] Download bank-holidays.json if the local copy is unreadable --- agenda/uk_holiday.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/agenda/uk_holiday.py b/agenda/uk_holiday.py index d1ae03e..5d4a5e0 100644 --- a/agenda/uk_holiday.py +++ b/agenda/uk_holiday.py @@ -8,7 +8,7 @@ from time import time import httpx from dateutil.easter import easter -from .types import Holiday +from .types import Holiday, StrDict async def bank_holiday_list( @@ -17,13 +17,23 @@ async def bank_holiday_list( """Date and name of the next UK bank holiday.""" url = "https://www.gov.uk/bank-holidays.json" filename = os.path.join(data_dir, "bank-holidays.json") - mtime = os.path.getmtime(filename) - if (time() - mtime) > 60 * 60 * 6: # six hours + use_cached = False + events: list[StrDict] + if os.path.exists(filename): + mtime = os.path.getmtime(filename) + if (time() - mtime) < 60 * 60 * 6: # six hours + use_cached = True + try: + events = json.load(open(filename))["england-and-wales"]["events"] + except json.decoder.JSONDecodeError: + use_cached = False + + if not use_cached: async with httpx.AsyncClient() as client: r = await client.get(url) open(filename, "w").write(r.text) + events = json.load(open(filename))["england-and-wales"]["events"] - events = json.load(open(filename))["england-and-wales"]["events"] hols: list[Holiday] = [] for event in events: event_date = datetime.strptime(event["date"], "%Y-%m-%d").date() From 3d16e30aa8bd1e8d5888a40f74da99d8816d8b2c Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Mon, 8 Jan 2024 15:19:20 +0000 Subject: [PATCH 014/449] Try and make mypy happy about types --- agenda/data.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/agenda/data.py b/agenda/data.py index 04de858..56ade71 100644 --- a/agenda/data.py +++ b/agenda/data.py @@ -267,9 +267,13 @@ def find_markets_during_stay( """Market events that happen during accommodation stays.""" overlapping_markets = [] for market in markets: + market_date = market.as_date + assert isinstance(market_date, date) for e in accommodation_events: + start, end = e.as_date, e.end_as_date + assert start and end and all(isinstance(i, date) for i in (start, end)) # Check if the market date is within the accommodation dates. - if e.as_date <= market.as_date <= e.end_as_date: + if start <= market_date <= end: overlapping_markets.append(market) break # Breaks the inner loop if overlap is found. return overlapping_markets From cd0ffb33909aea2f865d0ae21761e465cba4a2a1 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Mon, 8 Jan 2024 15:20:48 +0000 Subject: [PATCH 015/449] Hide LHG run club when on a trip Closes: #95 --- agenda/data.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/agenda/data.py b/agenda/data.py index 56ade71..a75a848 100644 --- a/agenda/data.py +++ b/agenda/data.py @@ -261,7 +261,7 @@ def read_events_yaml(data_dir: str, start: date, end: date) -> list[Event]: return events -def find_markets_during_stay( +def find_events_during_stay( accommodation_events: list[Event], markets: list[Event] ) -> list[Event]: """Market events that happen during accommodation stays.""" @@ -451,11 +451,15 @@ async def get_data( events += domains.renewal_dates(my_data) # hide markets that happen while away - markets = [e for e in events if e.name == "market"] + optional = [ + e + for e in events + if e.name == "market" or (e.title and "LHG Run Club" in e.title) + ] going = [e for e in events if e.going] - overlapping_markets = find_markets_during_stay( - accommodation_events + going, markets + overlapping_markets = find_events_during_stay( + accommodation_events + going, optional ) for market in overlapping_markets: events.remove(market) From 1453c4015c71f2fde3971f4632348cc92048fb86 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Mon, 8 Jan 2024 15:22:16 +0000 Subject: [PATCH 016/449] Show timings for index page data gathering --- agenda/data.py | 63 ++++++++++++++++++++++++++++++-------------- templates/index.html | 6 +++++ 2 files changed, 49 insertions(+), 20 deletions(-) diff --git a/agenda/data.py b/agenda/data.py index a75a848..536e5e8 100644 --- a/agenda/data.py +++ b/agenda/data.py @@ -6,6 +6,7 @@ import itertools import os import typing from datetime import date, datetime, timedelta +from time import time import dateutil.rrule import dateutil.tz @@ -353,6 +354,19 @@ def busy_event(e: Event) -> bool: return "rebels" not in lc_title and "south west data social" not in lc_title +async def time_function( + name: str, + func: typing.Callable[..., typing.Coroutine[typing.Any, typing.Any, typing.Any]], + *args, + **kwargs, +) -> tuple[str, typing.Any, float]: + """Time the execution of an asynchronous function.""" + start_time = time() + result = await func(*args, **kwargs) + end_time = time() + return name, result, end_time - start_time + + async def get_data( now: datetime, config: flask.config.Config ) -> typing.Mapping[str, str | object]: @@ -369,28 +383,37 @@ async def get_data( minus_365 = now - timedelta(days=365) plus_365 = now + timedelta(days=365) - ( - gbpusd, - gwr_advance_tickets, - bank_holiday, - rockets, - backwell_bins, - bristol_bins, - ) = await asyncio.gather( - fx.get_gbpusd(config), - gwr.advance_ticket_date(data_dir), - uk_holiday.bank_holiday_list(last_year, next_year, data_dir), - thespacedevs.get_launches(rocket_dir, limit=40), - waste_collection_events(data_dir), - bristol_waste_collection_events(data_dir, today), + t0 = time() + result_list = await asyncio.gather( + time_function("gbpusd", fx.get_gbpusd, config), + time_function("gwr_advance_tickets", gwr.advance_ticket_date, data_dir), + time_function( + "bank_holiday", uk_holiday.bank_holiday_list, last_year, next_year, data_dir + ), + time_function("rockets", thespacedevs.get_launches, rocket_dir, limit=40), + time_function("backwell_bins", waste_collection_events, data_dir), + time_function("bristol_bins", bristol_waste_collection_events, data_dir, today), ) + results = {call[0]: call[1] for call in result_list} + + gwr_advance_tickets = results["gwr_advance_tickets"] + + data_gather_seconds = time() - t0 + t0 = time() + + stock_market_times = stock_market.open_and_close() + stock_market_times_seconds = time() - t0 + reply: dict[str, typing.Any] = { "now": now, - "gbpusd": gbpusd, - "stock_markets": stock_market.open_and_close(), - "rockets": rockets, + "gbpusd": results["gbpusd"], + "stock_markets": stock_market_times, + "rockets": results["rockets"], "gwr_advance_tickets": gwr_advance_tickets, + "data_gather_seconds": data_gather_seconds, + "stock_market_times_seconds": stock_market_times_seconds, + "timings": [(call[0], call[2]) for call in result_list], } my_data = config["PERSONAL_DATA"] @@ -409,7 +432,7 @@ async def get_data( us_hols = us_holidays(last_year, next_year) - holidays: list[Holiday] = bank_holiday + us_hols + holidays: list[Holiday] = results["bank_holiday"] + us_hols for country in ( "at", "be", @@ -441,7 +464,7 @@ async def get_data( events += accommodation_events events += travel.all_events(my_data) events += conference.get_list(os.path.join(my_data, "conferences.yaml")) - events += backwell_bins + bristol_bins + events += results["backwell_bins"] + results["bristol_bins"] events += read_events_yaml(my_data, last_year, next_year) events += subscription.get_events(os.path.join(my_data, "subscriptions.yaml")) events += economist.publication_dates(last_week, next_year) @@ -464,7 +487,7 @@ async def get_data( for market in overlapping_markets: events.remove(market) - for launch in rockets: + for launch in results["rockets"]: dt = None if launch["net_precision"] == "Day": diff --git a/templates/index.html b/templates/index.html index 5dcf8c3..818ec90 100644 --- a/templates/index.html +++ b/templates/index.html @@ -125,6 +125,12 @@
  • Bristol Sunrise: {{ sunrise.strftime("%H:%M:%S") }} / Sunset: {{ sunset.strftime("%H:%M:%S") }}
  • +
  • Data gather took {{ "%.1f" | format(data_gather_seconds) }} seconds
  • +
  • Stock market open/close took + {{ "%.1f" | format(stock_market_times_seconds) }} seconds
  • + {% for name, seconds in timings %} +
  • {{ name }} took {{ "%.1f" | format(seconds) }} seconds
  • + {% endfor %}

    Stock markets

    From 7456f7232534ddbb206743dd0760a6e8d00da52a Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Mon, 8 Jan 2024 15:43:31 +0000 Subject: [PATCH 017/449] Update Bristol bins from cron to save time Closes: #96 --- update_bristol_bins.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100755 update_bristol_bins.py diff --git a/update_bristol_bins.py b/update_bristol_bins.py new file mode 100755 index 0000000..9b489e9 --- /dev/null +++ b/update_bristol_bins.py @@ -0,0 +1,31 @@ +#!/usr/bin/python3 +"""Update waste schedule from Bristol City Council.""" + +import asyncio +import sys +from datetime import date +from time import time + +import agenda.types +import agenda.waste_schedule + +config = __import__("config.default", fromlist=[""]) + + +async def bristol_waste_collection_events() -> list[agenda.types.Event]: + """Waste colllection events.""" + uprn = "358335" + + return await agenda.waste_schedule.get_bristol_gov_uk( + date.today(), config.DATA_DIR, uprn, refresh=True + ) + + +today = date.today() +t0 = time() +events = asyncio.run(bristol_waste_collection_events()) +time_taken = time() - t0 +if sys.stdin.isatty(): + for event in events: + print(event) + print(f"took {time_taken:.1f} seconds") From 199eb82bcee732f5f59c097fc65c5a1dde166e49 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Mon, 8 Jan 2024 15:45:08 +0000 Subject: [PATCH 018/449] Add refresh option for Bristol waste schedule --- agenda/waste_schedule.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/agenda/waste_schedule.py b/agenda/waste_schedule.py index 4f4dade..3e3f253 100644 --- a/agenda/waste_schedule.py +++ b/agenda/waste_schedule.py @@ -104,7 +104,9 @@ def parse(root: lxml.html.HtmlElement) -> list[Event]: BristolSchedule = list[dict[str, typing.Any]] -async def get_bristol_data(data_dir: str, uprn: str) -> BristolSchedule: +async def get_bristol_data( + data_dir: str, uprn: str, refresh: bool = False +) -> BristolSchedule: """Get Bristol Waste schedule, with cache.""" now = datetime.now() waste_dir = os.path.join(data_dir, "waste") @@ -122,7 +124,7 @@ async def get_bristol_data(data_dir: str, uprn: str) -> BristolSchedule: json_data = json.load(open(os.path.join(waste_dir, recent_filename))) return typing.cast(BristolSchedule, json_data["data"]) - if existing and delta < timedelta(hours=ttl_hours): + if not refresh and existing and delta < timedelta(hours=ttl_hours): return get_from_recent() try: @@ -186,9 +188,11 @@ async def get_bristol_gov_uk_data(uprn: str) -> httpx.Response: return response -async def get_bristol_gov_uk(start_date: date, data_dir: str, uprn: str) -> list[Event]: +async def get_bristol_gov_uk( + start_date: date, data_dir: str, uprn: str, refresh: bool = False +) -> list[Event]: """Get waste collection schedule from Bristol City Council.""" - data = await get_bristol_data(data_dir, uprn) + data = await get_bristol_data(data_dir, uprn, refresh) by_date: defaultdict[date, list[str]] = defaultdict(list) From 82de51109f5ccc705e6c05c9f9501fb8eded55ff Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Wed, 10 Jan 2024 13:06:29 +0000 Subject: [PATCH 019/449] import datetime --- agenda/types.py | 65 +++++++++++++++++++++++++++++-------------------- 1 file changed, 38 insertions(+), 27 deletions(-) diff --git a/agenda/types.py b/agenda/types.py index 1e0ba44..293e977 100644 --- a/agenda/types.py +++ b/agenda/types.py @@ -1,8 +1,8 @@ """Types.""" +import datetime import typing from dataclasses import dataclass, field -from datetime import date, datetime, timezone from pycountry.db import Country @@ -10,13 +10,14 @@ import agenda from agenda import format_list_with_ampersand StrDict = dict[str, typing.Any] +DateOrDateTime = datetime.datetime | datetime.date -def as_date(d: datetime | date) -> date: +def as_date(d: DateOrDateTime) -> datetime.date: """Convert datetime to date.""" - if isinstance(d, datetime): + if isinstance(d, datetime.datetime): return d.date() - assert isinstance(d, date) + assert isinstance(d, datetime.date) return d @@ -24,7 +25,7 @@ def as_date(d: datetime | date) -> date: class Trip: """Trip.""" - start: date + start: datetime.date travel: list[StrDict] = field(default_factory=list) accommodation: list[StrDict] = field(default_factory=list) conferences: list[StrDict] = field(default_factory=list) @@ -38,21 +39,21 @@ class Trip: ) @property - def end(self) -> date | None: + def end(self) -> datetime.date | None: """End date for trip.""" max_conference_end = ( max(as_date(item["end"]) for item in self.conferences) if self.conferences - else date.min + else datetime.date.min ) - assert isinstance(max_conference_end, date) + assert isinstance(max_conference_end, datetime.date) arrive = [item["arrive"].date() for item in self.travel if "arrive" in item] - travel_end = max(arrive) if arrive else date.min - assert isinstance(travel_end, date) + travel_end = max(arrive) if arrive else datetime.date.min + assert isinstance(travel_end, datetime.date) max_date = max(max_conference_end, travel_end) - return max_date if max_date != date.min else None + return max_date if max_date != datetime.date.min else None @property def countries(self) -> list[Country]: @@ -85,7 +86,7 @@ class Holiday: name: str country: str - date: date + date: datetime.date @dataclass @@ -93,40 +94,42 @@ class Event: """Event.""" name: str - date: date | datetime - end_date: date | datetime | None = None + date: DateOrDateTime + end_date: DateOrDateTime | None = None title: str | None = None url: str | None = None going: bool | None = None @property - def as_datetime(self) -> datetime: + def as_datetime(self) -> datetime.datetime: """Date/time of event.""" d = self.date - t0 = datetime.min.time() + t0 = datetime.datetime.min.time() return ( d - if isinstance(d, datetime) - else datetime.combine(d, t0).replace(tzinfo=timezone.utc) + if isinstance(d, datetime.datetime) + else datetime.datetime.combine(d, t0).replace(tzinfo=datetime.timezone.utc) ) @property def has_time(self) -> bool: """Event has a time associated with it.""" - return isinstance(self.date, datetime) + return isinstance(self.date, datetime.datetime) @property - def as_date(self) -> date: + def as_date(self) -> datetime.date: """Date of event.""" - return self.date.date() if isinstance(self.date, datetime) else self.date + return ( + self.date.date() if isinstance(self.date, datetime.datetime) else self.date + ) @property - def end_as_date(self) -> date: + def end_as_date(self) -> datetime.date: """Date of event.""" return ( ( self.end_date.date() - if isinstance(self.end_date, datetime) + if isinstance(self.end_date, datetime.datetime) else self.end_date ) if self.end_date @@ -136,14 +139,22 @@ class Event: @property def display_time(self) -> str | None: """Time for display on web page.""" - return self.date.strftime("%H:%M") if isinstance(self.date, datetime) else None + return ( + self.date.strftime("%H:%M") + if isinstance(self.date, datetime.datetime) + else None + ) @property def display_timezone(self) -> str | None: """Timezone for display on web page.""" - return self.date.strftime("%z") if isinstance(self.date, datetime) else None + return ( + self.date.strftime("%z") + if isinstance(self.date, datetime.datetime) + else None + ) - def delta_days(self, today: date) -> str: + def delta_days(self, today: datetime.date) -> str: """Return number of days from today as a string.""" delta = (self.as_date - today).days @@ -158,7 +169,7 @@ class Event: @property def display_date(self) -> str: """Date for display on web page.""" - if isinstance(self.date, datetime): + if isinstance(self.date, datetime.datetime): return self.date.strftime("%a, %d, %b %Y %H:%M %z") else: return self.date.strftime("%a, %d, %b %Y") From 8504a3a02210310ede813a656fae93daf2122a04 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Wed, 10 Jan 2024 13:26:59 +0000 Subject: [PATCH 020/449] Add radarbox.com links for flights --- templates/macros.html | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/templates/macros.html b/templates/macros.html index a9d7364..f7926e6 100644 --- a/templates/macros.html +++ b/templates/macros.html @@ -68,6 +68,8 @@ {% endmacro %} {% macro flight_row(item) %} + {% set full_flight_number = item.airline + item.flight_number %} + {% set url = "https://www.radarbox.com/data/flights/" + full_flight_number %}
    {{ item.depart.strftime("%a, %d %b %Y") }}
    {{ item.from }} → {{ item.to }}
    {{ item.depart.strftime("%H:%M") }}
    @@ -78,7 +80,9 @@ {% endif %}
    {{ item.duration }}
    -
    {{ item.airline }}{{ item.flight_number }}
    +
    {{ item.booking_reference }}
    {% endmacro %} From ad47f291f8f32a8fbd2a770ba0c7fbacfed32bd9 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Wed, 10 Jan 2024 13:27:25 +0000 Subject: [PATCH 021/449] Add events to trips --- agenda/types.py | 10 +++++++--- web_view.py | 1 + 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/agenda/types.py b/agenda/types.py index 293e977..84cbc59 100644 --- a/agenda/types.py +++ b/agenda/types.py @@ -29,13 +29,17 @@ class Trip: travel: list[StrDict] = field(default_factory=list) accommodation: list[StrDict] = field(default_factory=list) conferences: list[StrDict] = field(default_factory=list) + events: list[StrDict] = field(default_factory=list) @property def title(self) -> str: """Trip title.""" return ( - format_list_with_ampersand([conf["name"] for conf in self.conferences]) - or "[no conference]" + format_list_with_ampersand( + [conf["name"] for conf in self.conferences] + + [event["title"] for event in self.events] + ) + or "[unnamed trip]" ) @property @@ -60,7 +64,7 @@ class Trip: """Countries visited as part of trip, in order.""" seen: set[str] = set() items: list[Country] = [] - for item in self.conferences + self.accommodation: + for item in self.conferences + self.accommodation + self.events: if "country" not in item: continue if item["country"] in seen: diff --git a/web_view.py b/web_view.py index 5cc1d89..2ee8b01 100755 --- a/web_view.py +++ b/web_view.py @@ -178,6 +178,7 @@ def build_trip_list() -> list[Trip]: "travel": travel_items, "accommodation": travel.parse_yaml("accommodation", data_dir), "conferences": travel.parse_yaml("conferences", data_dir), + "events": travel.parse_yaml("events", data_dir), } for key, item_list in data.items(): From 2744f6798718db09e6ffd35767cc4f2a9fa3820a Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Fri, 12 Jan 2024 14:04:06 +0000 Subject: [PATCH 022/449] Add pages for individual trips Closes: #100 --- templates/trip_page.html | 71 ++++++++++++++++++++++++++++++++++++++++ templates/trips.html | 4 ++- web_view.py | 33 ++++++++++++++++++- 3 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 templates/trip_page.html diff --git a/templates/trip_page.html b/templates/trip_page.html new file mode 100644 index 0000000..43b9ed5 --- /dev/null +++ b/templates/trip_page.html @@ -0,0 +1,71 @@ +{% extends "base.html" %} + +{% from "macros.html" import display_date_no_year, display_date, conference_row, accommodation_row, flight_row, train_row with context %} + +{% set row = { "flight": flight_row, "train": train_row } %} + +{% block style %} +{% set conference_column_count = 6 %} +{% set accommodation_column_count = 7 %} +{% set travel_column_count = 7 %} + +{% endblock %} + +{% set end = trip.end %} + +{% block content %} +
    +

    {{ trip.title }}({{ display_date(trip.start) }})

    +
    Countries: {{ trip.countries_str }}
    + {% if end %} +
    Dates: {{ display_date_no_year(trip.start) }} to {{ display_date_no_year(end) }}
    + {% else %} +
    Start: {{ display_date_no_year(trip.start) }} (end date missing)
    + {% endif %} +
    + {% for conf in trip.conferences %} + {{ conference_row(conf, "going") }} + {% endfor %} +
    + +
    + {% for conf in trip.accommodation %} + {{ accommodation_row(conf, "going") }} + {% endfor %} +
    + +
    + {% for item in trip.travel %} + {{ row[item.type](item) }} + {% endfor %} +
    + + {#
    {{ trip | pprint }}
    #} + +
    +{% endblock %} diff --git a/templates/trips.html b/templates/trips.html index e2c45bd..7da70cb 100644 --- a/templates/trips.html +++ b/templates/trips.html @@ -44,7 +44,9 @@ {% for trip in items %} {% set end = trip.end %}
    -

    {{ trip.title }} ({{ display_date(trip.start) }})

    +

    + {{ trip.title }} + ({{ display_date(trip.start) }})

    Countries: {{ trip.countries_str }}
    {% if end %}
    Dates: {{ display_date_no_year(trip.start) }} to {{ display_date_no_year(end) }}
    diff --git a/web_view.py b/web_view.py index 2ee8b01..058f09e 100755 --- a/web_view.py +++ b/web_view.py @@ -170,8 +170,24 @@ def build_trip_list() -> list[Trip]: data_dir = app.config["PERSONAL_DATA"] + stations = travel.parse_yaml("stations", data_dir) + by_name = {station["name"]: station for station in stations} + + trains = load_travel("train") + for train in trains: + assert train["from"] in by_name + assert train["to"] in by_name + train["from_station"] = by_name[train["from"]] + train["to_station"] = by_name[train["to"]] + + for leg in train["legs"]: + assert leg["from"] in by_name + assert leg["to"] in by_name + leg["from_station"] = by_name[train["from"]] + leg["to_station"] = by_name[train["to"]] + travel_items = sorted( - load_travel("flight") + load_travel("train"), key=operator.itemgetter("depart") + load_travel("flight") + trains, key=operator.itemgetter("depart") ) data = { @@ -219,5 +235,20 @@ def trip_list() -> str: ) +@app.route("/trip/") +def trip_page(start: str) -> str: + trip_list = build_trip_list() + today = date.today() + + trip = next((trip for trip in trip_list if trip.start.isoformat() == start), None) + return flask.render_template( + "trip_page.html", + trip=trip, + today=today, + get_country=agenda.get_country, + format_list_with_ampersand=format_list_with_ampersand, + ) + + if __name__ == "__main__": app.run(host="0.0.0.0") From a9c9c719a44c1cfc6af587288b2974507ab2833a Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Fri, 12 Jan 2024 14:08:36 +0000 Subject: [PATCH 023/449] Return 404 not found for invalid trip IDs Closes: #103 --- web_view.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/web_view.py b/web_view.py index 058f09e..1bbfbc9 100755 --- a/web_view.py +++ b/web_view.py @@ -237,10 +237,15 @@ def trip_list() -> str: @app.route("/trip/") def trip_page(start: str) -> str: + """Individual trip page.""" trip_list = build_trip_list() today = date.today() trip = next((trip for trip in trip_list if trip.start.isoformat() == start), None) + + if not trip: + flask.abort(404) + return flask.render_template( "trip_page.html", trip=trip, From 60070d07fdc02d7272f278db8b9b44aa09c8b3a4 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Fri, 12 Jan 2024 15:04:08 +0000 Subject: [PATCH 024/449] Add maps to trip pages Closes: #102 --- templates/trip_page.html | 43 +++++++++++++++++++++++++++++++++++++++- web_view.py | 23 +++++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/templates/trip_page.html b/templates/trip_page.html index 43b9ed5..0ea5ac5 100644 --- a/templates/trip_page.html +++ b/templates/trip_page.html @@ -5,6 +5,13 @@ {% set row = { "flight": flight_row, "train": train_row } %} {% block style %} + +{% if station_coordinates %} + +{% endif %} + {% set conference_column_count = 6 %} {% set accommodation_column_count = 7 %} {% set travel_column_count = 7 %} @@ -33,6 +40,11 @@ .grid-item { /* Additional styling for grid items can go here */ } + +#map { + height: 400px; +} + {% endblock %} @@ -65,7 +77,36 @@ {% endfor %}
    - {#
    {{ trip | pprint }}
    #} + {% if station_coordinates %} +
    + {% endif %}
    {% endblock %} + +{% block scripts %} + +{% if station_coordinates %} + + + +{% endif %} +{% endblock %} diff --git a/web_view.py b/web_view.py index 1bbfbc9..8bb52ec 100755 --- a/web_view.py +++ b/web_view.py @@ -235,6 +235,26 @@ def trip_list() -> str: ) +def collect_station_coordinates(trip: Trip) -> list[tuple[float, float]]: + """Extract and deduplicate station coordinates from trip.""" + stations = {} + station_list = [] + for t in trip.travel: + if t["type"] != "train": + continue + station_list += [t["from_station"], t["to_station"]] + for leg in t["legs"]: + station_list.append(leg["from_station"]) + station_list.append(leg["to_station"]) + + for s in station_list: + if s["uic"] in stations: + continue + stations[s["uic"]] = s + + return [(s["latitude"], s["longitude"]) for s in stations.values()] + + @app.route("/trip/") def trip_page(start: str) -> str: """Individual trip page.""" @@ -246,10 +266,13 @@ def trip_page(start: str) -> str: if not trip: flask.abort(404) + station_coordinates = collect_station_coordinates(trip) + return flask.render_template( "trip_page.html", trip=trip, today=today, + station_coordinates=station_coordinates, get_country=agenda.get_country, format_list_with_ampersand=format_list_with_ampersand, ) From 0c02d9c899def8e7139fb5edabb6c4e1c4c5ad55 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Fri, 12 Jan 2024 16:20:36 +0000 Subject: [PATCH 025/449] Include airport pins on the map --- web_view.py | 55 ++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 42 insertions(+), 13 deletions(-) diff --git a/web_view.py b/web_view.py index 8bb52ec..7c612d7 100755 --- a/web_view.py +++ b/web_view.py @@ -3,6 +3,7 @@ """Web page to show upcoming events.""" import inspect +import itertools import operator import os.path import sys @@ -164,16 +165,14 @@ def load_travel(travel_type: str) -> list[StrDict]: return items -def build_trip_list() -> list[Trip]: - """Generate list of trips.""" - trips: dict[date, Trip] = {} - +def load_trains() -> list[StrDict]: + """Load trains.""" data_dir = app.config["PERSONAL_DATA"] + trains = load_travel("train") stations = travel.parse_yaml("stations", data_dir) by_name = {station["name"]: station for station in stations} - trains = load_travel("train") for train in trains: assert train["from"] in by_name assert train["to"] in by_name @@ -186,8 +185,30 @@ def build_trip_list() -> list[Trip]: leg["from_station"] = by_name[train["from"]] leg["to_station"] = by_name[train["to"]] + return trains + + +def load_flights() -> list[StrDict]: + """Load flights.""" + data_dir = app.config["PERSONAL_DATA"] + flights = load_travel("flight") + airports = travel.parse_yaml("airports", data_dir) + for flight in flights: + if flight["from"] in airports: + flight["from_airport"] = airports[flight["from"]] + if flight["to"] in airports: + flight["to_airport"] = airports[flight["to"]] + return flights + + +def build_trip_list() -> list[Trip]: + """Generate list of trips.""" + trips: dict[date, Trip] = {} + + data_dir = app.config["PERSONAL_DATA"] + travel_items = sorted( - load_travel("flight") + trains, key=operator.itemgetter("depart") + load_flights() + load_trains(), key=operator.itemgetter("depart") ) data = { @@ -239,20 +260,28 @@ def collect_station_coordinates(trip: Trip) -> list[tuple[float, float]]: """Extract and deduplicate station coordinates from trip.""" stations = {} station_list = [] + airports = {} for t in trip.travel: - if t["type"] != "train": - continue - station_list += [t["from_station"], t["to_station"]] - for leg in t["legs"]: - station_list.append(leg["from_station"]) - station_list.append(leg["to_station"]) + if t["type"] == "train": + station_list += [t["from_station"], t["to_station"]] + for leg in t["legs"]: + station_list.append(leg["from_station"]) + station_list.append(leg["to_station"]) + else: + assert t["type"] == "flight" + for field in "from_airport", "to_airport": + if field in t: + airports[t[field]["iata"]] = t[field] for s in station_list: if s["uic"] in stations: continue stations[s["uic"]] = s - return [(s["latitude"], s["longitude"]) for s in stations.values()] + return [ + (s["latitude"], s["longitude"]) + for s in itertools.chain(stations.values(), airports.values()) + ] @app.route("/trip/") From 4b8b1f755631bd53ee61955030745a7a8b2caa76 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Fri, 12 Jan 2024 16:54:52 +0000 Subject: [PATCH 026/449] Show station and airport icons on the map --- templates/trip_page.html | 32 ++++++++++++++++++++++++-------- web_view.py | 30 +++++++++++++++++++++--------- 2 files changed, 45 insertions(+), 17 deletions(-) diff --git a/templates/trip_page.html b/templates/trip_page.html index 0ea5ac5..13a6c88 100644 --- a/templates/trip_page.html +++ b/templates/trip_page.html @@ -6,7 +6,7 @@ {% block style %} -{% if station_coordinates %} +{% if coordinates %} @@ -77,7 +77,7 @@ {% endfor %}
    - {% if station_coordinates %} + {% if coordinates %}
    {% endif %} @@ -86,25 +86,41 @@ {% block scripts %} -{% if station_coordinates %} +{% if coordinates %} diff --git a/web_view.py b/web_view.py index 7c612d7..c771b0a 100755 --- a/web_view.py +++ b/web_view.py @@ -3,7 +3,6 @@ """Web page to show upcoming events.""" import inspect -import itertools import operator import os.path import sys @@ -182,8 +181,8 @@ def load_trains() -> list[StrDict]: for leg in train["legs"]: assert leg["from"] in by_name assert leg["to"] in by_name - leg["from_station"] = by_name[train["from"]] - leg["to_station"] = by_name[train["to"]] + leg["from_station"] = by_name[leg["from"]] + leg["to_station"] = by_name[leg["to"]] return trains @@ -256,8 +255,8 @@ def trip_list() -> str: ) -def collect_station_coordinates(trip: Trip) -> list[tuple[float, float]]: - """Extract and deduplicate station coordinates from trip.""" +def collect_trip_coordinates(trip: Trip) -> list[StrDict]: + """Extract and deduplicate airport and station coordinates from trip.""" stations = {} station_list = [] airports = {} @@ -279,8 +278,21 @@ def collect_station_coordinates(trip: Trip) -> list[tuple[float, float]]: stations[s["uic"]] = s return [ - (s["latitude"], s["longitude"]) - for s in itertools.chain(stations.values(), airports.values()) + { + "name": s["name"], + "type": "station", + "latitude": s["latitude"], + "longitude": s["longitude"], + } + for s in stations.values() + ] + [ + { + "name": s["name"], + "type": "airport", + "latitude": s["latitude"], + "longitude": s["longitude"], + } + for s in airports.values() ] @@ -295,13 +307,13 @@ def trip_page(start: str) -> str: if not trip: flask.abort(404) - station_coordinates = collect_station_coordinates(trip) + coordinates = collect_trip_coordinates(trip) return flask.render_template( "trip_page.html", trip=trip, today=today, - station_coordinates=station_coordinates, + coordinates=coordinates, get_country=agenda.get_country, format_list_with_ampersand=format_list_with_ampersand, ) From e9933299399f4c613ef0dc70118e693ee501c743 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Fri, 12 Jan 2024 17:17:12 +0000 Subject: [PATCH 027/449] Show lines connecting transport stops on map Closes: #104 --- templates/trip_page.html | 6 ++++++ web_view.py | 27 +++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/templates/trip_page.html b/templates/trip_page.html index 13a6c88..1b8f2e0 100644 --- a/templates/trip_page.html +++ b/templates/trip_page.html @@ -93,6 +93,7 @@ {% endif %} {% endblock %} diff --git a/web_view.py b/web_view.py index c771b0a..d53d433 100755 --- a/web_view.py +++ b/web_view.py @@ -296,6 +296,31 @@ def collect_trip_coordinates(trip: Trip) -> list[StrDict]: ] +def latlon_tuple(stop: StrDict) -> tuple[float, float]: + """Given a transport stop return the lat/lon as a tuple.""" + return (stop["latitude"], stop["longitude"]) + + +def get_trip_routes( + trip: Trip, +) -> list[tuple[tuple[float, float], tuple[float, float]]]: + routes = [] + for t in trip.travel: + if t["type"] == "flight": + if "from_airport" not in t or "to_airport" not in t: + continue + fly_from, fly_to = t["from_airport"], t["to_airport"] + routes.append((latlon_tuple(fly_from), latlon_tuple(fly_to))) + + else: + assert t["type"] == "train" + for leg in t["legs"]: + train_from, train_to = leg["from_station"], leg["to_station"] + routes.append((latlon_tuple(train_from), latlon_tuple(train_to))) + + return routes + + @app.route("/trip/") def trip_page(start: str) -> str: """Individual trip page.""" @@ -308,12 +333,14 @@ def trip_page(start: str) -> str: flask.abort(404) coordinates = collect_trip_coordinates(trip) + routes = get_trip_routes(trip) return flask.render_template( "trip_page.html", trip=trip, today=today, coordinates=coordinates, + routes=routes, get_country=agenda.get_country, format_list_with_ampersand=format_list_with_ampersand, ) From 4b62ec96dcf52f4e11fb7fb345e601d8e29a3dca Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Fri, 12 Jan 2024 19:43:22 +0000 Subject: [PATCH 028/449] Make the map bigger --- templates/trip_page.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/trip_page.html b/templates/trip_page.html index 1b8f2e0..ab039cd 100644 --- a/templates/trip_page.html +++ b/templates/trip_page.html @@ -42,7 +42,7 @@ } #map { - height: 400px; + height: 100vh; } From 4e719a07abcb9b677714b736cde220919d2efa89 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Fri, 12 Jan 2024 19:52:00 +0000 Subject: [PATCH 029/449] Show flights and trains in different colours --- templates/trip_page.html | 3 ++- web_view.py | 8 +++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/templates/trip_page.html b/templates/trip_page.html index ab039cd..f7c6e14 100644 --- a/templates/trip_page.html +++ b/templates/trip_page.html @@ -126,7 +126,8 @@ coordinates.forEach(function(item) { // Draw lines for routes routes.forEach(function(route) { - L.polyline(route, {color: 'blue'}).addTo(map); + var color = route[0] === "train" ? "green" : "red"; // Green for trains, red for flights + L.polyline([route[1], route[2]], {color: color}).addTo(map); }); diff --git a/web_view.py b/web_view.py index d53d433..18077c4 100755 --- a/web_view.py +++ b/web_view.py @@ -303,20 +303,22 @@ def latlon_tuple(stop: StrDict) -> tuple[float, float]: def get_trip_routes( trip: Trip, -) -> list[tuple[tuple[float, float], tuple[float, float]]]: +) -> list[tuple[str, tuple[float, float], tuple[float, float]]]: routes = [] for t in trip.travel: if t["type"] == "flight": if "from_airport" not in t or "to_airport" not in t: continue fly_from, fly_to = t["from_airport"], t["to_airport"] - routes.append((latlon_tuple(fly_from), latlon_tuple(fly_to))) + routes.append(("flight", latlon_tuple(fly_from), latlon_tuple(fly_to))) else: assert t["type"] == "train" for leg in t["legs"]: train_from, train_to = leg["from_station"], leg["to_station"] - routes.append((latlon_tuple(train_from), latlon_tuple(train_to))) + routes.append( + ("train", latlon_tuple(train_from), latlon_tuple(train_to)) + ) return routes From e2fdd1d19824a7b281651fedcbdf5d7635b8cdf1 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Fri, 12 Jan 2024 22:29:10 +0000 Subject: [PATCH 030/449] Show rail routes using GeoJSON --- templates/trip_page.html | 16 +++++++++++++--- web_view.py | 38 +++++++++++++++++++++++++++++++++----- 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/templates/trip_page.html b/templates/trip_page.html index f7c6e14..b49bb6f 100644 --- a/templates/trip_page.html +++ b/templates/trip_page.html @@ -124,10 +124,20 @@ coordinates.forEach(function(item) { marker.bindPopup(item.name); }); -// Draw lines for routes +// Draw routes routes.forEach(function(route) { - var color = route[0] === "train" ? "green" : "red"; // Green for trains, red for flights - L.polyline([route[1], route[2]], {color: color}).addTo(map); + if (route.geojson) { + // If route is defined as GeoJSON + L.geoJSON(JSON.parse(route.geojson), { + style: function(feature) { + return {color: route.type === "train" ? "green" : "blue"}; // Green for trains, blue for flights + } + }).addTo(map); + } else { + // If route is defined by 'from' and 'to' coordinates + var color = route.type === "train" ? "green" : "red"; // Green for trains, red for flights + L.polyline([route.from, route.to], {color: color}).addTo(map); + } }); diff --git a/web_view.py b/web_view.py index 18077c4..c4557f3 100755 --- a/web_view.py +++ b/web_view.py @@ -301,23 +301,51 @@ def latlon_tuple(stop: StrDict) -> tuple[float, float]: return (stop["latitude"], stop["longitude"]) -def get_trip_routes( - trip: Trip, -) -> list[tuple[str, tuple[float, float], tuple[float, float]]]: +def read_geojson(filename: str) -> str: + data_dir = app.config["PERSONAL_DATA"] + return open(os.path.join(data_dir, "train_routes", filename + ".geojson")).read() + + +def get_trip_routes(trip: Trip) -> list[StrDict]: routes = [] + seen_geojson = set() for t in trip.travel: if t["type"] == "flight": if "from_airport" not in t or "to_airport" not in t: continue fly_from, fly_to = t["from_airport"], t["to_airport"] - routes.append(("flight", latlon_tuple(fly_from), latlon_tuple(fly_to))) + routes.append( + { + "type": "flight", + "from": latlon_tuple(fly_from), + "to": latlon_tuple(fly_to), + } + ) else: assert t["type"] == "train" for leg in t["legs"]: train_from, train_to = leg["from_station"], leg["to_station"] + geojson_filename = train_from.get("routes", {}).get(train_to["uic"]) + if not geojson_filename: + routes.append( + { + "type": "train", + "from": latlon_tuple(train_from), + "to": latlon_tuple(train_to), + } + ) + continue + + if geojson_filename in seen_geojson: + continue + seen_geojson.add(geojson_filename) + routes.append( - ("train", latlon_tuple(train_from), latlon_tuple(train_to)) + { + "type": "train", + "geojson": read_geojson(geojson_filename), + } ) return routes From cb2a2c7fb868b5775c4a620f62e90115a28b1cfd Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sat, 13 Jan 2024 18:20:02 +0000 Subject: [PATCH 031/449] Change colour of rail journey on map to blue --- templates/trip_page.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/trip_page.html b/templates/trip_page.html index b49bb6f..2924746 100644 --- a/templates/trip_page.html +++ b/templates/trip_page.html @@ -130,12 +130,12 @@ routes.forEach(function(route) { // If route is defined as GeoJSON L.geoJSON(JSON.parse(route.geojson), { style: function(feature) { - return {color: route.type === "train" ? "green" : "blue"}; // Green for trains, blue for flights + return {color: route.type === "train" ? "blue" : "blue"}; // Green for trains, blue for flights } }).addTo(map); } else { // If route is defined by 'from' and 'to' coordinates - var color = route.type === "train" ? "green" : "red"; // Green for trains, red for flights + var color = route.type === "train" ? "blue" : "red"; // Green for trains, red for flights L.polyline([route.from, route.to], {color: color}).addTo(map); } }); From e0735b41858a5072eead43d4c46a7f303650572a Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sun, 14 Jan 2024 08:04:05 +0000 Subject: [PATCH 032/449] Refresh space launches from cron because API is slow Closes: #98 --- agenda/thespacedevs.py | 8 ++++++-- update_thespacedevs.py | 26 ++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) create mode 100755 update_thespacedevs.py diff --git a/agenda/thespacedevs.py b/agenda/thespacedevs.py index 6ece23c..23e2887 100644 --- a/agenda/thespacedevs.py +++ b/agenda/thespacedevs.py @@ -10,6 +10,8 @@ import httpx Launch = dict[str, typing.Any] Summary = dict[str, typing.Any] +ttl = 60 * 60 * 2 # two hours + async def next_launch_api(rocket_dir: str, limit: int = 200) -> list[Launch]: """Get the next upcoming launches from the API.""" @@ -137,14 +139,16 @@ def summarize_launch(launch: Launch) -> Summary: } -async def get_launches(rocket_dir: str, limit: int = 200) -> list[Summary]: +async def get_launches( + rocket_dir: str, limit: int = 200, refresh: bool = False +) -> list[Summary]: """Get rocket launches with caching.""" now = datetime.now() existing = [x for x in (filename_timestamp(f) for f in os.listdir(rocket_dir)) if x] existing.sort(reverse=True) - if not existing or (now - existing[0][0]).seconds > 3600: # one hour + if refresh or not existing or (now - existing[0][0]).seconds > ttl: try: return await next_launch_api(rocket_dir, limit=limit) except httpx.ReadTimeout: diff --git a/update_thespacedevs.py b/update_thespacedevs.py new file mode 100755 index 0000000..9ecec7e --- /dev/null +++ b/update_thespacedevs.py @@ -0,0 +1,26 @@ +#!/usr/bin/python3 +"""Update cache of space launch API.""" + +import asyncio +import os +import sys +from time import time + +import agenda.thespacedevs + +config = __import__("config.default", fromlist=[""]) +rocket_dir = os.path.join(config.DATA_DIR, "thespacedevs") + + +async def get_launches() -> list[agenda.thespacedevs.Launch]: + """Call space launch API and cache results.""" + return await agenda.thespacedevs.get_launches(rocket_dir, limit=200, refresh=True) + + +t0 = time() +rockets = asyncio.run(get_launches()) +time_taken = time() - t0 +if not sys.stdin.isatty(): + sys.exit(0) +print(len(rockets), "launches") +print(f"took {time_taken:.1f} seconds") From 4a990a9fe5d8496262771e0f77cae879eee5bd70 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sun, 14 Jan 2024 10:14:05 +0000 Subject: [PATCH 033/449] Move trip code into separate file --- agenda/trip.py | 180 +++++++++++++++++++++++++++++++++++++++++++++++++ web_view.py | 180 ++----------------------------------------------- 2 files changed, 185 insertions(+), 175 deletions(-) create mode 100644 agenda/trip.py diff --git a/agenda/trip.py b/agenda/trip.py new file mode 100644 index 0000000..6bc4a7d --- /dev/null +++ b/agenda/trip.py @@ -0,0 +1,180 @@ +import operator +import os +from datetime import date + +import flask + +from agenda import travel +from agenda.types import StrDict, Trip + + +def load_travel(travel_type: str) -> list[StrDict]: + """Read flight and train journeys.""" + data_dir = flask.current_app.config["PERSONAL_DATA"] + items = travel.parse_yaml(travel_type + "s", data_dir) + for item in items: + item["type"] = travel_type + return items + + +def load_trains() -> list[StrDict]: + """Load trains.""" + data_dir = flask.current_app.config["PERSONAL_DATA"] + + trains = load_travel("train") + stations = travel.parse_yaml("stations", data_dir) + by_name = {station["name"]: station for station in stations} + + for train in trains: + assert train["from"] in by_name + assert train["to"] in by_name + train["from_station"] = by_name[train["from"]] + train["to_station"] = by_name[train["to"]] + + for leg in train["legs"]: + assert leg["from"] in by_name + assert leg["to"] in by_name + leg["from_station"] = by_name[leg["from"]] + leg["to_station"] = by_name[leg["to"]] + + return trains + + +def load_flights() -> list[StrDict]: + """Load flights.""" + data_dir = flask.current_app.config["PERSONAL_DATA"] + flights = load_travel("flight") + airports = travel.parse_yaml("airports", data_dir) + for flight in flights: + if flight["from"] in airports: + flight["from_airport"] = airports[flight["from"]] + if flight["to"] in airports: + flight["to_airport"] = airports[flight["to"]] + return flights + + +def build_trip_list() -> list[Trip]: + """Generate list of trips.""" + trips: dict[date, Trip] = {} + + data_dir = flask.current_app.config["PERSONAL_DATA"] + + travel_items = sorted( + load_flights() + load_trains(), key=operator.itemgetter("depart") + ) + + data = { + "travel": travel_items, + "accommodation": travel.parse_yaml("accommodation", data_dir), + "conferences": travel.parse_yaml("conferences", data_dir), + "events": travel.parse_yaml("events", data_dir), + } + + for key, item_list in data.items(): + assert isinstance(item_list, list) + for item in item_list: + if not (start := item.get("trip")): + continue + if start not in trips: + trips[start] = Trip(start=start) + getattr(trips[start], key).append(item) + + return [trip for _, trip in sorted(trips.items())] + + +def collect_trip_coordinates(trip: Trip) -> list[StrDict]: + """Extract and deduplicate airport and station coordinates from trip.""" + stations = {} + station_list = [] + airports = {} + for t in trip.travel: + if t["type"] == "train": + station_list += [t["from_station"], t["to_station"]] + for leg in t["legs"]: + station_list.append(leg["from_station"]) + station_list.append(leg["to_station"]) + else: + assert t["type"] == "flight" + for field in "from_airport", "to_airport": + if field in t: + airports[t[field]["iata"]] = t[field] + + for s in station_list: + if s["uic"] in stations: + continue + stations[s["uic"]] = s + + return [ + { + "name": s["name"], + "type": "station", + "latitude": s["latitude"], + "longitude": s["longitude"], + } + for s in stations.values() + ] + [ + { + "name": s["name"], + "type": "airport", + "latitude": s["latitude"], + "longitude": s["longitude"], + } + for s in airports.values() + ] + + +def latlon_tuple(stop: StrDict) -> tuple[float, float]: + """Given a transport stop return the lat/lon as a tuple.""" + return (stop["latitude"], stop["longitude"]) + + +def read_geojson(filename: str) -> str: + """Read GeoJSON from file.""" + data_dir = flask.current_app.config["PERSONAL_DATA"] + return open(os.path.join(data_dir, "train_routes", filename + ".geojson")).read() + + +def get_trip_routes(trip: Trip) -> list[StrDict]: + """Get routes for given trip to show on map.""" + routes = [] + seen_geojson = set() + for t in trip.travel: + if t["type"] == "flight": + if "from_airport" not in t or "to_airport" not in t: + continue + fly_from, fly_to = t["from_airport"], t["to_airport"] + routes.append( + { + "type": "flight", + "from": latlon_tuple(fly_from), + "to": latlon_tuple(fly_to), + } + ) + + else: + assert t["type"] == "train" + for leg in t["legs"]: + train_from, train_to = leg["from_station"], leg["to_station"] + geojson_filename = train_from.get("routes", {}).get(train_to["uic"]) + if not geojson_filename: + routes.append( + { + "type": "train", + "from": latlon_tuple(train_from), + "to": latlon_tuple(train_to), + } + ) + continue + + if geojson_filename in seen_geojson: + continue + seen_geojson.add(geojson_filename) + + routes.append( + { + "type": "train", + "geojson": read_geojson(geojson_filename), + } + ) + + return routes diff --git a/web_view.py b/web_view.py index c4557f3..9def860 100755 --- a/web_view.py +++ b/web_view.py @@ -17,8 +17,8 @@ import yaml import agenda.data import agenda.error_mail import agenda.thespacedevs +import agenda.trip from agenda import format_list_with_ampersand, travel -from agenda.types import StrDict, Trip app = flask.Flask(__name__) app.debug = False @@ -155,84 +155,10 @@ def accommodation_list() -> str: ) -def load_travel(travel_type: str) -> list[StrDict]: - """Read flight and train journeys.""" - data_dir = app.config["PERSONAL_DATA"] - items = travel.parse_yaml(travel_type + "s", data_dir) - for item in items: - item["type"] = travel_type - return items - - -def load_trains() -> list[StrDict]: - """Load trains.""" - data_dir = app.config["PERSONAL_DATA"] - - trains = load_travel("train") - stations = travel.parse_yaml("stations", data_dir) - by_name = {station["name"]: station for station in stations} - - for train in trains: - assert train["from"] in by_name - assert train["to"] in by_name - train["from_station"] = by_name[train["from"]] - train["to_station"] = by_name[train["to"]] - - for leg in train["legs"]: - assert leg["from"] in by_name - assert leg["to"] in by_name - leg["from_station"] = by_name[leg["from"]] - leg["to_station"] = by_name[leg["to"]] - - return trains - - -def load_flights() -> list[StrDict]: - """Load flights.""" - data_dir = app.config["PERSONAL_DATA"] - flights = load_travel("flight") - airports = travel.parse_yaml("airports", data_dir) - for flight in flights: - if flight["from"] in airports: - flight["from_airport"] = airports[flight["from"]] - if flight["to"] in airports: - flight["to_airport"] = airports[flight["to"]] - return flights - - -def build_trip_list() -> list[Trip]: - """Generate list of trips.""" - trips: dict[date, Trip] = {} - - data_dir = app.config["PERSONAL_DATA"] - - travel_items = sorted( - load_flights() + load_trains(), key=operator.itemgetter("depart") - ) - - data = { - "travel": travel_items, - "accommodation": travel.parse_yaml("accommodation", data_dir), - "conferences": travel.parse_yaml("conferences", data_dir), - "events": travel.parse_yaml("events", data_dir), - } - - for key, item_list in data.items(): - assert isinstance(item_list, list) - for item in item_list: - if not (start := item.get("trip")): - continue - if start not in trips: - trips[start] = Trip(start=start) - getattr(trips[start], key).append(item) - - return [trip for _, trip in sorted(trips.items())] - - @app.route("/trip") def trip_list() -> str: """Page showing a list of trips.""" - trip_list = build_trip_list() + trip_list = agenda.trip.build_trip_list() today = date.today() current = [ @@ -255,106 +181,10 @@ def trip_list() -> str: ) -def collect_trip_coordinates(trip: Trip) -> list[StrDict]: - """Extract and deduplicate airport and station coordinates from trip.""" - stations = {} - station_list = [] - airports = {} - for t in trip.travel: - if t["type"] == "train": - station_list += [t["from_station"], t["to_station"]] - for leg in t["legs"]: - station_list.append(leg["from_station"]) - station_list.append(leg["to_station"]) - else: - assert t["type"] == "flight" - for field in "from_airport", "to_airport": - if field in t: - airports[t[field]["iata"]] = t[field] - - for s in station_list: - if s["uic"] in stations: - continue - stations[s["uic"]] = s - - return [ - { - "name": s["name"], - "type": "station", - "latitude": s["latitude"], - "longitude": s["longitude"], - } - for s in stations.values() - ] + [ - { - "name": s["name"], - "type": "airport", - "latitude": s["latitude"], - "longitude": s["longitude"], - } - for s in airports.values() - ] - - -def latlon_tuple(stop: StrDict) -> tuple[float, float]: - """Given a transport stop return the lat/lon as a tuple.""" - return (stop["latitude"], stop["longitude"]) - - -def read_geojson(filename: str) -> str: - data_dir = app.config["PERSONAL_DATA"] - return open(os.path.join(data_dir, "train_routes", filename + ".geojson")).read() - - -def get_trip_routes(trip: Trip) -> list[StrDict]: - routes = [] - seen_geojson = set() - for t in trip.travel: - if t["type"] == "flight": - if "from_airport" not in t or "to_airport" not in t: - continue - fly_from, fly_to = t["from_airport"], t["to_airport"] - routes.append( - { - "type": "flight", - "from": latlon_tuple(fly_from), - "to": latlon_tuple(fly_to), - } - ) - - else: - assert t["type"] == "train" - for leg in t["legs"]: - train_from, train_to = leg["from_station"], leg["to_station"] - geojson_filename = train_from.get("routes", {}).get(train_to["uic"]) - if not geojson_filename: - routes.append( - { - "type": "train", - "from": latlon_tuple(train_from), - "to": latlon_tuple(train_to), - } - ) - continue - - if geojson_filename in seen_geojson: - continue - seen_geojson.add(geojson_filename) - - routes.append( - { - "type": "train", - "geojson": read_geojson(geojson_filename), - } - ) - - return routes - - @app.route("/trip/") def trip_page(start: str) -> str: """Individual trip page.""" - trip_list = build_trip_list() + trip_list = agenda.trip.build_trip_list() today = date.today() trip = next((trip for trip in trip_list if trip.start.isoformat() == start), None) @@ -362,8 +192,8 @@ def trip_page(start: str) -> str: if not trip: flask.abort(404) - coordinates = collect_trip_coordinates(trip) - routes = get_trip_routes(trip) + coordinates = agenda.trip.collect_trip_coordinates(trip) + routes = agenda.trip.get_trip_routes(trip) return flask.render_template( "trip_page.html", From fd341903684ce5064b020423175e1e33340e591f Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sun, 14 Jan 2024 10:31:51 +0000 Subject: [PATCH 034/449] Map of all upcoming travel on trips page Closes: #107 --- agenda/trip.py | 46 +++++++++++++++------------- templates/trips.html | 72 +++++++++++++++++++++++++++++++++++++++++++- web_view.py | 27 +++++++++++++++++ 3 files changed, 123 insertions(+), 22 deletions(-) diff --git a/agenda/trip.py b/agenda/trip.py index 6bc4a7d..a306bee 100644 --- a/agenda/trip.py +++ b/agenda/trip.py @@ -143,38 +143,42 @@ def get_trip_routes(trip: Trip) -> list[StrDict]: if "from_airport" not in t or "to_airport" not in t: continue fly_from, fly_to = t["from_airport"], t["to_airport"] + key = "_".join(["flight"] + sorted([fly_from["iata"], fly_to["iata"]])) routes.append( { "type": "flight", + "key": key, "from": latlon_tuple(fly_from), "to": latlon_tuple(fly_to), } ) - - else: - assert t["type"] == "train" - for leg in t["legs"]: - train_from, train_to = leg["from_station"], leg["to_station"] - geojson_filename = train_from.get("routes", {}).get(train_to["uic"]) - if not geojson_filename: - routes.append( - { - "type": "train", - "from": latlon_tuple(train_from), - "to": latlon_tuple(train_to), - } - ) - continue - - if geojson_filename in seen_geojson: - continue - seen_geojson.add(geojson_filename) - + continue + assert t["type"] == "train" + for leg in t["legs"]: + train_from, train_to = leg["from_station"], leg["to_station"] + geojson_filename = train_from.get("routes", {}).get(train_to["uic"]) + key = "_".join(["train"] + sorted([train_from["name"], train_to["name"]])) + if not geojson_filename: routes.append( { "type": "train", - "geojson": read_geojson(geojson_filename), + "key": key, + "from": latlon_tuple(train_from), + "to": latlon_tuple(train_to), } ) + continue + + if geojson_filename in seen_geojson: + continue + seen_geojson.add(geojson_filename) + + routes.append( + { + "type": "train", + "key": key, + "geojson_filename": geojson_filename, + } + ) return routes diff --git a/templates/trips.html b/templates/trips.html index 7da70cb..d0f5624 100644 --- a/templates/trips.html +++ b/templates/trips.html @@ -5,6 +5,11 @@ {% set row = { "flight": flight_row, "train": train_row } %} {% block style %} + + + {% set conference_column_count = 6 %} {% set accommodation_column_count = 7 %} {% set travel_column_count = 7 %} @@ -33,6 +38,13 @@ .grid-item { /* Additional styling for grid items can go here */ } + +#map { + height: 80vh; +} + + + {% endblock %} @@ -79,11 +91,69 @@ {% block content %}
    +

    Trips

    {{ section("Current", current, "attending") }} {{ section("Future", future, "going") }} {{ section("Past", past|reverse, "went") }}
    - +{% endblock %} + +{% block scripts %} + + + + {% endblock %} diff --git a/web_view.py b/web_view.py index 9def860..1b57fb9 100755 --- a/web_view.py +++ b/web_view.py @@ -170,11 +170,35 @@ def trip_list() -> str: past = [item for item in trip_list if (item.end or item.start) < today] future = [item for item in trip_list if item.start > today] + future_coordinates = [] + seen_future_coordinates: set[tuple[str, str]] = set() + future_routes = [] + seen_future_routes: set[str] = set() + for trip in future: + for stop in agenda.trip.collect_trip_coordinates(trip): + key = (stop["type"], stop["name"]) + if key in seen_future_coordinates: + continue + future_coordinates.append(stop) + seen_future_coordinates.add(key) + + for route in agenda.trip.get_trip_routes(trip): + if route["key"] in seen_future_routes: + continue + future_routes.append(route) + seen_future_routes.add(route["key"]) + + for route in future_routes: + if "geojson_filename" in route: + route["geojson"] = agenda.trip.read_geojson(route.pop("geojson_filename")) + return flask.render_template( "trips.html", current=current, past=past, future=future, + future_coordinates=future_coordinates, + future_routes=future_routes, today=today, get_country=agenda.get_country, format_list_with_ampersand=format_list_with_ampersand, @@ -194,6 +218,9 @@ def trip_page(start: str) -> str: coordinates = agenda.trip.collect_trip_coordinates(trip) routes = agenda.trip.get_trip_routes(trip) + for route in routes: + if "geojson_filename" in route: + route["geojson"] = agenda.trip.read_geojson(route.pop("geojson_filename")) return flask.render_template( "trip_page.html", From fab478dc61fe5ecac7bc1008919af115f35d3fd4 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sun, 14 Jan 2024 10:32:52 +0000 Subject: [PATCH 035/449] Rename trips.html template to trip_list.html --- templates/{trips.html => trip_list.html} | 0 web_view.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename templates/{trips.html => trip_list.html} (100%) diff --git a/templates/trips.html b/templates/trip_list.html similarity index 100% rename from templates/trips.html rename to templates/trip_list.html diff --git a/web_view.py b/web_view.py index 1b57fb9..16daefe 100755 --- a/web_view.py +++ b/web_view.py @@ -193,7 +193,7 @@ def trip_list() -> str: route["geojson"] = agenda.trip.read_geojson(route.pop("geojson_filename")) return flask.render_template( - "trips.html", + "trip_list.html", current=current, past=past, future=future, From bd61b1bccd3239253b72d302b8b685eb26db2540 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sun, 14 Jan 2024 12:01:33 +0000 Subject: [PATCH 036/449] Move map code into dedicated JS file --- static/js/map.js | 53 ++++++++++++++++++++++++++++++++++++++++ templates/trip_list.html | 48 +++--------------------------------- templates/trip_page.html | 50 ++++--------------------------------- 3 files changed, 62 insertions(+), 89 deletions(-) create mode 100644 static/js/map.js diff --git a/static/js/map.js b/static/js/map.js new file mode 100644 index 0000000..3bd6a42 --- /dev/null +++ b/static/js/map.js @@ -0,0 +1,53 @@ +function emoji_icon(emoji) { + return L.divIcon({ + className: 'custom-div-icon', + html: "
    🚉
    ", + iconSize: [30, 42], + }); +} + +var stationIcon = emoji_icon("🚉"); +var airportIcon = emoji_icon("✈️<"); + +function build_map(map_id, coordinates, routes) { + // Initialize the map + var map = L.map(map_id).fitBounds(coordinates.map(function(station) { + return [station.latitude, station.longitude]; + })); + + // Set up the tile layer + L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + attribution: '© OpenStreetMap contributors' + }).addTo(map); + + // Add markers with appropriate icons to the map + coordinates.forEach(function(item) { + var icon = item.type === "station" ? stationIcon : airportIcon; + var marker = L.marker([item.latitude, item.longitude], { icon: icon }).addTo(map); + marker.bindPopup(item.name); + }); + + // Draw routes + routes.forEach(function(route) { + if (route.geojson) { + // If route is defined as GeoJSON + L.geoJSON(JSON.parse(route.geojson), { + style: function(feature) { + return {color: route.type === "train" ? "blue" : "blue"}; // Green for trains, blue for flights + } + }).addTo(map); + } else if (route.type === "flight") { + var flightPath = new L.Geodesic([[route.from, route.to]], { + weight: 3, + opacity: 0.5, + color: 'red' + }).addTo(map); + } else { + // If route is defined by 'from' and 'to' coordinates + var color = route.type === "train" ? "blue" : "red"; // Green for trains, red for flights + L.polyline([route.from, route.to], {color: color}).addTo(map); + } + }); + + return map; +} diff --git a/templates/trip_list.html b/templates/trip_list.html index d0f5624..d856cae 100644 --- a/templates/trip_list.html +++ b/templates/trip_list.html @@ -106,54 +106,14 @@ integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""> + + + {% endblock %} diff --git a/templates/trip_page.html b/templates/trip_page.html index 2924746..a4157a6 100644 --- a/templates/trip_page.html +++ b/templates/trip_page.html @@ -42,7 +42,7 @@ } #map { - height: 100vh; + height: 80vh; } @@ -91,54 +91,14 @@ integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""> + + + {% endif %} From 36b5d3827461cd6317875f34aead2ecfb226adab Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sun, 14 Jan 2024 12:17:22 +0000 Subject: [PATCH 037/449] Show map of past trips --- agenda/trip.py | 28 ++++++++++++++++++++++++++++ static/js/map.js | 4 ++-- templates/trip_list.html | 12 +++++++++--- web_view.py | 25 ++++--------------------- 4 files changed, 43 insertions(+), 26 deletions(-) diff --git a/agenda/trip.py b/agenda/trip.py index a306bee..ed9005a 100644 --- a/agenda/trip.py +++ b/agenda/trip.py @@ -182,3 +182,31 @@ def get_trip_routes(trip: Trip) -> list[StrDict]: ) return routes + + +def get_coordinates_and_routes( + trip_list: list[Trip], +) -> tuple[list[StrDict], list[StrDict]]: + coordinates = [] + seen_coordinates: set[tuple[str, str]] = set() + routes = [] + seen_routes: set[str] = set() + for trip in trip_list: + for stop in collect_trip_coordinates(trip): + key = (stop["type"], stop["name"]) + if key in seen_coordinates: + continue + coordinates.append(stop) + seen_coordinates.add(key) + + for route in get_trip_routes(trip): + if route["key"] in seen_routes: + continue + routes.append(route) + seen_routes.add(route["key"]) + + for route in routes: + if "geojson_filename" in route: + route["geojson"] = read_geojson(route.pop("geojson_filename")) + + return (coordinates, routes) diff --git a/static/js/map.js b/static/js/map.js index 3bd6a42..698dd39 100644 --- a/static/js/map.js +++ b/static/js/map.js @@ -1,13 +1,13 @@ function emoji_icon(emoji) { return L.divIcon({ className: 'custom-div-icon', - html: "
    🚉
    ", + html: "
    " + emoji + "
    ", iconSize: [30, 42], }); } var stationIcon = emoji_icon("🚉"); -var airportIcon = emoji_icon("✈️<"); +var airportIcon = emoji_icon("✈️"); function build_map(map_id, coordinates, routes) { // Initialize the map diff --git a/templates/trip_list.html b/templates/trip_list.html index d856cae..9118f1c 100644 --- a/templates/trip_list.html +++ b/templates/trip_list.html @@ -39,7 +39,7 @@ /* Additional styling for grid items can go here */ } -#map { +.map { height: 80vh; } @@ -91,11 +91,12 @@ {% block content %}
    -

    Trips

    +
    {{ section("Current", current, "attending") }} {{ section("Future", future, "going") }} +
    {{ section("Past", past|reverse, "went") }}
    {% endblock %} @@ -113,7 +114,12 @@ var future_coordinates = {{ future_coordinates | tojson }}; var future_routes = {{ future_routes | tojson }}; -build_map("map", future_coordinates, future_routes); +build_map("future-map", future_coordinates, future_routes); + +var past_coordinates = {{ past_coordinates | tojson }}; +var past_routes = {{ past_routes | tojson }}; + +build_map("past-map", past_coordinates, past_routes); {% endblock %} diff --git a/web_view.py b/web_view.py index 16daefe..2f17afb 100755 --- a/web_view.py +++ b/web_view.py @@ -170,27 +170,8 @@ def trip_list() -> str: past = [item for item in trip_list if (item.end or item.start) < today] future = [item for item in trip_list if item.start > today] - future_coordinates = [] - seen_future_coordinates: set[tuple[str, str]] = set() - future_routes = [] - seen_future_routes: set[str] = set() - for trip in future: - for stop in agenda.trip.collect_trip_coordinates(trip): - key = (stop["type"], stop["name"]) - if key in seen_future_coordinates: - continue - future_coordinates.append(stop) - seen_future_coordinates.add(key) - - for route in agenda.trip.get_trip_routes(trip): - if route["key"] in seen_future_routes: - continue - future_routes.append(route) - seen_future_routes.add(route["key"]) - - for route in future_routes: - if "geojson_filename" in route: - route["geojson"] = agenda.trip.read_geojson(route.pop("geojson_filename")) + future_coordinates, future_routes = agenda.trip.get_coordinates_and_routes(future) + past_coordinates, past_routes = agenda.trip.get_coordinates_and_routes(past) return flask.render_template( "trip_list.html", @@ -199,6 +180,8 @@ def trip_list() -> str: future=future, future_coordinates=future_coordinates, future_routes=future_routes, + past_coordinates=past_coordinates, + past_routes=past_routes, today=today, get_country=agenda.get_country, format_list_with_ampersand=format_list_with_ampersand, From fbee775f5b698384017758e6e80d80d885c3382e Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sun, 14 Jan 2024 12:29:39 +0000 Subject: [PATCH 038/449] Next trip and previous trip links on trip pages Closes: #110 --- templates/macros.html | 4 ++++ templates/trip_page.html | 7 ++++++- web_view.py | 11 +++++++++-- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/templates/macros.html b/templates/macros.html index f7926e6..cd331cd 100644 --- a/templates/macros.html +++ b/templates/macros.html @@ -3,6 +3,10 @@ {% macro display_date(dt) %}{{ dt.strftime("%a %-d %b %Y") }}{% endmacro %} {% macro display_date_no_year(dt) %}{{ dt.strftime("%a %-d %b") }}{% endmacro %} +{% macro trip_link(trip) %} + {{ trip.title }} +{% endmacro %} + {% macro conference_row(item, badge) %} {% set country = get_country(item.country) if item.country else None %}
    {{ item.start.strftime("%a, %d %b %Y") }}
    diff --git a/templates/trip_page.html b/templates/trip_page.html index a4157a6..45d4ec1 100644 --- a/templates/trip_page.html +++ b/templates/trip_page.html @@ -1,6 +1,6 @@ {% extends "base.html" %} -{% from "macros.html" import display_date_no_year, display_date, conference_row, accommodation_row, flight_row, train_row with context %} +{% from "macros.html" import trip_link, display_date_no_year, display_date, conference_row, accommodation_row, flight_row, train_row with context %} {% set row = { "flight": flight_row, "train": train_row } %} @@ -77,6 +77,11 @@ {% endfor %}
    +

    + {% if prev_trip %}previous: {{ trip_link(prev_trip) }}{% endif %} + {% if next_trip %}next: {{ trip_link(next_trip) }}{% endif %} +

    + {% if coordinates %}
    {% endif %} diff --git a/web_view.py b/web_view.py index 2f17afb..0c57977 100755 --- a/web_view.py +++ b/web_view.py @@ -191,10 +191,15 @@ def trip_list() -> str: @app.route("/trip/") def trip_page(start: str) -> str: """Individual trip page.""" - trip_list = agenda.trip.build_trip_list() + trip_iter = iter(agenda.trip.build_trip_list()) today = date.today() - trip = next((trip for trip in trip_list if trip.start.isoformat() == start), None) + prev_trip = None + for trip in trip_iter: + if trip.start.isoformat() == start: + break + prev_trip = trip + next_trip = next(trip_iter, None) if not trip: flask.abort(404) @@ -208,6 +213,8 @@ def trip_page(start: str) -> str: return flask.render_template( "trip_page.html", trip=trip, + prev_trip=prev_trip, + next_trip=next_trip, today=today, coordinates=coordinates, routes=routes, From e86bd69ddb702abe716e184f60e2e3f7e612f1b9 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sun, 14 Jan 2024 12:35:15 +0000 Subject: [PATCH 039/449] Show number of days between trips --- templates/trip_page.html | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/templates/trip_page.html b/templates/trip_page.html index 45d4ec1..389831a 100644 --- a/templates/trip_page.html +++ b/templates/trip_page.html @@ -78,8 +78,12 @@

    - {% if prev_trip %}previous: {{ trip_link(prev_trip) }}{% endif %} - {% if next_trip %}next: {{ trip_link(next_trip) }}{% endif %} + {% if prev_trip %} + previous: {{ trip_link(prev_trip) }} ({{ (trip.start - prev_trip.end).days }} days) + {% endif %} + {% if next_trip %} + next: {{ trip_link(next_trip) }} ({{ (next_trip.start - trip.end).days }} days) + {% endif %}

    {% if coordinates %} From f3a4f1dcd1c375bb42ba732a76d4474a43e70c61 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sun, 14 Jan 2024 15:42:48 +0000 Subject: [PATCH 040/449] Use unpkg.com as CDN to be consistent --- templates/base.html | 4 ++-- templates/trip_list.html | 2 +- templates/trip_page.html | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/templates/base.html b/templates/base.html index 94fe993..7b1cbde 100644 --- a/templates/base.html +++ b/templates/base.html @@ -7,7 +7,7 @@ {% block title %}{% endblock %} - + {% block style %} {% endblock %} @@ -18,6 +18,6 @@ {% block nav %}{{ navbar() }}{% endblock %} {% block content %}{% endblock %} {% block scripts %}{% endblock %} - + diff --git a/templates/trip_list.html b/templates/trip_list.html index 9118f1c..9c059c1 100644 --- a/templates/trip_list.html +++ b/templates/trip_list.html @@ -107,7 +107,7 @@ integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""> - + - + + From e475f98dd6b40fd74b53f6da5701faf1cffcc29a Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Fri, 19 Jan 2024 19:53:21 +0000 Subject: [PATCH 074/449] Use cache for space launch data --- agenda/data.py | 6 +++--- agenda/thespacedevs.py | 12 ++++++++++++ update_thespacedevs.py | 2 +- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/agenda/data.py b/agenda/data.py index c7268e6..eb42c26 100644 --- a/agenda/data.py +++ b/agenda/data.py @@ -274,10 +274,10 @@ async def get_data( result_list = await asyncio.gather( time_function("gbpusd", fx.get_gbpusd, config), time_function("gwr_advance_tickets", gwr.advance_ticket_date, data_dir), - time_function("rockets", thespacedevs.get_launches, rocket_dir, limit=40), time_function("backwell_bins", waste_collection_events, data_dir), time_function("bristol_bins", bristol_waste_collection_events, data_dir, today), ) + rockets = thespacedevs.read_cached_launches(rocket_dir) results = {call[0]: call[1] for call in result_list} @@ -293,7 +293,7 @@ async def get_data( "now": now, "gbpusd": results["gbpusd"], "stock_markets": stock_market_times, - "rockets": results["rockets"], + "rockets": rockets, "gwr_advance_tickets": gwr_advance_tickets, "data_gather_seconds": data_gather_seconds, "stock_market_times_seconds": stock_market_times_seconds, @@ -350,7 +350,7 @@ async def get_data( for market in overlapping_markets: events.remove(market) - for launch in results["rockets"]: + for launch in rockets: dt = None if launch["net_precision"] == "Day": diff --git a/agenda/thespacedevs.py b/agenda/thespacedevs.py index 23e2887..c1c785e 100644 --- a/agenda/thespacedevs.py +++ b/agenda/thespacedevs.py @@ -139,6 +139,18 @@ def summarize_launch(launch: Launch) -> Summary: } +def read_cached_launches(rocket_dir: str) -> list[Summary]: + """Read the most recent cache of launches.""" + existing = [x for x in (filename_timestamp(f) for f in os.listdir(rocket_dir)) if x] + + existing.sort(reverse=True) + f = existing[0][1] + + filename = os.path.join(rocket_dir, f) + data = json.load(open(filename)) + return [summarize_launch(launch) for launch in data["results"]] + + async def get_launches( rocket_dir: str, limit: int = 200, refresh: bool = False ) -> list[Summary]: diff --git a/update_thespacedevs.py b/update_thespacedevs.py index 9ecec7e..44a96f8 100755 --- a/update_thespacedevs.py +++ b/update_thespacedevs.py @@ -14,7 +14,7 @@ rocket_dir = os.path.join(config.DATA_DIR, "thespacedevs") async def get_launches() -> list[agenda.thespacedevs.Launch]: """Call space launch API and cache results.""" - return await agenda.thespacedevs.get_launches(rocket_dir, limit=200, refresh=True) + return await agenda.thespacedevs.next_launch_api(rocket_dir) t0 = time() From e16e04ab514e455e637e9d8bf3ae0bb1c8dd70b9 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Fri, 19 Jan 2024 20:35:52 +0000 Subject: [PATCH 075/449] Show more detail on space launch page --- agenda/__init__.py | 11 ++++++++++- templates/launches.html | 37 ++++++++++++++++++++++++++++--------- web_view.py | 4 +++- 3 files changed, 41 insertions(+), 11 deletions(-) diff --git a/agenda/__init__.py b/agenda/__init__.py index 63b6e7d..80be7d2 100644 --- a/agenda/__init__.py +++ b/agenda/__init__.py @@ -22,12 +22,21 @@ def format_list_with_ampersand(items: list[str]) -> str: return "" +ESA = "AUT,BEL,CZE,DNK,FIN,FRA,DEU,GRC,IRE,ITA,LUZ,NLD,NOR,POL,PRT,ROU,ESP,SWE,CHE,GBR" + + def get_country(alpha_2: str) -> pycountry.db.Country | None: """Lookup country by alpha-2 country code.""" + if alpha_2 == ESA: + return pycountry.db.Country(flag="🇪🇺", name="ESA") if not alpha_2: return None if alpha_2 == "xk": return pycountry.db.Country(flag="\U0001F1FD\U0001F1F0", name="Kosovo") - country: pycountry.db.Country = pycountry.countries.get(alpha_2=alpha_2.upper()) + country: pycountry.db.Country + if len(alpha_2) == 2: + country = pycountry.countries.get(alpha_2=alpha_2.upper()) + elif len(alpha_2) == 3: + country = pycountry.countries.get(alpha_3=alpha_2.upper()) return country diff --git a/templates/launches.html b/templates/launches.html index c048d5e..a4e718c 100644 --- a/templates/launches.html +++ b/templates/launches.html @@ -5,6 +5,7 @@

    Space launches

    {% for launch in rockets %} + {% set country = get_country(launch.country_code) %}
    {{ launch.t0_date }} @@ -17,7 +18,10 @@ launch status: {{ launch.status.abbrev }}
    -
    {{ launch.rocket }} +
    +
    + {{ country.flag }} + {{ launch.rocket }} – {{launch.mission.name }} – @@ -30,14 +34,29 @@ ({{ launch.launch_provider_type }}) — {{ launch.orbit.name }} ({{ launch.orbit.abbrev }}) -
    - {% if launch.pad_wikipedia_url %} - {{ launch.pad_name }} - {% else %} - {{ launch.pad_name }} {% if launch.pad_name != "Unknown Pad" %}(no Wikipedia article){% endif %} + — + {{ launch.mission.type }} +
    +
    + {% if launch.pad_wikipedia_url %} + {{ launch.pad_name }} + {% else %} + {{ launch.pad_name }} {% if launch.pad_name != "Unknown Pad" %}(no Wikipedia article){% endif %} + {% endif %} + — {{ launch.location }} +
    + {% if launch.mission.agencies | count %} +
    + agencies: + {% for agency in launch.mission.agencies %} + {%- if not loop.first %}, {% endif %} + {{agency.name }} + {{ get_country(agency.country_code).flag }} + ({{ agency.type }}) {# #} + {% endfor %} +
    {% endif %} - — {{ launch.location }}
    - +
    {% if launch.mission %} {% for line in launch.mission.description.splitlines() %}

    {{ line }}

    @@ -45,7 +64,7 @@ {% else %}

    No description.

    {% endif %} - +
    {% endfor %} diff --git a/web_view.py b/web_view.py index f11fc68..2604353 100755 --- a/web_view.py +++ b/web_view.py @@ -74,7 +74,9 @@ async def launch_list() -> str: rocket_dir = os.path.join(data_dir, "thespacedevs") rockets = await agenda.thespacedevs.get_launches(rocket_dir, limit=100) - return flask.render_template("launches.html", rockets=rockets, now=now) + return flask.render_template( + "launches.html", rockets=rockets, now=now, get_country=agenda.get_country + ) @app.route("/gaps") From cd60ebdea211108010483fca5e9ff6f1871220aa Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Fri, 19 Jan 2024 20:47:03 +0000 Subject: [PATCH 076/449] Show days until holiday on holidays page --- templates/holiday_list.html | 3 ++- web_view.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/templates/holiday_list.html b/templates/holiday_list.html index a8d3e73..de544e4 100644 --- a/templates/holiday_list.html +++ b/templates/holiday_list.html @@ -10,8 +10,9 @@ {% if loop.first or item.date != loop.previtem.date %} {{ display_date(item.date) }} + in {{ (item.date - today).days }} days {% else %} - + {% endif %} {{ country.flag }} {{ country.name }} {{ item.display_name }} diff --git a/web_view.py b/web_view.py index 2604353..99e5cf7 100755 --- a/web_view.py +++ b/web_view.py @@ -293,7 +293,7 @@ def holiday_list() -> str: items.sort(key=lambda item: (item.date, item.country)) return flask.render_template( - "holiday_list.html", items=items, get_country=agenda.get_country + "holiday_list.html", items=items, get_country=agenda.get_country, today=today ) From 073f452356e31c609feaac356d2cbc495dc39af6 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Fri, 19 Jan 2024 20:49:48 +0000 Subject: [PATCH 077/449] Tidy template --- templates/launches.html | 1 - 1 file changed, 1 deletion(-) diff --git a/templates/launches.html b/templates/launches.html index a4e718c..b60fbc2 100644 --- a/templates/launches.html +++ b/templates/launches.html @@ -47,7 +47,6 @@
    {% if launch.mission.agencies | count %}
    - agencies: {% for agency in launch.mission.agencies %} {%- if not loop.first %}, {% endif %} {{agency.name }} From 566b09f88806a33b3ba47b012dc46cf07ef2e097 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Fri, 19 Jan 2024 21:08:50 +0000 Subject: [PATCH 078/449] Don't bother with httpx for the space launch API --- agenda/thespacedevs.py | 14 +++++--------- update_thespacedevs.py | 10 ++-------- 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/agenda/thespacedevs.py b/agenda/thespacedevs.py index c1c785e..130be2f 100644 --- a/agenda/thespacedevs.py +++ b/agenda/thespacedevs.py @@ -5,7 +5,7 @@ import os import typing from datetime import datetime -import httpx +import requests Launch = dict[str, typing.Any] Summary = dict[str, typing.Any] @@ -13,15 +13,14 @@ Summary = dict[str, typing.Any] ttl = 60 * 60 * 2 # two hours -async def next_launch_api(rocket_dir: str, limit: int = 200) -> list[Launch]: +def next_launch_api(rocket_dir: str, limit: int = 200) -> list[Launch]: """Get the next upcoming launches from the API.""" now = datetime.now() filename = os.path.join(rocket_dir, now.strftime("%Y-%m-%d_%H:%M:%S.json")) url = "https://ll.thespacedevs.com/2.2.0/launch/upcoming/" params: dict[str, str | int] = {"limit": limit} - async with httpx.AsyncClient() as client: - r = await client.get(url, params=params) + r = requests.get(url, params=params) open(filename, "w").write(r.text) data = r.json() return [summarize_launch(launch) for launch in data["results"]] @@ -151,7 +150,7 @@ def read_cached_launches(rocket_dir: str) -> list[Summary]: return [summarize_launch(launch) for launch in data["results"]] -async def get_launches( +def get_launches( rocket_dir: str, limit: int = 200, refresh: bool = False ) -> list[Summary]: """Get rocket launches with caching.""" @@ -161,10 +160,7 @@ async def get_launches( existing.sort(reverse=True) if refresh or not existing or (now - existing[0][0]).seconds > ttl: - try: - return await next_launch_api(rocket_dir, limit=limit) - except httpx.ReadTimeout: - pass + return next_launch_api(rocket_dir, limit=limit) f = existing[0][1] diff --git a/update_thespacedevs.py b/update_thespacedevs.py index 44a96f8..e5b9ec8 100755 --- a/update_thespacedevs.py +++ b/update_thespacedevs.py @@ -1,7 +1,6 @@ #!/usr/bin/python3 """Update cache of space launch API.""" -import asyncio import os import sys from time import time @@ -11,14 +10,9 @@ import agenda.thespacedevs config = __import__("config.default", fromlist=[""]) rocket_dir = os.path.join(config.DATA_DIR, "thespacedevs") - -async def get_launches() -> list[agenda.thespacedevs.Launch]: - """Call space launch API and cache results.""" - return await agenda.thespacedevs.next_launch_api(rocket_dir) - - t0 = time() -rockets = asyncio.run(get_launches()) + +rockets = agenda.thespacedevs.next_launch_api(rocket_dir) time_taken = time() - t0 if not sys.stdin.isatty(): sys.exit(0) From 6d65f5045e2de9d644c9caa877e4781eb557b522 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sun, 21 Jan 2024 08:07:11 +0000 Subject: [PATCH 079/449] Ensure space launch JSON can be parsed before saving --- agenda/thespacedevs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agenda/thespacedevs.py b/agenda/thespacedevs.py index 130be2f..18a5bbb 100644 --- a/agenda/thespacedevs.py +++ b/agenda/thespacedevs.py @@ -21,8 +21,8 @@ def next_launch_api(rocket_dir: str, limit: int = 200) -> list[Launch]: params: dict[str, str | int] = {"limit": limit} r = requests.get(url, params=params) - open(filename, "w").write(r.text) data = r.json() + open(filename, "w").write(r.text) return [summarize_launch(launch) for launch in data["results"]] From 2b89ff7ff9268d863a6fc5a53e95bd14f160b58d Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sun, 21 Jan 2024 15:55:31 +0000 Subject: [PATCH 080/449] Add authentication via UniAuth --- agenda/auth.py | 29 +++++++++++++++++++++++++++++ web_view.py | 3 +++ 2 files changed, 32 insertions(+) create mode 100644 agenda/auth.py diff --git a/agenda/auth.py b/agenda/auth.py new file mode 100644 index 0000000..d44696a --- /dev/null +++ b/agenda/auth.py @@ -0,0 +1,29 @@ +"""Authentication via UniAuth.""" + +import flask +import werkzeug +from itsdangerous.url_safe import URLSafeTimedSerializer + +max_age = 60 * 60 * 24 * 90 + + +def verify_auth_token(token: str) -> str | None: + """Verify the authentication token.""" + serializer = URLSafeTimedSerializer(flask.current_app.config["SECRET_KEY"]) + try: + username = serializer.loads(token, salt="auth", max_age=max_age) + except Exception: + return None + + assert isinstance(username, str) + return username + + +def require_authentication() -> werkzeug.Response | None: + """Require authentication.""" + token = flask.request.cookies.get("auth_token") + return ( + None + if token and verify_auth_token(token) + else flask.redirect(flask.current_app.config["UNIAUTH_LOGIN_URL"]) + ) diff --git a/web_view.py b/web_view.py index 99e5cf7..259e751 100755 --- a/web_view.py +++ b/web_view.py @@ -15,6 +15,7 @@ import werkzeug import werkzeug.debug.tbtools import yaml +import agenda.auth import agenda.data import agenda.error_mail import agenda.holidays @@ -27,6 +28,8 @@ app = flask.Flask(__name__) app.debug = False app.config.from_object("config.default") +app.before_request(agenda.auth.require_authentication) + agenda.error_mail.setup_error_mail(app) From ac32b4fe895b6fac9613d1af0e08943d71985f36 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sun, 21 Jan 2024 15:56:18 +0000 Subject: [PATCH 081/449] Bug fix --- web_view.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web_view.py b/web_view.py index 99e5cf7..6141035 100755 --- a/web_view.py +++ b/web_view.py @@ -67,12 +67,12 @@ async def index() -> str: @app.route("/launches") -async def launch_list() -> str: +def launch_list() -> str: """Web page showing List of space launches.""" now = datetime.now() data_dir = app.config["DATA_DIR"] rocket_dir = os.path.join(data_dir, "thespacedevs") - rockets = await agenda.thespacedevs.get_launches(rocket_dir, limit=100) + rockets = agenda.thespacedevs.get_launches(rocket_dir, limit=100) return flask.render_template( "launches.html", rockets=rockets, now=now, get_country=agenda.get_country From d41d53367fdb6462e3625f8a375a501fdc26ab3c Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sun, 21 Jan 2024 16:23:46 +0000 Subject: [PATCH 082/449] Redirect back to agenda after login Closes: #91 --- agenda/auth.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/agenda/auth.py b/agenda/auth.py index d44696a..3e2f2b8 100644 --- a/agenda/auth.py +++ b/agenda/auth.py @@ -20,10 +20,14 @@ def verify_auth_token(token: str) -> str | None: def require_authentication() -> werkzeug.Response | None: - """Require authentication.""" + """Require authentication and redirect with return URL.""" token = flask.request.cookies.get("auth_token") - return ( - None - if token and verify_auth_token(token) - else flask.redirect(flask.current_app.config["UNIAUTH_LOGIN_URL"]) + if token and verify_auth_token(token): + return None + + # Construct the redirect URL with the original URL as a parameter + return flask.redirect( + flask.current_app.config["UNIAUTH_LOGIN_URL"] + + "?next=" + + werkzeug.urls.url_quote(flask.request.url) ) From 389092cbb40eb7146bb7fc89b3dd47c08eee2292 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Mon, 22 Jan 2024 12:43:09 +0000 Subject: [PATCH 083/449] Option to disable auth for testing --- agenda/auth.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/agenda/auth.py b/agenda/auth.py index d44696a..fc75d08 100644 --- a/agenda/auth.py +++ b/agenda/auth.py @@ -21,6 +21,9 @@ def verify_auth_token(token: str) -> str | None: def require_authentication() -> werkzeug.Response | None: """Require authentication.""" + if not flask.current_app.config.get("REQUIRE_AUTH"): + return None + token = flask.request.cookies.get("auth_token") return ( None From bdaad42eba152ce8f3d9f86962a51c3c2d8dd7c6 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Mon, 22 Jan 2024 12:43:32 +0000 Subject: [PATCH 084/449] Rename UNIAUTH_URL setting --- agenda/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agenda/auth.py b/agenda/auth.py index fc75d08..925ead3 100644 --- a/agenda/auth.py +++ b/agenda/auth.py @@ -28,5 +28,5 @@ def require_authentication() -> werkzeug.Response | None: return ( None if token and verify_auth_token(token) - else flask.redirect(flask.current_app.config["UNIAUTH_LOGIN_URL"]) + else flask.redirect(flask.current_app.config["UNIAUTH_URL"] + "/login") ) From b4a79cae6972d3182492cde9166a580dcc7f2834 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Mon, 22 Jan 2024 12:46:46 +0000 Subject: [PATCH 085/449] Add logout link Closes: #123 --- templates/navbar.html | 3 +++ 1 file changed, 3 insertions(+) diff --git a/templates/navbar.html b/templates/navbar.html index 9f8085c..2ba8830 100644 --- a/templates/navbar.html +++ b/templates/navbar.html @@ -29,6 +29,9 @@ {% endfor %} +
    From f028e40df8bb9bf7dc3ccffbeec8d9050b3fef52 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Mon, 22 Jan 2024 12:47:22 +0000 Subject: [PATCH 086/449] Show trip nights Closes: #119 --- templates/trip_page.html | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/templates/trip_page.html b/templates/trip_page.html index 715b14a..e7c9ce2 100644 --- a/templates/trip_page.html +++ b/templates/trip_page.html @@ -57,7 +57,10 @@

    {{ trip.title }}({{ display_date(trip.start) }})

    Countries: {{ trip.countries_str }}
    {% if end %} -
    Dates: {{ display_date_no_year(trip.start) }} to {{ display_date_no_year(end) }}
    +
    + Dates: {{ display_date_no_year(trip.start) }} to {{ display_date_no_year(end) }} + ({{ (end - trip.start).days }} nights) +
    {% else %}
    Start: {{ display_date_no_year(trip.start) }} (end date missing)
    {% endif %} From b7d655a21ecbbf46d9e02f7f80dd465b2119dc13 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Mon, 22 Jan 2024 13:04:08 +0000 Subject: [PATCH 087/449] Conference CFP end dates as events Closes: #122 --- agenda/conference.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/agenda/conference.py b/agenda/conference.py index 50b0b35..24ed552 100644 --- a/agenda/conference.py +++ b/agenda/conference.py @@ -48,8 +48,9 @@ class Conference: def get_list(filepath: str) -> list[Event]: """Read conferences from a YAML file and return a list of Event objects.""" - return [ - Event( + events: list[Event] = [] + for conf in (Conference(**conf) for conf in yaml.safe_load(open(filepath, "r"))): + event = Event( name="conference", date=conf.start, end_date=conf.end, @@ -57,5 +58,15 @@ def get_list(filepath: str) -> list[Event]: url=conf.url, going=conf.going, ) - for conf in (Conference(**conf) for conf in yaml.safe_load(open(filepath, "r"))) - ] + events.append(event) + if not conf.cfp_end: + continue + cfp_end_event = Event( + name="cfp_end", + date=conf.cfp_end, + title="CFP end: " + conf.display_name, + url=conf.cfp_url or conf.url, + ) + events.append(cfp_end_event) + + return events From 7e51a3221071eadbb589f2f18cca22bc9c6b1117 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Mon, 22 Jan 2024 13:12:33 +0000 Subject: [PATCH 088/449] Trip page to show how many days/weeks/months until trip Closes #118 --- templates/trip_page.html | 5 +++++ web_view.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/templates/trip_page.html b/templates/trip_page.html index e7c9ce2..a7a6169 100644 --- a/templates/trip_page.html +++ b/templates/trip_page.html @@ -65,6 +65,11 @@
    Start: {{ display_date_no_year(trip.start) }} (end date missing)
    {% endif %} + {% set delta = human_readable_delta(trip.start) %} + {% if delta %} +
    {{ delta }} time
    + {% endif %} +
    {% for conf in trip.conferences %} {{ conference_row(conf, "going") }} {% endfor %}
    diff --git a/web_view.py b/web_view.py index 91e9cc2..ea84364 100755 --- a/web_view.py +++ b/web_view.py @@ -219,6 +219,39 @@ def trip_list() -> str: ) +def human_readable_delta(future_date: date) -> str | None: + """ + Calculate the human-readable time delta for a given future date. + + Args: + future_date (date): The future date as a datetime.date object. + + Returns: + str: Human-readable time delta. + """ + # Ensure the input is a future date + if future_date <= date.today(): + return None + + # Calculate the delta + delta = future_date - date.today() + + # Convert delta to a more human-readable format + months, days = divmod(delta.days, 30) + weeks, days = divmod(days, 7) + + # Formatting the output + parts = [] + if months > 0: + parts.append(f"{months} months") + if weeks > 0: + parts.append(f"{weeks} weeks") + if days > 0: + parts.append(f"{days} days") + + return " ".join(parts) if parts else None + + @app.route("/trip/") def trip_page(start: str) -> str: """Individual trip page.""" @@ -282,6 +315,7 @@ def trip_page(start: str) -> str: get_country=agenda.get_country, format_list_with_ampersand=format_list_with_ampersand, holidays=holidays, + human_readable_delta=human_readable_delta, ) From 5f0d2e884fa4e5e5aa2e25f4fdc49470b8ebe330 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Mon, 22 Jan 2024 14:13:02 +0000 Subject: [PATCH 089/449] Add Rio Carnival to agenda --- agenda/data.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/agenda/data.py b/agenda/data.py index eb42c26..1cb891f 100644 --- a/agenda/data.py +++ b/agenda/data.py @@ -14,6 +14,7 @@ import isodate # type: ignore import lxml import pytz import yaml +from dateutil.easter import easter from . import ( accommodation, @@ -65,6 +66,32 @@ def midnight(d: date) -> datetime: return datetime.combine(d, datetime.min.time()) +def rio_carnival_events(start_date: date, end_date: date) -> list[Event]: + """List of events for Rio Carnival for each year between start_date and end_date.""" + events = [] + for year in range(start_date.year, end_date.year + 1): + easter_date = easter(year) + carnival_start = easter_date - timedelta(days=51) + carnival_end = easter_date - timedelta(days=46) + + # Only include the carnival if it falls within the specified date range + if ( + start_date <= carnival_start <= end_date + or start_date <= carnival_end <= end_date + ): + events.append( + Event( + name="carnival", + title="Rio Carnival", + date=carnival_start, + end_date=carnival_end, + url="https://en.wikipedia.org/wiki/Rio_Carnival", + ) + ) + + return events + + def dates_from_rrule( rrule: str, start: date, end: date ) -> typing.Sequence[datetime | date]: @@ -333,6 +360,7 @@ async def get_data( events += economist.publication_dates(last_week, next_year) events += meetup.get_events(my_data) events += hn.whoishiring(last_year, next_year) + events += rio_carnival_events(last_year, next_year) events += domains.renewal_dates(my_data) From fc36647d4950084d2307c1d9547db5f65a102a63 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Tue, 23 Jan 2024 10:49:58 +0000 Subject: [PATCH 090/449] Switch to UniAuth.auth --- agenda/auth.py | 36 ------------------------------------ web_view.py | 4 ++-- 2 files changed, 2 insertions(+), 38 deletions(-) delete mode 100644 agenda/auth.py diff --git a/agenda/auth.py b/agenda/auth.py deleted file mode 100644 index fefbee9..0000000 --- a/agenda/auth.py +++ /dev/null @@ -1,36 +0,0 @@ -"""Authentication via UniAuth.""" - -import flask -import werkzeug -from itsdangerous.url_safe import URLSafeTimedSerializer - -max_age = 60 * 60 * 24 * 90 - - -def verify_auth_token(token: str) -> str | None: - """Verify the authentication token.""" - serializer = URLSafeTimedSerializer(flask.current_app.config["SECRET_KEY"]) - try: - username = serializer.loads(token, salt="auth", max_age=max_age) - except Exception: - return None - - assert isinstance(username, str) - return username - - -def require_authentication() -> werkzeug.Response | None: - """Require authentication and redirect with return URL.""" - if not flask.current_app.config.get("REQUIRE_AUTH"): - return None - - token = flask.request.cookies.get("auth_token") - if token and verify_auth_token(token): - return None - - # Construct the redirect URL with the original URL as a parameter - return flask.redirect( - flask.current_app.config["UNIAUTH_URL"] - + "/login?next=" - + werkzeug.urls.url_quote(flask.request.url) - ) diff --git a/web_view.py b/web_view.py index ea84364..16aa6d7 100755 --- a/web_view.py +++ b/web_view.py @@ -11,11 +11,11 @@ import typing from datetime import date, datetime, timedelta import flask +import UniAuth.auth import werkzeug import werkzeug.debug.tbtools import yaml -import agenda.auth import agenda.data import agenda.error_mail import agenda.holidays @@ -28,7 +28,7 @@ app = flask.Flask(__name__) app.debug = False app.config.from_object("config.default") -app.before_request(agenda.auth.require_authentication) +app.before_request(UniAuth.auth.require_authentication) agenda.error_mail.setup_error_mail(app) From 72e7945fbe12e85a875ec77d2fa5e04cd4e2ed35 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Tue, 23 Jan 2024 15:55:28 +0000 Subject: [PATCH 091/449] Change layout of trip page --- agenda/conference.py | 2 + templates/trip_page.html | 176 ++++++++++++++++++++++++++------------- 2 files changed, 121 insertions(+), 57 deletions(-) diff --git a/agenda/conference.py b/agenda/conference.py index 24ed552..19e77bf 100644 --- a/agenda/conference.py +++ b/agenda/conference.py @@ -35,6 +35,8 @@ class Conference: longitude: float | None = None cfp_end: date | None = None cfp_url: str | None = None + free: bool | None = None + hackathon: bool | None = None @property def display_name(self) -> str: diff --git a/templates/trip_page.html b/templates/trip_page.html index a7a6169..a9bf8a8 100644 --- a/templates/trip_page.html +++ b/templates/trip_page.html @@ -44,7 +44,7 @@ } #map { - height: 80vh; + height: 90vh; } @@ -53,75 +53,138 @@ {% set end = trip.end %} {% block content %} -
    -

    {{ trip.title }}({{ display_date(trip.start) }})

    -
    Countries: {{ trip.countries_str }}
    - {% if end %} -
    - Dates: {{ display_date_no_year(trip.start) }} to {{ display_date_no_year(end) }} - ({{ (end - trip.start).days }} nights) +
    +
    +

    {{ trip.title }}

    +

    + {% if end %} + {{ display_date_no_year(trip.start) }} to {{ display_date_no_year(end) }} + ({{ (end - trip.start).days }} nights) + {% else %} + {{ display_date_no_year(trip.start) }} (end date missing) + {% endif %} +

    + +
    +
    Countries: {{ trip.countries_str }}
    + + {% set delta = human_readable_delta(trip.start) %} + {% if delta %} +
    How long until trip: {{ delta }}
    + {% endif %}
    - {% else %} -
    Start: {{ display_date_no_year(trip.start) }} (end date missing)
    - {% endif %} - {% set delta = human_readable_delta(trip.start) %} - {% if delta %} -
    {{ delta }} time
    - {% endif %} + {% for item in trip.conferences %} + {% set country = get_country(item.country) if item.country else None %} +
    +
    +
    + {{ item.name }} + + {{ display_date_no_year(item.start) }} to {{ display_date_no_year(item.end) }} + +
    +

    + Topic: {{ item.topic }} + | Venue: {{ item.venue }} + | Location: {{ item.location }} + {% if country %} + {{ country.flag }} + {% elif item.online %} + 💻 Online + {% else %} + + country code {{ item.country }} not found + + {% endif %} + {% if item.free %} + | free to attend + {% elif item.price and item.currency %} + | price: {{ item.price }} {{ item.currency }} + {% endif %} +

    +
    +
    + {% endfor %} -
    - {% for conf in trip.conferences %} {{ conference_row(conf, "going") }} {% endfor %} -
    + {% for item in trip.accommodation %} + {% set country = get_country(item.country) if item.country else None %} + {% set nights = (item.to.date() - item.from.date()).days %} +
    +
    +
    + {% if item.operator %}{{ item.operator }}: {% endif %} + {{ item.name }} + + {{ display_date_no_year(item.from) }} to {{ display_date_no_year(item.to) }} + ({% if nights == 1 %}1 night{% else %}{{ nights }} nights{% endif %}) + +
    +

    + Address: {{ item.address }} + | Location: {{ item.location }} + {% if country %} + {{ country.flag }} + {% elif item.online %} + 💻 Online + {% else %} + + country code {{ item.country }} not found + + {% endif %} + {% if item.price and item.currency %} + | price: {{ item.price }} {{ item.currency }} + {% endif %} +

    +
    +
    + {% endfor %} -
    - {% for conf in trip.accommodation %} {{ accommodation_row(conf, "going") }} {% endfor %} -
    +
    + {% for item in trip.travel %} {{ row[item.type](item) }} {% endfor %} +
    -
    - {% for item in trip.travel %} {{ row[item.type](item) }} {% endfor %} -
    +
    +

    Holidays

    + {% if holidays %} - {% if holidays %} -
    -

    Holidays

    + + {% for item in holidays %} + {% set country = get_country(item.country) %} + + {% if loop.first or item.date != loop.previtem.date %} + + {% else %} + + {% endif %} + + + + {% endfor %} +
    {{ display_date(item.date) }}{{ country.flag }} {{ country.name }}{{ item.display_name }}
    + {% else %} +

    No public holidays during trip.

    + {% endif %} +
    - - {% for item in holidays %} - {% set country = get_country(item.country) %} - - {% if loop.first or item.date != loop.previtem.date %} - - {% else %} - +

    + {% if prev_trip %} + previous: {{ trip_link(prev_trip) }} ({{ (trip.start - prev_trip.end).days }} days) {% endif %} -

    - - - {% endfor %} -
    {{ display_date(item.date) }}{{ country.flag }} {{ country.name }}{{ item.display_name }}
    + {% if next_trip %} + next: {{ trip_link(next_trip) }} ({{ (next_trip.start - trip.end).days }} days) + {% endif %} +

    + +
    +
    +
    - {% endif %} - -

    - {% if prev_trip %} - previous: {{ trip_link(prev_trip) }} ({{ (trip.start - prev_trip.end).days }} days) - {% endif %} - {% if next_trip %} - next: {{ trip_link(next_trip) }} ({{ (next_trip.start - trip.end).days }} days) - {% endif %} -

    - - {% if coordinates %} -
    - {% endif %} -
    {% endblock %} {% block scripts %} -{% if coordinates %} @@ -137,5 +200,4 @@ var routes = {{ routes | tojson }}; build_map("map", coordinates, routes); -{% endif %} {% endblock %} From 6475692db156740d4a68de9f26b0f982bcc88f5d Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Tue, 23 Jan 2024 15:56:23 +0000 Subject: [PATCH 092/449] Consider accommodation for trip end date --- agenda/types.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/agenda/types.py b/agenda/types.py index 4bcfa5b..a96eff3 100644 --- a/agenda/types.py +++ b/agenda/types.py @@ -56,7 +56,14 @@ class Trip: travel_end = max(arrive) if arrive else datetime.date.min assert isinstance(travel_end, datetime.date) - max_date = max(max_conference_end, travel_end) + accommodation_end = ( + max(as_date(item["to"]) for item in self.accommodation) + if self.accommodation + else datetime.date.min + ) + assert isinstance(accommodation_end, datetime.date) + + max_date = max(max_conference_end, travel_end, accommodation_end) return max_date if max_date != datetime.date.min else None @property From f76f9e03dacc1c177c1997c8cf2d78947885ed65 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Tue, 23 Jan 2024 15:57:12 +0000 Subject: [PATCH 093/449] Add trip country_flags method --- agenda/types.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/agenda/types.py b/agenda/types.py index a96eff3..37f656e 100644 --- a/agenda/types.py +++ b/agenda/types.py @@ -90,6 +90,11 @@ class Trip: [f"{c.flag} {c.name}" for c in self.countries] ) + @property + def country_flags(self) -> str: + """Countries flags for trip.""" + return "".join(c.flag for c in self.countries) + @dataclass class Holiday: From d6ebd86232d049274b858a1ee6ce2a994b6f7ac8 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Tue, 23 Jan 2024 15:57:36 +0000 Subject: [PATCH 094/449] Add more emojis --- agenda/types.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/agenda/types.py b/agenda/types.py index 37f656e..c0f28b1 100644 --- a/agenda/types.py +++ b/agenda/types.py @@ -132,6 +132,8 @@ emojis = { "economist": "📰", "running": "🏃", "critical_mass": "🚴", + "trip": "🧳", + "hackathon": "💻", } From 6c1c63810417493aafff576da9d7ee95a6a35c05 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Tue, 23 Jan 2024 15:58:01 +0000 Subject: [PATCH 095/449] Gap page to show trips Closes: #90 Closes: #97 --- agenda/data.py | 44 +++++++++++++++++++++++++++++++++++++++++--- templates/gaps.html | 24 ++++++++++++++++++++++-- web_view.py | 5 +++-- 3 files changed, 66 insertions(+), 7 deletions(-) diff --git a/agenda/data.py b/agenda/data.py index 1cb891f..23dd6e1 100644 --- a/agenda/data.py +++ b/agenda/data.py @@ -37,7 +37,7 @@ from . import ( uk_tz, waste_schedule, ) -from .types import Event, StrDict +from .types import Event, StrDict, Trip here = dateutil.tz.tzlocal() @@ -144,10 +144,14 @@ def get_yaml_event_end_date_field(item: dict[str, str]) -> str: ) -def read_events_yaml(data_dir: str, start: date, end: date) -> list[Event]: +def read_events_yaml( + data_dir: str, start: date, end: date, skip_trips: bool = False +) -> list[Event]: """Read eventes from YAML file.""" events: list[Event] = [] for item in yaml.safe_load(open(os.path.join(data_dir, "events.yaml"))): + if "trip" in item and skip_trips: + continue duration = ( isodate.parse_duration(item["duration"]) if "duration" in item else None ) @@ -247,10 +251,10 @@ def busy_event(e: Event) -> bool: "event", "accommodation", "conference", - "dodainville", "transport", "meetup", "party", + "trip", }: return False @@ -281,6 +285,40 @@ async def time_function( return name, result, end_time - start_time +def gap_list( + today: date, config: flask.config.Config, trips: list[Trip] +) -> list[StrDict]: + last_year = today - timedelta(days=365) + next_year = today + timedelta(days=2 * 365) + + my_data = config["PERSONAL_DATA"] + events = read_events_yaml(my_data, last_year, next_year, skip_trips=True) + + for trip in trips: + event_type = "trip" + if trip.events and not trip.conferences: + event_type = trip.events[0]["name"] + elif len(trip.conferences) == 1 and trip.conferences[0].get("hackathon"): + event_type = "hackathon" + events.append( + Event( + name=event_type, + title=trip.title + " " + trip.country_flags, + date=trip.start, + end_date=trip.end, + url=flask.url_for("trip_page", start=trip.start.isoformat()), + ) + ) + + busy_events = [ + e + for e in sorted(events, key=lambda e: e.as_date) + if e.as_date > today and e.as_date < next_year and busy_event(e) + ] + + return find_gaps(busy_events) + + async def get_data( now: datetime, config: flask.config.Config ) -> typing.Mapping[str, str | object]: diff --git a/templates/gaps.html b/templates/gaps.html index ae24883..7cb7fbf 100644 --- a/templates/gaps.html +++ b/templates/gaps.html @@ -17,11 +17,31 @@ {% for gap in gaps %} - {% for event in gap.before %}{% if not loop.first %}
    {% endif %}{{ event.title or event.name }}{% endfor %} + + {% for event in gap.before %} +
    + {% if event.url %} + {{ event.title_with_emoji }} + {% else %} + {{ event.title_with_emoji }} + {% endif %} +
    + {% endfor %} + {{ gap.start.strftime("%A, %-d %b %Y") }} {{ (gap.end - gap.start).days }} days {{ gap.end.strftime("%A, %-d %b %Y") }} - {% for event in gap.after %}{% if not loop.first %}
    {% endif %}{{ event.title or event.name }}{% endfor %} + + {% for event in gap.after %} +
    + {% if event.url %} + {{ event.title_with_emoji }} + {% else %} + {{ event.title_with_emoji }} + {% endif %} +
    + {% endfor %} + {% endfor %} diff --git a/web_view.py b/web_view.py index 16aa6d7..9d72286 100755 --- a/web_view.py +++ b/web_view.py @@ -86,8 +86,9 @@ def launch_list() -> str: async def gaps_page() -> str: """List of available gaps.""" now = datetime.now() - data = await agenda.data.get_data(now, app.config) - return flask.render_template("gaps.html", today=now.date(), gaps=data["gaps"]) + trip_list = agenda.trip.build_trip_list() + gaps = agenda.data.gap_list(now.date(), app.config, trip_list) + return flask.render_template("gaps.html", today=now.date(), gaps=gaps) @app.route("/travel") From 14c25e16ed612e08fb45c2f499be44bbb677e05a Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Tue, 23 Jan 2024 16:28:20 +0000 Subject: [PATCH 096/449] Add next and previous links at top of trip page --- templates/trip_page.html | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/templates/trip_page.html b/templates/trip_page.html index a9bf8a8..001ba6e 100644 --- a/templates/trip_page.html +++ b/templates/trip_page.html @@ -6,6 +6,17 @@ {% set row = { "flight": flight_row, "train": train_row } %} +{% macro next_and_previous() %} +

    + {% if prev_trip %} + previous: {{ trip_link(prev_trip) }} ({{ (trip.start - prev_trip.end).days }} days) + {% endif %} + {% if next_trip %} + next: {{ trip_link(next_trip) }} ({{ (next_trip.start - trip.end).days }} days) + {% endif %} +

    +{% endmacro %} + {% block style %} {% if coordinates %} @@ -55,6 +66,7 @@ {% block content %}
    + {{ next_and_previous() }}

    {{ trip.title }}

    {% if end %} @@ -167,14 +179,7 @@ {% endif %}

    -

    - {% if prev_trip %} - previous: {{ trip_link(prev_trip) }} ({{ (trip.start - prev_trip.end).days }} days) - {% endif %} - {% if next_trip %} - next: {{ trip_link(next_trip) }} ({{ (next_trip.start - trip.end).days }} days) - {% endif %} -

    + {{ next_and_previous() }}
    From 89ff92c53399ae808d69b8e103221920f526cda5 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Wed, 24 Jan 2024 12:03:56 +0000 Subject: [PATCH 097/449] Show linked events on trip page Closes: #124 --- agenda/trip.py | 6 +++++- static/js/map.js | 1 + templates/trip_page.html | 28 ++++++++++++++++++++++++++-- 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/agenda/trip.py b/agenda/trip.py index 6234bf6..d5f9adc 100644 --- a/agenda/trip.py +++ b/agenda/trip.py @@ -106,7 +106,11 @@ def collect_trip_coordinates(trip: Trip) -> list[StrDict]: coords = [] - src = [("accommodation", trip.accommodation), ("conference", trip.conferences)] + src = [ + ("accommodation", trip.accommodation), + ("conference", trip.conferences), + ("event", trip.events), + ] for coord_type, item_list in src: coords += [ { diff --git a/static/js/map.js b/static/js/map.js index addc29a..21872c9 100644 --- a/static/js/map.js +++ b/static/js/map.js @@ -18,6 +18,7 @@ var icons = { "airport": emoji_icon("✈️"), "accommodation": emoji_icon("🏨"), "conference": emoji_icon("🎤"), + "event": emoji_icon("🍷"), } function build_map(map_id, coordinates, routes) { diff --git a/templates/trip_page.html b/templates/trip_page.html index 001ba6e..03e86d1 100644 --- a/templates/trip_page.html +++ b/templates/trip_page.html @@ -137,8 +137,32 @@ | Location: {{ item.location }} {% if country %} {{ country.flag }} - {% elif item.online %} - 💻 Online + {% else %} + + country code {{ item.country }} not found + + {% endif %} + {% if item.price and item.currency %} + | price: {{ item.price }} {{ item.currency }} + {% endif %} +

    +
    +
    + {% endfor %} + + {% for item in trip.events %} + {% set country = get_country(item.country) if item.country else None %} +
    +
    +
    + {{ item.title }} + {{ display_date_no_year(item.date) }} +
    +

    + Address: {{ item.address }} + | Location: {{ item.location }} + {% if country %} + {{ country.flag }} {% else %} country code {{ item.country }} not found From 6c8e1bf48dc893d9cdd1e9b1c6f64c212593ce1a Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Thu, 25 Jan 2024 16:48:31 +0000 Subject: [PATCH 098/449] Add new conference field --- agenda/conference.py | 1 + 1 file changed, 1 insertion(+) diff --git a/agenda/conference.py b/agenda/conference.py index 19e77bf..6123afe 100644 --- a/agenda/conference.py +++ b/agenda/conference.py @@ -37,6 +37,7 @@ class Conference: cfp_url: str | None = None free: bool | None = None hackathon: bool | None = None + ticket_type: str | None = None @property def display_name(self) -> str: From 8b777e64fc2d719f9980830243b558b10ed138fb Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Tue, 30 Jan 2024 10:35:57 +0000 Subject: [PATCH 099/449] Add page to generate a list of trips as text --- agenda/types.py | 26 ++++++++++++++ templates/trip_list_text.html | 67 +++++++++++++++++++++++++++++++++++ web_view.py | 17 +++++++++ 3 files changed, 110 insertions(+) create mode 100644 templates/trip_list_text.html diff --git a/agenda/types.py b/agenda/types.py index c0f28b1..d1668c5 100644 --- a/agenda/types.py +++ b/agenda/types.py @@ -66,6 +66,25 @@ class Trip: max_date = max(max_conference_end, travel_end, accommodation_end) return max_date if max_date != datetime.date.min else None + def locations(self) -> list[tuple[str, Country]]: + """Locations for trip.""" + seen: set[tuple[str, str]] = set() + items = [] + + for item in self.conferences + self.accommodation + self.events: + if "country" not in item or "location" not in item: + continue + key = (item["location"], item["country"]) + if key in seen: + continue + seen.add(key) + + country = agenda.get_country(item["country"]) + assert country + items.append((item["location"], country)) + + return items + @property def countries(self) -> list[Country]: """Countries visited as part of trip, in order.""" @@ -90,6 +109,13 @@ class Trip: [f"{c.flag} {c.name}" for c in self.countries] ) + @property + def locations_str(self) -> str: + """List of countries visited on this trip.""" + return format_list_with_ampersand( + [f"{location} {c.flag}" for location, c in self.locations()] + ) + @property def country_flags(self) -> str: """Countries flags for trip.""" diff --git a/templates/trip_list_text.html b/templates/trip_list_text.html new file mode 100644 index 0000000..a5aa5b5 --- /dev/null +++ b/templates/trip_list_text.html @@ -0,0 +1,67 @@ +{% extends "base.html" %} + +{% from "macros.html" import trip_link, display_date_no_year, display_date, conference_row, accommodation_row, flight_row, train_row with context %} + +{% set row = { "flight": flight_row, "train": train_row } %} + +{% block style %} + + + +{% set conference_column_count = 7 %} +{% set accommodation_column_count = 7 %} +{% set travel_column_count = 8 %} + +{% endblock %} + + +{% block content %} +

    + +

    Trips

    +

    {{ future | count }} trips

    + {% for trip in future %} + {% set end = trip.end %} +
    + {{ display_date_no_year(trip.start) }} to {{ display_date_no_year(end) }}: + {{ trip.title }} — {{ trip.locations_str }} +
    + {% endfor %} + + +
    +{% endblock %} diff --git a/web_view.py b/web_view.py index 9d72286..389b7c0 100755 --- a/web_view.py +++ b/web_view.py @@ -220,6 +220,23 @@ def trip_list() -> str: ) +@app.route("/trip/text") +def trip_list_text() -> str: + """Page showing a list of trips.""" + trip_list = agenda.trip.build_trip_list() + + today = date.today() + future = [item for item in trip_list if item.start > today] + + return flask.render_template( + "trip_list_text.html", + future=future, + today=today, + get_country=agenda.get_country, + format_list_with_ampersand=format_list_with_ampersand, + ) + + def human_readable_delta(future_date: date) -> str | None: """ Calculate the human-readable time delta for a given future date. From f3304d0ffe332c2ebe9e22341e8452e75a5ed1cc Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Tue, 30 Jan 2024 10:36:42 +0000 Subject: [PATCH 100/449] We don't need to show GBPUSD --- agenda/data.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/agenda/data.py b/agenda/data.py index 23dd6e1..d572c1d 100644 --- a/agenda/data.py +++ b/agenda/data.py @@ -23,7 +23,6 @@ from . import ( conference, domains, economist, - fx, gwr, hn, holidays, @@ -337,7 +336,6 @@ async def get_data( t0 = time() result_list = await asyncio.gather( - time_function("gbpusd", fx.get_gbpusd, config), time_function("gwr_advance_tickets", gwr.advance_ticket_date, data_dir), time_function("backwell_bins", waste_collection_events, data_dir), time_function("bristol_bins", bristol_waste_collection_events, data_dir, today), @@ -356,7 +354,6 @@ async def get_data( reply: dict[str, typing.Any] = { "now": now, - "gbpusd": results["gbpusd"], "stock_markets": stock_market_times, "rockets": rockets, "gwr_advance_tickets": gwr_advance_tickets, From f54c9cfbb785b5239e0cccda98c3caea3c9ff54e Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Tue, 30 Jan 2024 11:07:28 +0000 Subject: [PATCH 101/449] Switch to using cards for trip pay layout Closes: #125 --- agenda/trip.py | 4 ++++ agenda/types.py | 4 ++-- templates/trip_page.html | 52 ++++++++++++++++++++++++++++++++++++---- 3 files changed, 53 insertions(+), 7 deletions(-) diff --git a/agenda/trip.py b/agenda/trip.py index d5f9adc..fe2be90 100644 --- a/agenda/trip.py +++ b/agenda/trip.py @@ -3,6 +3,7 @@ import os from datetime import date import flask +import yaml from agenda import travel from agenda.types import StrDict, Trip @@ -44,12 +45,15 @@ def load_flights() -> list[StrDict]: """Load flights.""" data_dir = flask.current_app.config["PERSONAL_DATA"] flights = load_travel("flight") + airlines = yaml.safe_load(open(os.path.join(data_dir, "airlines.yaml"))) airports = travel.parse_yaml("airports", data_dir) for flight in flights: if flight["from"] in airports: flight["from_airport"] = airports[flight["from"]] if flight["to"] in airports: flight["to_airport"] = airports[flight["to"]] + if "airline" in flight: + flight["airline_name"] = airlines.get(flight["airline"], "[unknown]") return flights diff --git a/agenda/types.py b/agenda/types.py index d1668c5..608a6fb 100644 --- a/agenda/types.py +++ b/agenda/types.py @@ -106,14 +106,14 @@ class Trip: def countries_str(self) -> str: """List of countries visited on this trip.""" return format_list_with_ampersand( - [f"{c.flag} {c.name}" for c in self.countries] + [f"{c.name} {c.flag}" for c in self.countries] ) @property def locations_str(self) -> str: """List of countries visited on this trip.""" return format_list_with_ampersand( - [f"{location} {c.flag}" for location, c in self.locations()] + [f"{location} ({c.name}) {c.flag}" for location, c in self.locations()] ) @property diff --git a/templates/trip_page.html b/templates/trip_page.html index 03e86d1..b002643 100644 --- a/templates/trip_page.html +++ b/templates/trip_page.html @@ -2,7 +2,7 @@ {% block title %}{{ trip.title }} ({{ display_date(trip.start) }}){% endblock %} -{% from "macros.html" import trip_link, display_date_no_year, display_date, conference_row, accommodation_row, flight_row, train_row with context %} +{% from "macros.html" import trip_link, display_datetime, display_date_no_year, display_date, conference_row, accommodation_row, flight_row, train_row with context %} {% set row = { "flight": flight_row, "train": train_row } %} @@ -78,7 +78,8 @@

    -
    Countries: {{ trip.countries_str }}
    + {#
    Countries: {{ trip.countries_str }}
    #} +
    Locations: {{ trip.locations_str }}
    {% set delta = human_readable_delta(trip.start) %} {% if delta %} @@ -176,9 +177,50 @@
    {% endfor %} -
    - {% for item in trip.travel %} {{ row[item.type](item) }} {% endfor %} -
    + {% for item in trip.travel %} +
    +
    +
    + {% if item.type == "flight" %} + ✈️ + {{ item.from_airport.name }} ({{ item.from_airport.iata}}) + → + {{ item.to_airport.name }} ({{item.to_airport.iata}}) + {% elif item.type == "train" %} + 🚆 + {{ item.from }} + → + {{ item.to }} + {% endif %} +
    +

    + {% if item.type == "flight" %} +

    + {{ item.airline_name }} ({{ item.airline }}) + ✨ + {{ display_datetime(item.depart) }} + → + {{ item.arrive.strftime("%H:%M %z") }} + ✨ + {{ ((item.arrive - item.depart).total_seconds() // 60) | int }} mins + ✨ + {{ item.airline }}{{ item.flight_number }} +
    + {% elif item.type == "train" %} +
    + {{ display_datetime(item.depart) }} + → + {{ item.arrive.strftime("%H:%M %z") }} + {% if item.class %} + {{ item.class }} + {% endif %} + {{ ((item.arrive - item.depart).total_seconds() // 60) | int }} mins +
    + {% endif %} +

    +
    +
    + {% endfor %}

    Holidays

    From 3163bca99be065f2c3eca3563539df17c597e2b1 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sun, 11 Feb 2024 07:15:08 +0000 Subject: [PATCH 102/449] Read extra headers for mail from config --- update_gwr_advance_ticket_date.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/update_gwr_advance_ticket_date.py b/update_gwr_advance_ticket_date.py index 2e24bb9..2a02e3d 100755 --- a/update_gwr_advance_ticket_date.py +++ b/update_gwr_advance_ticket_date.py @@ -24,6 +24,10 @@ def send_mail(subject: str, body: str) -> None: msg["Date"] = formatdate() msg["Message-ID"] = make_msgid() + # Add extra mail headers + for header, value in config.getattr("MAIL_HEADERS", []): + msg[header] = value + msg.set_content(body) s = smtplib.SMTP(config.SMTP_HOST) From b66f85225680edf1a5f09417632e24473d3818ed Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sun, 11 Feb 2024 07:42:45 +0000 Subject: [PATCH 103/449] Avoid space launches with vague dates in agenda Closes: #127 --- agenda/data.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/agenda/data.py b/agenda/data.py index d572c1d..75043bf 100644 --- a/agenda/data.py +++ b/agenda/data.py @@ -416,9 +416,16 @@ async def get_data( for launch in rockets: dt = None - if launch["net_precision"] == "Day": + net_precision = launch["net_precision"] + skip = {"Year", "Month", "Quarter", "Fiscal Year"} + if net_precision == "Day": dt = datetime.strptime(launch["net"], "%Y-%m-%dT%H:%M:%SZ").date() - elif launch["t0_time"]: + elif ( + net_precision + and net_precision not in skip + and "Year" not in net_precision + and launch["t0_time"] + ): dt = pytz.utc.localize( datetime.strptime(launch["net"], "%Y-%m-%dT%H:%M:%SZ") ) From f19e4e4dd49e69f52c4e93c323f7f5ce8d4525b4 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sun, 18 Feb 2024 22:07:38 +0000 Subject: [PATCH 104/449] Include UniAuth callback --- web_view.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/web_view.py b/web_view.py index 389b7c0..db52555 100755 --- a/web_view.py +++ b/web_view.py @@ -352,5 +352,11 @@ def holiday_list() -> str: ) +@app.route("/callback") +def auth_callback() -> tuple[str, int] | werkzeug.Response: + """Process the authentication callback.""" + return UniAuth.auth.auth_callback() + + if __name__ == "__main__": app.run(host="0.0.0.0") From 7a9fbcec7bda3448dc9450930b48268f2f693713 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sun, 18 Feb 2024 22:36:15 +0000 Subject: [PATCH 105/449] Catch errors from external service and display in alert box Closes: #129 --- agenda/data.py | 18 +++++++++++++----- templates/index.html | 8 ++++++++ 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/agenda/data.py b/agenda/data.py index 75043bf..27e2cf1 100644 --- a/agenda/data.py +++ b/agenda/data.py @@ -276,12 +276,15 @@ async def time_function( func: typing.Callable[..., typing.Coroutine[typing.Any, typing.Any, typing.Any]], *args, **kwargs, -) -> tuple[str, typing.Any, float]: +) -> tuple[str, typing.Any, float, Exception | None]: """Time the execution of an asynchronous function.""" - start_time = time() - result = await func(*args, **kwargs) + start_time, result, exception = time(), None, None + try: + result = await func(*args, **kwargs) + except Exception as e: + exception = e end_time = time() - return name, result, end_time - start_time + return name, result, end_time - start_time, exception def gap_list( @@ -344,6 +347,8 @@ async def get_data( results = {call[0]: call[1] for call in result_list} + errors = [(call[0], call[3]) for call in result_list if call[3]] + gwr_advance_tickets = results["gwr_advance_tickets"] data_gather_seconds = time() - t0 @@ -389,7 +394,9 @@ async def get_data( events += accommodation_events events += travel.all_events(my_data) events += conference.get_list(os.path.join(my_data, "conferences.yaml")) - events += results["backwell_bins"] + results["bristol_bins"] + for key in "backwell_bins", "bristol_bins": + if results[key]: + events += results[key] events += read_events_yaml(my_data, last_year, next_year) events += subscription.get_events(os.path.join(my_data, "subscriptions.yaml")) events += economist.publication_dates(last_week, next_year) @@ -467,5 +474,6 @@ async def get_data( reply["two_weeks_ago"] = two_weeks_ago reply["fullcalendar_events"] = calendar.build_events(events) + reply["errors"] = errors return reply diff --git a/templates/index.html b/templates/index.html index 6c452ec..4034a2f 100644 --- a/templates/index.html +++ b/templates/index.html @@ -127,6 +127,14 @@ Sunset: {{ sunset.strftime("%H:%M:%S") }} + {% if errors %} + {% for error in errors %} + + {% endfor %} + {% endif %} +

    Stock markets

    {% for market in stock_markets %}

    {{ market }}

    From 38dccc15299a607939d59f9aa173b0748f14b82e Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Mon, 19 Feb 2024 09:46:57 +0000 Subject: [PATCH 106/449] Fix trip page layout on mobile --- templates/trip_page.html | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/templates/trip_page.html b/templates/trip_page.html index b002643..651bec2 100644 --- a/templates/trip_page.html +++ b/templates/trip_page.html @@ -65,7 +65,8 @@ {% block content %}
    -
    +
    +
    {{ next_and_previous() }}

    {{ trip.title }}

    @@ -248,7 +249,8 @@ {{ next_and_previous() }}

    -
    +
    +
    From 5ffb389c536102a1034147c2330f2d267683512a Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Wed, 21 Feb 2024 13:06:40 +0000 Subject: [PATCH 107/449] Add weekend availability view Closes: #130 --- agenda/data.py | 42 ++++++++++++++++++++++++++++++++++++--- templates/navbar.html | 1 + templates/weekends.html | 44 +++++++++++++++++++++++++++++++++++++++++ web_view.py | 13 +++++++++++- 4 files changed, 96 insertions(+), 4 deletions(-) create mode 100644 templates/weekends.html diff --git a/agenda/data.py b/agenda/data.py index 27e2cf1..d209020 100644 --- a/agenda/data.py +++ b/agenda/data.py @@ -287,9 +287,10 @@ async def time_function( return name, result, end_time - start_time, exception -def gap_list( +def get_busy_events( today: date, config: flask.config.Config, trips: list[Trip] -) -> list[StrDict]: +) -> list[Event]: + """Find busy events from a year ago to two years in the future.""" last_year = today - timedelta(days=365) next_year = today + timedelta(days=2 * 365) @@ -311,6 +312,7 @@ def gap_list( url=flask.url_for("trip_page", start=trip.start.isoformat()), ) ) + # pprint(events) busy_events = [ e @@ -318,7 +320,41 @@ def gap_list( if e.as_date > today and e.as_date < next_year and busy_event(e) ] - return find_gaps(busy_events) + return busy_events + + +def weekends(busy_events: list[Event]) -> typing.Sequence[StrDict]: + """Next ten weekends.""" + today = datetime.today() + weekday = today.weekday() + + # Calculate the difference to the next or previous Saturday + if weekday == 6: # Sunday + start_date = (today - timedelta(days=1)).date() + else: + start_date = (today + timedelta(days=(5 - weekday))).date() + + weekends_info = [] + for i in range(52): + saturday = start_date + timedelta(weeks=i) + sunday = saturday + timedelta(days=1) + + saturday_events = [ + event + for event in busy_events + if event.end_date and event.as_date <= saturday <= event.end_as_date + ] + sunday_events = [ + event + for event in busy_events + if event.end_date and event.as_date <= sunday <= event.end_as_date + ] + + weekends_info.append( + {"date": saturday, "saturday": saturday_events, "sunday": sunday_events} + ) + + return weekends_info async def get_data( diff --git a/templates/navbar.html b/templates/navbar.html index 2ba8830..4f4046c 100644 --- a/templates/navbar.html +++ b/templates/navbar.html @@ -7,6 +7,7 @@ {"endpoint": "travel_list", "label": "Travel" }, {"endpoint": "accommodation_list", "label": "Accommodation" }, {"endpoint": "gaps_page", "label": "Gaps" }, + {"endpoint": "weekends", "label": "Weekends" }, {"endpoint": "launch_list", "label": "Space launches" }, {"endpoint": "holiday_list", "label": "Holidays" }, ] %} diff --git a/templates/weekends.html b/templates/weekends.html new file mode 100644 index 0000000..a5d7659 --- /dev/null +++ b/templates/weekends.html @@ -0,0 +1,44 @@ +{% extends "base.html" %} + +{% block content %} +
    + +

    Weekends

    + + + + + + + + + + + + {% for weekend in items %} + + + + {% for day in "saturday", "sunday" %} + + {% endfor %} + + {% endfor %} + +
    WeekDateSaturdaySunday
    + {{ weekend.date.isocalendar().week }} + + {{ weekend.date.strftime("%-d %b %Y") }} + + {% if weekend[day] %} + {% for event in weekend[day] %} + {{ event.title }}{% if not loop.last %},{%endif %} + {% endfor %} + {% else %} + 🆓 🍃 📖 + {% endif %} +
    +
    + +{% endblock %} + diff --git a/web_view.py b/web_view.py index db52555..50c0c20 100755 --- a/web_view.py +++ b/web_view.py @@ -87,10 +87,21 @@ async def gaps_page() -> str: """List of available gaps.""" now = datetime.now() trip_list = agenda.trip.build_trip_list() - gaps = agenda.data.gap_list(now.date(), app.config, trip_list) + busy_events = agenda.data.get_busy_events(now.date(), app.config, trip_list) + gaps = agenda.data.find_gaps(busy_events) return flask.render_template("gaps.html", today=now.date(), gaps=gaps) +@app.route("/weekends") +async def weekends() -> str: + """List of available gaps.""" + now = datetime.now() + trip_list = agenda.trip.build_trip_list() + busy_events = agenda.data.get_busy_events(now.date(), app.config, trip_list) + weekends = agenda.data.weekends(busy_events) + return flask.render_template("weekends.html", today=now.date(), items=weekends) + + @app.route("/travel") def travel_list() -> str: """Page showing a list of upcoming travel.""" From 8f749c8e3536ee4141f19ea3e67d64c5cec75c45 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sun, 25 Feb 2024 09:08:19 +0000 Subject: [PATCH 108/449] Allow unprivileged view Closes: #101 --- agenda/data.py | 9 +++++---- templates/macros.html | 16 ++++++++++++++-- templates/navbar.html | 6 +++++- web_view.py | 21 +++++++++++++++++++-- 4 files changed, 43 insertions(+), 9 deletions(-) diff --git a/agenda/data.py b/agenda/data.py index d209020..c84c7c0 100644 --- a/agenda/data.py +++ b/agenda/data.py @@ -312,7 +312,6 @@ def get_busy_events( url=flask.url_for("trip_page", start=trip.start.isoformat()), ) ) - # pprint(events) busy_events = [ e @@ -426,7 +425,11 @@ async def get_data( holiday_list = holidays.get_all(last_year, next_year, data_dir) events += holidays.combine_holidays(holiday_list) - events += birthday.get_birthdays(last_year, os.path.join(my_data, "entities.yaml")) + if flask.g.user.is_authenticated: + events += birthday.get_birthdays( + last_year, os.path.join(my_data, "entities.yaml") + ) + events += domains.renewal_dates(my_data) events += accommodation_events events += travel.all_events(my_data) events += conference.get_list(os.path.join(my_data, "conferences.yaml")) @@ -440,8 +443,6 @@ async def get_data( events += hn.whoishiring(last_year, next_year) events += rio_carnival_events(last_year, next_year) - events += domains.renewal_dates(my_data) - # hide markets that happen while away optional = [ e diff --git a/templates/macros.html b/templates/macros.html index b7eb761..110a780 100644 --- a/templates/macros.html +++ b/templates/macros.html @@ -86,7 +86,13 @@
    {{ item.duration }}
    {{ full_flight_number }}
    -
    {{ item.booking_reference }}
    +
    + {% if g.user.is_authenticated %} + {{ item.booking_reference }} + {% else %} + redacted + {% endif %} +
    flightradar24 | FlightAware @@ -106,6 +112,12 @@
    {{ ((item.arrive - item.depart).total_seconds() // 60) | int }} mins
    {{ item.operator }}
    -
    {{ item.booking_reference }}
    +
    + {% if g.user.is_authenticated %} + {{ item.booking_reference }} + {% else %} + redacted + {% endif %} +
    {% endmacro %} diff --git a/templates/navbar.html b/templates/navbar.html index 4f4046c..f0ea7bf 100644 --- a/templates/navbar.html +++ b/templates/navbar.html @@ -31,7 +31,11 @@ {% endfor %}
    diff --git a/web_view.py b/web_view.py index 50c0c20..8c744ab 100755 --- a/web_view.py +++ b/web_view.py @@ -28,11 +28,15 @@ app = flask.Flask(__name__) app.debug = False app.config.from_object("config.default") -app.before_request(UniAuth.auth.require_authentication) - agenda.error_mail.setup_error_mail(app) +@app.before_request +def handle_auth() -> None: + """Handle autentication and set global user.""" + flask.g.user = UniAuth.auth.get_current_user() + + @app.errorhandler(werkzeug.exceptions.InternalServerError) def exception_handler(e: werkzeug.exceptions.InternalServerError) -> tuple[str, int]: """Handle exception.""" @@ -369,5 +373,18 @@ def auth_callback() -> tuple[str, int] | werkzeug.Response: return UniAuth.auth.auth_callback() +@app.route("/login") +def login() -> werkzeug.Response: + """Login.""" + next_url = flask.request.args["next"] + return UniAuth.auth.redirect_to_login(next_url) + + +@app.route("/logout") +def logout() -> werkzeug.Response: + """Logout.""" + return UniAuth.auth.redirect_to_logout(flask.request.args["next"]) + + if __name__ == "__main__": app.run(host="0.0.0.0") From 5fdfd9d53350e7010e81cac2e524102b071b947e Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Wed, 28 Feb 2024 15:49:48 +0000 Subject: [PATCH 109/449] Generate trip titles from railway station names --- agenda/types.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/agenda/types.py b/agenda/types.py index 608a6fb..cb4f505 100644 --- a/agenda/types.py +++ b/agenda/types.py @@ -34,13 +34,21 @@ class Trip: @property def title(self) -> str: """Trip title.""" - return ( - format_list_with_ampersand( - [conf["name"] for conf in self.conferences] - + [event["title"] for event in self.events] - ) - or "[unnamed trip]" - ) + titles: list[str] = [conf["name"] for conf in self.conferences] + [ + event["title"] for event in self.events + ] + if not titles: + for travel in self.travel: + if travel["depart"] and travel["depart"].date() != self.start: + place = travel["from"] + if place not in titles: + titles.append(place) + if travel["depart"] and travel["depart"].date() != self.end: + place = travel["to"] + if place not in titles: + titles.append(place) + + return format_list_with_ampersand(titles) or "[unnamed trip]" @property def end(self) -> datetime.date | None: From 0e7a4c23864092cbd2ee675c393a914e3cc8ed4b Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Tue, 5 Mar 2024 07:44:12 +0100 Subject: [PATCH 110/449] Fix incorrect docstring --- update_bank_holiday_list.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/update_bank_holiday_list.py b/update_bank_holiday_list.py index 98adb0c..90a62c1 100755 --- a/update_bank_holiday_list.py +++ b/update_bank_holiday_list.py @@ -13,7 +13,7 @@ config = __import__("config.default", fromlist=[""]) async def get_bank_holidays() -> list[StrDict]: - """Call space launch API and cache results.""" + """Call UK Government bank holidays API and cache results.""" return await agenda.uk_holiday.get_holiday_list(config.DATA_DIR) From 7d5cfe859a3001dae6ed35ea4526af28b6583149 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Tue, 5 Mar 2024 08:01:17 +0100 Subject: [PATCH 111/449] Don't show prices for travel and accommodation if not authenticated --- templates/trip_page.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/trip_page.html b/templates/trip_page.html index 651bec2..ac8a381 100644 --- a/templates/trip_page.html +++ b/templates/trip_page.html @@ -144,7 +144,7 @@ country code {{ item.country }} not found {% endif %} - {% if item.price and item.currency %} + {% if g.user.is_authenticated and item.price and item.currency %} | price: {{ item.price }} {{ item.currency }} {% endif %}

    @@ -170,7 +170,7 @@ country code {{ item.country }} not found {% endif %} - {% if item.price and item.currency %} + {% if g.user.is_authenticated and item.price and item.currency %} | price: {{ item.price }} {{ item.currency }} {% endif %}

    From 4ade643de6563d18fb912d476ff158afaf84f09f Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Tue, 5 Mar 2024 10:07:21 +0100 Subject: [PATCH 112/449] Move UPRN and postcode values to config Closes: #136 --- agenda/data.py | 27 ++++++++++++++++++--------- update_bristol_bins.py | 4 +--- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/agenda/data.py b/agenda/data.py index c84c7c0..3e3efa0 100644 --- a/agenda/data.py +++ b/agenda/data.py @@ -105,11 +105,10 @@ def dates_from_rrule( ] -async def waste_collection_events(data_dir: str) -> list[Event]: +async def waste_collection_events( + data_dir: str, postcode: str, uprn: str +) -> list[Event]: """Waste colllection events.""" - postcode = "BS48 3HG" - uprn = "24071046" - html = await waste_schedule.get_html(data_dir, postcode, uprn) root = lxml.html.fromstring(html) events = waste_schedule.parse(root) @@ -117,11 +116,9 @@ async def waste_collection_events(data_dir: str) -> list[Event]: async def bristol_waste_collection_events( - data_dir: str, start_date: date + data_dir: str, start_date: date, uprn: str ) -> list[Event]: """Waste colllection events.""" - uprn = "358335" - return await waste_schedule.get_bristol_gov_uk(start_date, data_dir, uprn) @@ -375,8 +372,20 @@ async def get_data( t0 = time() result_list = await asyncio.gather( time_function("gwr_advance_tickets", gwr.advance_ticket_date, data_dir), - time_function("backwell_bins", waste_collection_events, data_dir), - time_function("bristol_bins", bristol_waste_collection_events, data_dir, today), + time_function( + "backwell_bins", + waste_collection_events, + data_dir, + config["BACKWELL_POSTCODE"], + config["BACKWELL_UPRN"], + ), + time_function( + "bristol_bins", + bristol_waste_collection_events, + data_dir, + today, + config["BRISTOL_UPRN"], + ), ) rockets = thespacedevs.read_cached_launches(rocket_dir) diff --git a/update_bristol_bins.py b/update_bristol_bins.py index 9b489e9..80ab91c 100755 --- a/update_bristol_bins.py +++ b/update_bristol_bins.py @@ -14,10 +14,8 @@ config = __import__("config.default", fromlist=[""]) async def bristol_waste_collection_events() -> list[agenda.types.Event]: """Waste colllection events.""" - uprn = "358335" - return await agenda.waste_schedule.get_bristol_gov_uk( - date.today(), config.DATA_DIR, uprn, refresh=True + date.today(), config.DATA_DIR, config.BRISTOL_UPRN, refresh=True ) From a37af733cd16da9b1fa2996873162bbe0d4ce592 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Tue, 5 Mar 2024 10:26:54 +0100 Subject: [PATCH 113/449] Combine cron update scripts into one script Closes: #135 --- update.py | 124 ++++++++++++++++++++++++++++++ update_bank_holiday_list.py | 26 ------- update_bristol_bins.py | 29 ------- update_gwr_advance_ticket_date.py | 66 ---------------- update_thespacedevs.py | 20 ----- 5 files changed, 124 insertions(+), 141 deletions(-) create mode 100755 update.py delete mode 100755 update_bank_holiday_list.py delete mode 100755 update_bristol_bins.py delete mode 100755 update_gwr_advance_ticket_date.py delete mode 100755 update_thespacedevs.py diff --git a/update.py b/update.py new file mode 100755 index 0000000..18265d2 --- /dev/null +++ b/update.py @@ -0,0 +1,124 @@ +#!/usr/bin/python3 +"""Combined update script for various data sources.""" + +import asyncio +import os +import smtplib +import sys +from datetime import date, datetime +from email.message import EmailMessage +from email.utils import formatdate, make_msgid +from time import time + +import requests + +import agenda.thespacedevs +import agenda.types +import agenda.uk_holiday +import agenda.waste_schedule +from agenda import gwr + +config = __import__("config.default", fromlist=[""]) + + +async def update_bank_holidays() -> None: + """Update cached copy of UK Bank holidays.""" + t0 = time() + events = await agenda.uk_holiday.get_holiday_list(config.DATA_DIR) + time_taken = time() - t0 + if not sys.stdin.isatty(): + return + print(len(events), "bank holidays in list") + print(f"took {time_taken:.1f} seconds") + + +async def update_bristol_bins() -> None: + """Update waste schedule from Bristol City Council.""" + t0 = time() + events = await agenda.waste_schedule.get_bristol_gov_uk( + date.today(), config.DATA_DIR, config.BRISTOL_UPRN, refresh=True + ) + time_taken = time() - t0 + if not sys.stdin.isatty(): + return + for event in events: + print(event) + print(f"took {time_taken:.1f} seconds") + + +def send_mail(subject: str, body: str) -> None: + """Send an e-mail.""" + msg = EmailMessage() + + msg["Subject"] = subject + msg["To"] = f"{config.NAME} <{config.MAIL_TO}>" + msg["From"] = f"{config.NAME} <{config.MAIL_FROM}>" + msg["Date"] = formatdate() + msg["Message-ID"] = make_msgid() + + # Add extra mail headers + for header, value in config.getattr("MAIL_HEADERS", []): + msg[header] = value + + msg.set_content(body) + + s = smtplib.SMTP(config.SMTP_HOST) + s.sendmail(config.MAIL_TO, [config.MAIL_TO], msg.as_string()) + s.quit() + + +def update_gwr_advance_ticket_date() -> None: + """Update GWR advance ticket date cache.""" + filename = os.path.join(config.DATA_DIR, "advance-tickets.html") + existing_html = open(filename).read() + existing_date = gwr.extract_weekday_date(existing_html) + + new_html = requests.get(gwr.url).text + open(filename, "w").write(new_html) + + new_date = gwr.extract_weekday_date(new_html) + + if existing_date == new_date: + if sys.stdin.isatty(): + print("date has't changed:", existing_date) + return + + subject = f"New GWR advance ticket booking date: {new_date}" + body = f"""Old date: {existing_date} +New date: {new_date} + +{gwr.url} + +Agenda: https://edwardbetts.com/agenda/ +""" + send_mail(subject, body) + + +def update_thespacedevs() -> None: + """Update cache of space launch API.""" + rocket_dir = os.path.join(config.DATA_DIR, "thespacedevs") + + t0 = time() + rockets = agenda.thespacedevs.next_launch_api(rocket_dir) + time_taken = time() - t0 + if not sys.stdin.isatty(): + return + print(len(rockets), "launches") + print(f"took {time_taken:.1f} seconds") + + +def main() -> None: + """Update caches.""" + now = datetime.now() + hour = now.hour + + if hour % 3 == 0: + asyncio.run(update_bank_holidays()) + asyncio.run(update_bristol_bins()) + update_gwr_advance_ticket_date() + + update_thespacedevs() + + +if __name__ == "__main__": + main() diff --git a/update_bank_holiday_list.py b/update_bank_holiday_list.py deleted file mode 100755 index 90a62c1..0000000 --- a/update_bank_holiday_list.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/python3 -"""Update cached copy of UK Bank holidays.""" - -import asyncio -import sys -from time import time - -import agenda.types -import agenda.uk_holiday -from agenda.types import StrDict - -config = __import__("config.default", fromlist=[""]) - - -async def get_bank_holidays() -> list[StrDict]: - """Call UK Government bank holidays API and cache results.""" - return await agenda.uk_holiday.get_holiday_list(config.DATA_DIR) - - -t0 = time() -events: list[StrDict] = asyncio.run(get_bank_holidays()) -time_taken = time() - t0 -if not sys.stdin.isatty(): - sys.exit(0) -print(len(events), "bank holidays in list") -print(f"took {time_taken:.1f} seconds") diff --git a/update_bristol_bins.py b/update_bristol_bins.py deleted file mode 100755 index 80ab91c..0000000 --- a/update_bristol_bins.py +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/python3 -"""Update waste schedule from Bristol City Council.""" - -import asyncio -import sys -from datetime import date -from time import time - -import agenda.types -import agenda.waste_schedule - -config = __import__("config.default", fromlist=[""]) - - -async def bristol_waste_collection_events() -> list[agenda.types.Event]: - """Waste colllection events.""" - return await agenda.waste_schedule.get_bristol_gov_uk( - date.today(), config.DATA_DIR, config.BRISTOL_UPRN, refresh=True - ) - - -today = date.today() -t0 = time() -events = asyncio.run(bristol_waste_collection_events()) -time_taken = time() - t0 -if sys.stdin.isatty(): - for event in events: - print(event) - print(f"took {time_taken:.1f} seconds") diff --git a/update_gwr_advance_ticket_date.py b/update_gwr_advance_ticket_date.py deleted file mode 100755 index 2a02e3d..0000000 --- a/update_gwr_advance_ticket_date.py +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/python3 -"""Update GWR advance ticket date cache.""" - -import os.path -import smtplib -import sys -from email.message import EmailMessage -from email.utils import formatdate, make_msgid - -import requests - -from agenda import gwr - -config = __import__("config.default", fromlist=[""]) - - -def send_mail(subject: str, body: str) -> None: - """Send an e-mail.""" - msg = EmailMessage() - - msg["Subject"] = subject - msg["To"] = f"{config.NAME} <{config.MAIL_TO}>" - msg["From"] = f"{config.NAME} <{config.MAIL_FROM}>" - msg["Date"] = formatdate() - msg["Message-ID"] = make_msgid() - - # Add extra mail headers - for header, value in config.getattr("MAIL_HEADERS", []): - msg[header] = value - - msg.set_content(body) - - s = smtplib.SMTP(config.SMTP_HOST) - s.sendmail(config.MAIL_TO, [config.MAIL_TO], msg.as_string()) - s.quit() - - -def main() -> None: - """Get date from web page and compare with existing.""" - filename = os.path.join(config.DATA_DIR, "advance-tickets.html") - existing_html = open(filename).read() - existing_date = gwr.extract_weekday_date(existing_html) - - new_html = requests.get(gwr.url).text - open(filename, "w").write(new_html) - - new_date = gwr.extract_weekday_date(new_html) - - if existing_date == new_date: - if sys.stdin.isatty(): - print("date has't changed:", existing_date) - return - - subject = f"New GWR advance ticket booking date: {new_date}" - body = f"""Old date: {existing_date} -New date: {new_date} - -{gwr.url} - -Agenda: https://edwardbetts.com/agenda/ -""" - send_mail(subject, body) - - -if __name__ == "__main__": - main() diff --git a/update_thespacedevs.py b/update_thespacedevs.py deleted file mode 100755 index e5b9ec8..0000000 --- a/update_thespacedevs.py +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/python3 -"""Update cache of space launch API.""" - -import os -import sys -from time import time - -import agenda.thespacedevs - -config = __import__("config.default", fromlist=[""]) -rocket_dir = os.path.join(config.DATA_DIR, "thespacedevs") - -t0 = time() - -rockets = agenda.thespacedevs.next_launch_api(rocket_dir) -time_taken = time() - t0 -if not sys.stdin.isatty(): - sys.exit(0) -print(len(rockets), "launches") -print(f"took {time_taken:.1f} seconds") From ff15f380fabe78d2cf2b877805f072dfb58092d6 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Tue, 5 Mar 2024 12:28:34 +0100 Subject: [PATCH 114/449] Consider current trips for free weekends list Closes: #138 --- agenda/data.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/agenda/data.py b/agenda/data.py index 3e3efa0..65a7731 100644 --- a/agenda/data.py +++ b/agenda/data.py @@ -313,7 +313,9 @@ def get_busy_events( busy_events = [ e for e in sorted(events, key=lambda e: e.as_date) - if e.as_date > today and e.as_date < next_year and busy_event(e) + if (e.as_date >= today or (e.end_date and e.end_as_date >= today)) + and e.as_date < next_year + and busy_event(e) ] return busy_events From 96ab89b42fca7d0490d9dc03c66c870eb367a17e Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Tue, 5 Mar 2024 11:29:52 +0000 Subject: [PATCH 115/449] No need for emojis for free weekends --- templates/weekends.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/weekends.html b/templates/weekends.html index a5d7659..831bd59 100644 --- a/templates/weekends.html +++ b/templates/weekends.html @@ -30,7 +30,7 @@ {{ event.title }}{% if not loop.last %},{%endif %} {% endfor %} {% else %} - 🆓 🍃 📖 + free {% endif %} {% endfor %} From 1ed6c50ad891a2583e8236547384aedbfdcea76a Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Mon, 11 Mar 2024 10:46:18 +0100 Subject: [PATCH 116/449] Only show trains with a full departure datetime --- web_view.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/web_view.py b/web_view.py index 8c744ab..468e58f 100755 --- a/web_view.py +++ b/web_view.py @@ -111,7 +111,11 @@ def travel_list() -> str: """Page showing a list of upcoming travel.""" data_dir = app.config["PERSONAL_DATA"] flights = travel.parse_yaml("flights", data_dir) - trains = travel.parse_yaml("trains", data_dir) + trains = [ + item + for item in travel.parse_yaml("trains", data_dir) + if isinstance(item["depart"], datetime) + ] return flask.render_template("travel.html", flights=flights, trains=trains) From f1338e5970b8067ab0adfc017eb66635519f5687 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Mon, 11 Mar 2024 10:53:55 +0100 Subject: [PATCH 117/449] Handle rail journeys without specific time --- agenda/trip.py | 15 ++++++++++----- agenda/types.py | 2 +- templates/macros.html | 2 +- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/agenda/trip.py b/agenda/trip.py index fe2be90..bf22c3d 100644 --- a/agenda/trip.py +++ b/agenda/trip.py @@ -1,6 +1,6 @@ -import operator import os -from datetime import date +from datetime import date, datetime, time +from zoneinfo import ZoneInfo import flask import yaml @@ -41,6 +41,13 @@ def load_trains() -> list[StrDict]: return trains +def depart_datetime(item: StrDict) -> datetime: + depart = item["depart"] + if isinstance(depart, datetime): + return depart + return datetime.combine(depart, time.min).replace(tzinfo=ZoneInfo("UTC")) + + def load_flights() -> list[StrDict]: """Load flights.""" data_dir = flask.current_app.config["PERSONAL_DATA"] @@ -63,9 +70,7 @@ def build_trip_list() -> list[Trip]: data_dir = flask.current_app.config["PERSONAL_DATA"] - travel_items = sorted( - load_flights() + load_trains(), key=operator.itemgetter("depart") - ) + travel_items = sorted(load_flights() + load_trains(), key=depart_datetime) data = { "travel": travel_items, diff --git a/agenda/types.py b/agenda/types.py index cb4f505..4a569d6 100644 --- a/agenda/types.py +++ b/agenda/types.py @@ -60,7 +60,7 @@ class Trip: ) assert isinstance(max_conference_end, datetime.date) - arrive = [item["arrive"].date() for item in self.travel if "arrive" in item] + arrive = [as_date(item["arrive"]) for item in self.travel if "arrive" in item] travel_end = max(arrive) if arrive else datetime.date.min assert isinstance(travel_end, datetime.date) diff --git a/templates/macros.html b/templates/macros.html index 110a780..1ce9ec8 100644 --- a/templates/macros.html +++ b/templates/macros.html @@ -107,7 +107,7 @@
    {% if item.arrive %} {{ item.arrive.strftime("%H:%M") }} - {% if item.arrive.date() != item.depart.date() %}+1 day{% endif %} + {% if item.depart != item.arrive and item.arrive.date() != item.depart.date() %}+1 day{% endif %} {% endif %}
    {{ ((item.arrive - item.depart).total_seconds() // 60) | int }} mins
    From 4ebb08f68e9489cba0efdaef6ebb4a3b24d0c9c4 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Mon, 11 Mar 2024 15:58:56 +0000 Subject: [PATCH 118/449] Add command line utility to validate YAML --- agenda/trip.py | 23 +++++++++++------------ validate_yaml.py | 24 ++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 12 deletions(-) create mode 100755 validate_yaml.py diff --git a/agenda/trip.py b/agenda/trip.py index bf22c3d..b303a6a 100644 --- a/agenda/trip.py +++ b/agenda/trip.py @@ -9,20 +9,17 @@ from agenda import travel from agenda.types import StrDict, Trip -def load_travel(travel_type: str) -> list[StrDict]: +def load_travel(travel_type: str, data_dir: str) -> list[StrDict]: """Read flight and train journeys.""" - data_dir = flask.current_app.config["PERSONAL_DATA"] items = travel.parse_yaml(travel_type + "s", data_dir) for item in items: item["type"] = travel_type return items -def load_trains() -> list[StrDict]: +def load_trains(data_dir: str) -> list[StrDict]: """Load trains.""" - data_dir = flask.current_app.config["PERSONAL_DATA"] - - trains = load_travel("train") + trains = load_travel("train", data_dir) stations = travel.parse_yaml("stations", data_dir) by_name = {station["name"]: station for station in stations} @@ -48,10 +45,9 @@ def depart_datetime(item: StrDict) -> datetime: return datetime.combine(depart, time.min).replace(tzinfo=ZoneInfo("UTC")) -def load_flights() -> list[StrDict]: +def load_flights(data_dir: str) -> list[StrDict]: """Load flights.""" - data_dir = flask.current_app.config["PERSONAL_DATA"] - flights = load_travel("flight") + flights = load_travel("flight", data_dir) airlines = yaml.safe_load(open(os.path.join(data_dir, "airlines.yaml"))) airports = travel.parse_yaml("airports", data_dir) for flight in flights: @@ -64,13 +60,16 @@ def load_flights() -> list[StrDict]: return flights -def build_trip_list() -> list[Trip]: +def build_trip_list(data_dir: str | None = None) -> list[Trip]: """Generate list of trips.""" trips: dict[date, Trip] = {} - data_dir = flask.current_app.config["PERSONAL_DATA"] + if data_dir is None: + data_dir = flask.current_app.config["PERSONAL_DATA"] - travel_items = sorted(load_flights() + load_trains(), key=depart_datetime) + travel_items = sorted( + load_flights(data_dir) + load_trains(data_dir), key=depart_datetime + ) data = { "travel": travel_items, diff --git a/validate_yaml.py b/validate_yaml.py new file mode 100755 index 0000000..e506441 --- /dev/null +++ b/validate_yaml.py @@ -0,0 +1,24 @@ +#!/usr/bin/python3 +"""Load YAML data to ensure validity.""" + +import os + +import agenda.conference +import agenda.travel +import agenda.trip + +config = __import__("config.default", fromlist=[""]) + +data_dir = config.PERSONAL_DATA + +trip_list = agenda.trip.build_trip_list(data_dir) +print(len(trip_list), "trips") + +flights = agenda.travel.parse_yaml("flights", data_dir) +print(len(flights), "flights") + +trains = agenda.travel.parse_yaml("trains", data_dir) +print(len(trains), "trains") + +conferences = agenda.conference.get_list(os.path.join(data_dir, "conferences.yaml")) +print(len(conferences), "conferences") From 9d691bee40d9fe5dc7f33246322c4861c18fff0f Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Tue, 12 Mar 2024 15:09:53 +0000 Subject: [PATCH 119/449] Fix trip page that crashes when showing Unicode Kosovo flag Closes: #139 --- agenda/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/agenda/__init__.py b/agenda/__init__.py index 80be7d2..866e7e3 100644 --- a/agenda/__init__.py +++ b/agenda/__init__.py @@ -32,7 +32,9 @@ def get_country(alpha_2: str) -> pycountry.db.Country | None: if not alpha_2: return None if alpha_2 == "xk": - return pycountry.db.Country(flag="\U0001F1FD\U0001F1F0", name="Kosovo") + return pycountry.db.Country( + flag="\U0001F1FD\U0001F1F0", name="Kosovo", alpha_2="xk" + ) country: pycountry.db.Country if len(alpha_2) == 2: From e3cae68d2f669c0022b26c532f5bb03b28412b3a Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Tue, 12 Mar 2024 15:10:17 +0000 Subject: [PATCH 120/449] Flight arrive can be missing --- templates/trip_page.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/templates/trip_page.html b/templates/trip_page.html index ac8a381..1743951 100644 --- a/templates/trip_page.html +++ b/templates/trip_page.html @@ -200,10 +200,12 @@ {{ item.airline_name }} ({{ item.airline }}) ✨ {{ display_datetime(item.depart) }} + {% if item.arrive %} → {{ item.arrive.strftime("%H:%M %z") }} ✨ {{ ((item.arrive - item.depart).total_seconds() // 60) | int }} mins + {% endif %} ✨ {{ item.airline }}{{ item.flight_number }}
    From d690442f0fb3ac212a57758d7cd5fd6a4777ad7b Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Tue, 26 Mar 2024 14:54:02 +0000 Subject: [PATCH 121/449] Add option for unpublished trips --- agenda/trip.py | 9 ++++++++- agenda/types.py | 4 ++++ web_view.py | 21 ++++++++++++++++++--- 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/agenda/trip.py b/agenda/trip.py index b303a6a..06fbf2a 100644 --- a/agenda/trip.py +++ b/agenda/trip.py @@ -67,6 +67,10 @@ def build_trip_list(data_dir: str | None = None) -> list[Trip]: if data_dir is None: data_dir = flask.current_app.config["PERSONAL_DATA"] + yaml_trip_list = travel.parse_yaml("trips", data_dir) + + yaml_trip_lookup = {item["trip"]: item for item in yaml_trip_list} + travel_items = sorted( load_flights(data_dir) + load_trains(data_dir), key=depart_datetime ) @@ -84,7 +88,10 @@ def build_trip_list(data_dir: str | None = None) -> list[Trip]: if not (start := item.get("trip")): continue if start not in trips: - trips[start] = Trip(start=start) + from_yaml = yaml_trip_lookup.get(start, {}) + trips[start] = Trip( + start=start, **{k: v for k, v in from_yaml.items() if k != "trip"} + ) getattr(trips[start], key).append(item) return [trip for _, trip in sorted(trips.items())] diff --git a/agenda/types.py b/agenda/types.py index 4a569d6..ed59d86 100644 --- a/agenda/types.py +++ b/agenda/types.py @@ -30,10 +30,14 @@ class Trip: accommodation: list[StrDict] = field(default_factory=list) conferences: list[StrDict] = field(default_factory=list) events: list[StrDict] = field(default_factory=list) + name: str | None = None + private: bool = False @property def title(self) -> str: """Trip title.""" + if self.name: + return self.name titles: list[str] = [conf["name"] for conf in self.conferences] + [ event["title"] for event in self.events ] diff --git a/web_view.py b/web_view.py index 468e58f..33be6f7 100755 --- a/web_view.py +++ b/web_view.py @@ -209,7 +209,11 @@ def accommodation_list() -> str: @app.route("/trip") def trip_list() -> str: """Page showing a list of trips.""" - trip_list = agenda.trip.build_trip_list() + trip_list = [ + trip + for trip in agenda.trip.build_trip_list() + if flask.g.user.is_authenticated or not trip.private + ] today = date.today() current = [ @@ -242,7 +246,11 @@ def trip_list() -> str: @app.route("/trip/text") def trip_list_text() -> str: """Page showing a list of trips.""" - trip_list = agenda.trip.build_trip_list() + trip_list = [ + trip + for trip in agenda.trip.build_trip_list() + if flask.g.user.is_authenticated or not trip.private + ] today = date.today() future = [item for item in trip_list if item.start > today] @@ -292,7 +300,14 @@ def human_readable_delta(future_date: date) -> str | None: @app.route("/trip/") def trip_page(start: str) -> str: """Individual trip page.""" - trip_iter = iter(agenda.trip.build_trip_list()) + + trip_list = [ + trip + for trip in agenda.trip.build_trip_list() + if flask.g.user.is_authenticated or not trip.private + ] + + trip_iter = iter(trip_list) today = date.today() data_dir = flask.current_app.config["PERSONAL_DATA"] From 826eafbc8613ac10dc4d513bd2fabc642a46b36a Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Wed, 27 Mar 2024 16:34:24 +0000 Subject: [PATCH 122/449] Add UnknownStation Exception --- agenda/trip.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/agenda/trip.py b/agenda/trip.py index 06fbf2a..db148e9 100644 --- a/agenda/trip.py +++ b/agenda/trip.py @@ -9,6 +9,12 @@ from agenda import travel from agenda.types import StrDict, Trip +class UnknownStation(Exception): + """Unknown station.""" + + pass + + def load_travel(travel_type: str, data_dir: str) -> list[StrDict]: """Read flight and train journeys.""" items = travel.parse_yaml(travel_type + "s", data_dir) @@ -24,8 +30,10 @@ def load_trains(data_dir: str) -> list[StrDict]: by_name = {station["name"]: station for station in stations} for train in trains: - assert train["from"] in by_name - assert train["to"] in by_name + if train["from"] not in by_name: + raise UnknownStation(train["from"]) + if train["to"] not in by_name: + raise UnknownStation(train["to"]) train["from_station"] = by_name[train["from"]] train["to_station"] = by_name[train["to"]] From cff981eb8ba74d24bae12e18f0c48860935cb3a5 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Wed, 27 Mar 2024 16:35:13 +0000 Subject: [PATCH 123/449] Adjust default event duration to be 30 minutes --- agenda/calendar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agenda/calendar.py b/agenda/calendar.py index 56ef6c8..039c546 100644 --- a/agenda/calendar.py +++ b/agenda/calendar.py @@ -61,7 +61,7 @@ def build_events(events: list[Event]) -> list[dict[str, typing.Any]]: continue if e.has_time: - end = e.end_date or e.date + timedelta(hours=1) + end = e.end_date or e.date + timedelta(minutes=30) else: end = (e.end_as_date if e.end_date else e.as_date) + one_day item = { From 6018f0217d22ad39d73650db6c4743b6a2344a3f Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Wed, 27 Mar 2024 16:37:20 +0000 Subject: [PATCH 124/449] Make use of CDN optional --- templates/base.html | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/templates/base.html b/templates/base.html index 7b1cbde..08ccfb7 100644 --- a/templates/base.html +++ b/templates/base.html @@ -7,7 +7,11 @@ {% block title %}{% endblock %} - + {% if config.USE_CDN %} + + {% else %} + + {% endif %} {% block style %} {% endblock %} @@ -18,6 +22,10 @@ {% block nav %}{{ navbar() }}{% endblock %} {% block content %}{% endblock %} {% block scripts %}{% endblock %} - + {% if config.USE_CDN %} + + {% else %} + + {% endif %} From 1e90df76dde5b1c6eb0a75ad7b7d5f60ed219f94 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Wed, 27 Mar 2024 16:38:18 +0000 Subject: [PATCH 125/449] Bug fix --- update.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/update.py b/update.py index 18265d2..e03adc3 100755 --- a/update.py +++ b/update.py @@ -57,7 +57,7 @@ def send_mail(subject: str, body: str) -> None: msg["Message-ID"] = make_msgid() # Add extra mail headers - for header, value in config.getattr("MAIL_HEADERS", []): + for header, value in config.MAIL_HEADERS: msg[header] = value msg.set_content(body) From 422cd8aa9dc47b92d3523824a4f7a232827bebc0 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Wed, 27 Mar 2024 17:47:25 +0000 Subject: [PATCH 126/449] Use gandi API to get domain renewal dates Closes: #134 --- agenda/data.py | 3 +++ agenda/gandi.py | 26 ++++++++++++++++++++++++++ update.py | 15 +++++++++++++++ 3 files changed, 44 insertions(+) create mode 100644 agenda/gandi.py diff --git a/agenda/data.py b/agenda/data.py index 65a7731..d9efcef 100644 --- a/agenda/data.py +++ b/agenda/data.py @@ -23,6 +23,7 @@ from . import ( conference, domains, economist, + gandi, gwr, hn, holidays, @@ -38,6 +39,7 @@ from . import ( ) from .types import Event, StrDict, Trip + here = dateutil.tz.tzlocal() # deadline to file tax return @@ -449,6 +451,7 @@ async def get_data( events += results[key] events += read_events_yaml(my_data, last_year, next_year) events += subscription.get_events(os.path.join(my_data, "subscriptions.yaml")) + events += gandi.get_events(data_dir) events += economist.publication_dates(last_week, next_year) events += meetup.get_events(my_data) events += hn.whoishiring(last_year, next_year) diff --git a/agenda/gandi.py b/agenda/gandi.py new file mode 100644 index 0000000..b5e60e1 --- /dev/null +++ b/agenda/gandi.py @@ -0,0 +1,26 @@ +"""Gandi domain renewal dates.""" + +import os +from .types import Event + +import json + + +def get_events(data_dir: str) -> list[Event]: + """Get subscription renewal dates.""" + filename = os.path.join(data_dir, "gandi_domains.json") + + with open(filename) as f: + items = json.load(f) + + assert isinstance(items, list) + assert all(item["fqdn"] and item["dates"]["registry_ends_at"] for item in items) + + return [ + Event( + date=item["dates"]["registry_ends_at"], + name="domain", + title=item["fqdn"] + " renewal", + ) + for item in items + ] diff --git a/update.py b/update.py index e03adc3..75d75d6 100755 --- a/update.py +++ b/update.py @@ -18,6 +18,7 @@ import agenda.uk_holiday import agenda.waste_schedule from agenda import gwr + config = __import__("config.default", fromlist=[""]) @@ -107,6 +108,20 @@ def update_thespacedevs() -> None: print(f"took {time_taken:.1f} seconds") +def update_gandi() -> None: + """Retrieve list of domains from gandi.net.""" + url = "https://api.gandi.net/v5/domain/domains" + headers = {"authorization": "Bearer " + config.GANDI_TOKEN} + filename = os.path.join(config.DATA_DIR, "gandi_domains.json") + + r = requests.request("GET", url, headers=headers) + items = r.json() + assert isinstance(items, list) + assert all(item["fqdn"] and item["dates"]["registry_ends_at"] for item in items) + with open(filename, "w") as out: + out.write(r.text) + + def main() -> None: """Update caches.""" now = datetime.now() From efc660b0ac7f629da17ec567d8e63436a2fe0472 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sat, 30 Mar 2024 10:18:21 +0000 Subject: [PATCH 127/449] Avoid CDN for frontend CSS and Javascript Closes: #137 --- package.json | 27 +++++++++++++++++++++++++++ templates/base.html | 12 ++---------- templates/index.html | 6 +++--- templates/trip_list.html | 10 +++------- templates/trip_page.html | 11 +++-------- webpack.config.js | 18 ++++++++++++++++++ 6 files changed, 56 insertions(+), 28 deletions(-) create mode 100644 package.json create mode 100644 webpack.config.js diff --git a/package.json b/package.json new file mode 100644 index 0000000..082b994 --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "agenda", + "version": "1.0.0", + "directories": { + "test": "tests" + }, + "repository": { + "type": "git", + "url": "https://git.4angle.com/edward/agenda.git" + }, + "license": "ISC", + "devDependencies": { + "copy-webpack-plugin": "^12.0.2", + "webpack": "^5.91.0", + "webpack-cli": "^5.1.4" + }, + "dependencies": { + "@fullcalendar/core": "^6.1.11", + "@fullcalendar/daygrid": "^6.1.11", + "@fullcalendar/list": "^6.1.11", + "@fullcalendar/timegrid": "^6.1.11", + "bootstrap": "^5.3.3", + "es-module-shims": "^1.8.3", + "leaflet": "^1.9.4", + "leaflet.geodesic": "^2.7.1" + } +} diff --git a/templates/base.html b/templates/base.html index 08ccfb7..c89cf90 100644 --- a/templates/base.html +++ b/templates/base.html @@ -7,11 +7,7 @@ {% block title %}{% endblock %} - {% if config.USE_CDN %} - - {% else %} - - {% endif %} + {% block style %} {% endblock %} @@ -22,10 +18,6 @@ {% block nav %}{{ navbar() }}{% endblock %} {% block content %}{% endblock %} {% block scripts %}{% endblock %} - {% if config.USE_CDN %} - - {% else %} - - {% endif %} + diff --git a/templates/index.html b/templates/index.html index 4034a2f..720520a 100644 --- a/templates/index.html +++ b/templates/index.html @@ -4,10 +4,10 @@ Agenda - + - + + diff --git a/templates/trip_list.html b/templates/trip_list.html index 22f2f56..83525ee 100644 --- a/templates/trip_list.html +++ b/templates/trip_list.html @@ -6,9 +6,7 @@ {% block style %} - + {% set conference_column_count = 7 %} {% set accommodation_column_count = 7 %} @@ -112,11 +110,9 @@ {% block scripts %} - + - + - - + + diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..67d98db --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,18 @@ +const path = require('path'); +const CopyPlugin = require('copy-webpack-plugin'); + +module.exports = { + mode: 'development', + entry: './frontend/index.js', // Ensure this entry point exists and is valid. + plugins: [ + new CopyPlugin({ + patterns: [ + // Copy Bootstrap's CSS and JS from node_modules to your desired location + { from: 'node_modules/bootstrap/dist', to: path.resolve(__dirname, 'static/bootstrap5') }, + { from: 'node_modules/leaflet/dist', to: path.resolve(__dirname, 'static/leaflet') }, + { from: 'node_modules/leaflet.geodesic/dist', to: path.resolve(__dirname, 'static/leaflet-geodesic'), }, + { from: 'node_modules/es-module-shims/dist', to: path.resolve(__dirname, 'static/es-module-shims') } + ], + }), + ] +}; From ebd46a7a21b6a2d341ac9b59a609741ce6fb90cd Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sat, 30 Mar 2024 10:19:54 +0000 Subject: [PATCH 128/449] Add empty index.js for webpack --- frontend/index.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 frontend/index.js diff --git a/frontend/index.js b/frontend/index.js new file mode 100644 index 0000000..e69de29 From d813bff812d90bf697f12e0dbba2bd78e451fed3 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sat, 30 Mar 2024 19:31:29 +0000 Subject: [PATCH 129/449] dockbot is optional --- agenda/data.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/agenda/data.py b/agenda/data.py index d9efcef..21a3d16 100644 --- a/agenda/data.py +++ b/agenda/data.py @@ -39,7 +39,6 @@ from . import ( ) from .types import Event, StrDict, Trip - here = dateutil.tz.tzlocal() # deadline to file tax return @@ -267,7 +266,11 @@ def busy_event(e: Event) -> bool: return False lc_title = e.title.lower() - return "rebels" not in lc_title and "south west data social" not in lc_title + return ( + "rebels" not in lc_title + and "south west data social" not in lc_title + and "dorkbot" not in lc_title + ) async def time_function( From 748ec3a1bc3f467c1629b1563f35304c8bc5aaa8 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sat, 30 Mar 2024 19:31:48 +0000 Subject: [PATCH 130/449] pass gandi domain end date as date --- agenda/gandi.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/agenda/gandi.py b/agenda/gandi.py index b5e60e1..d833472 100644 --- a/agenda/gandi.py +++ b/agenda/gandi.py @@ -2,6 +2,7 @@ import os from .types import Event +from datetime import datetime import json @@ -18,7 +19,7 @@ def get_events(data_dir: str) -> list[Event]: return [ Event( - date=item["dates"]["registry_ends_at"], + date=datetime.fromisoformat(item["dates"]["registry_ends_at"]).date(), name="domain", title=item["fqdn"] + " renewal", ) From f0c28d24408d78b1b9e1512e6bd5fa31d3e4f8e4 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sat, 30 Mar 2024 19:32:05 +0000 Subject: [PATCH 131/449] Download domain info from gandi --- update.py | 1 + 1 file changed, 1 insertion(+) diff --git a/update.py b/update.py index 75d75d6..24c0976 100755 --- a/update.py +++ b/update.py @@ -131,6 +131,7 @@ def main() -> None: asyncio.run(update_bank_holidays()) asyncio.run(update_bristol_bins()) update_gwr_advance_ticket_date() + update_gandi() update_thespacedevs() From ae630a8f6859b14409fcddec6a678e3b45d4f4ae Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Mon, 1 Apr 2024 10:36:48 +0100 Subject: [PATCH 132/449] Show links for train journeys Closes: #79 --- templates/macros.html | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/templates/macros.html b/templates/macros.html index 1ce9ec8..d0cd0ab 100644 --- a/templates/macros.html +++ b/templates/macros.html @@ -101,8 +101,13 @@ {% endmacro %} {% macro train_row(item) %} + {% set url = item.url %}
    {{ item.depart.strftime("%a, %d %b %Y") }}
    -
    {{ item.from }} → {{ item.to }}
    +
    {{ item.depart.strftime("%H:%M") }}
    {% if item.arrive %} @@ -119,5 +124,11 @@ redacted {% endif %}
    -
    +
    + {% for leg in item.legs %} + {% if leg.url %} + [{{ loop.index }}] + {% endif %} + {% endfor %} +
    {% endmacro %} From 7208e10cb2b4266f98df2fbeb0ed2b74f28fc56e Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Tue, 2 Apr 2024 10:37:06 +0100 Subject: [PATCH 133/449] Hide booking URLs if not logged in --- templates/macros.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/templates/macros.html b/templates/macros.html index d0cd0ab..d1d42a8 100644 --- a/templates/macros.html +++ b/templates/macros.html @@ -64,7 +64,7 @@ {% endif %}
    - {% if item.url %} + {% if g.user.is_authenticated and item.url %} {{ item.name }} {% else %} {{ item.name }} @@ -104,9 +104,9 @@ {% set url = item.url %}
    {{ item.depart.strftime("%a, %d %b %Y") }}
    - {% if url %}{% endif %} + {% if g.user.is_authenticated and item.url %}{% endif %} {{ item.from }} → {{ item.to }} - {% if url %}{% endif %} + {% if g.user.is_authenticated and item.url %}{% endif %}
    {{ item.depart.strftime("%H:%M") }}
    From e5325a0392c6151ca707f93b1edbb7882594a36a Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Tue, 2 Apr 2024 10:42:06 +0100 Subject: [PATCH 134/449] Hide booking URLs on calendar if not logged in --- agenda/travel.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/agenda/travel.py b/agenda/travel.py index 4825c9c..d3f5975 100644 --- a/agenda/travel.py +++ b/agenda/travel.py @@ -3,6 +3,7 @@ import os import typing +import flask import yaml from .types import Event @@ -26,7 +27,7 @@ def get_flights(data_dir: str) -> list[Event]: end_date=item.get("arrive"), name="transport", title=f'✈️ {item["from"]} to {item["to"]} ({flight_number(item)})', - url=item.get("url"), + url=(item.get("url") if flask.g.user.is_authenticated else None), ) for item in parse_yaml("flights", data_dir) if item["depart"].date() @@ -43,7 +44,7 @@ def get_trains(data_dir: str) -> list[Event]: end_date=leg["arrive"], name="transport", title=f'🚆 {leg["from"]} to {leg["to"]}', - url=item.get("url"), + url=(item.get("url") if flask.g.user.is_authenticated else None), ) for leg in item["legs"] ] From a607f29259bc47f273c3be9fa68465dd8f0d4095 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Fri, 5 Apr 2024 11:21:41 +0200 Subject: [PATCH 135/449] Validate YAML to catch bad train rotues Closes: #143 --- agenda/trip.py | 9 +++++---- validate_yaml.py | 4 ++++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/agenda/trip.py b/agenda/trip.py index db148e9..2a4f7ee 100644 --- a/agenda/trip.py +++ b/agenda/trip.py @@ -165,9 +165,8 @@ def latlon_tuple(stop: StrDict) -> tuple[float, float]: return (stop["latitude"], stop["longitude"]) -def read_geojson(filename: str) -> str: +def read_geojson(data_dir: str, filename: str) -> str: """Read GeoJSON from file.""" - data_dir = flask.current_app.config["PERSONAL_DATA"] return open(os.path.join(data_dir, "train_routes", filename + ".geojson")).read() @@ -238,8 +237,10 @@ def get_trip_routes(trip: Trip) -> list[StrDict]: def get_coordinates_and_routes( - trip_list: list[Trip], + trip_list: list[Trip], data_dir: str | None = None ) -> tuple[list[StrDict], list[StrDict]]: + if data_dir is None: + data_dir = flask.current_app.config["PERSONAL_DATA"] coordinates = [] seen_coordinates: set[tuple[str, str]] = set() routes = [] @@ -260,6 +261,6 @@ def get_coordinates_and_routes( for route in routes: if "geojson_filename" in route: - route["geojson"] = read_geojson(route.pop("geojson_filename")) + route["geojson"] = read_geojson(data_dir, route.pop("geojson_filename")) return (coordinates, routes) diff --git a/validate_yaml.py b/validate_yaml.py index e506441..75fb42e 100755 --- a/validate_yaml.py +++ b/validate_yaml.py @@ -14,6 +14,10 @@ data_dir = config.PERSONAL_DATA trip_list = agenda.trip.build_trip_list(data_dir) print(len(trip_list), "trips") +coords, routes = agenda.trip.get_coordinates_and_routes(trip_list, data_dir) +print(len(coords), "coords") +print(len(routes), "routes") + flights = agenda.travel.parse_yaml("flights", data_dir) print(len(flights), "flights") From 5964899a008ad3e57cbbbb30f8f4d3f62dc4b5b2 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Fri, 5 Apr 2024 11:23:29 +0200 Subject: [PATCH 136/449] Validate YAML to check events --- validate_yaml.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/validate_yaml.py b/validate_yaml.py index 75fb42e..c61a488 100755 --- a/validate_yaml.py +++ b/validate_yaml.py @@ -2,8 +2,10 @@ """Load YAML data to ensure validity.""" import os +from datetime import date, timedelta import agenda.conference +import agenda.data import agenda.travel import agenda.trip @@ -26,3 +28,10 @@ print(len(trains), "trains") conferences = agenda.conference.get_list(os.path.join(data_dir, "conferences.yaml")) print(len(conferences), "conferences") + +today = date.today() +last_year = today - timedelta(days=365) +next_year = today + timedelta(days=2 * 365) + +events = agenda.data.read_events_yaml(data_dir, last_year, next_year) +print(len(events), "events") From 8ef67e0cee05a993d060e3cc035c3845464f5dd9 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Fri, 5 Apr 2024 15:58:44 +0200 Subject: [PATCH 137/449] Add train route distance info --- agenda/travel.py | 48 +++++++++++++++++++++++++++++++++++++++- templates/macros.html | 7 ++++++ templates/travel.html | 4 ++-- templates/trip_list.html | 2 +- templates/trip_page.html | 2 +- web_view.py | 14 ++++++++++-- 6 files changed, 70 insertions(+), 7 deletions(-) diff --git a/agenda/travel.py b/agenda/travel.py index d3f5975..6e3d2c9 100644 --- a/agenda/travel.py +++ b/agenda/travel.py @@ -1,12 +1,14 @@ """Travel.""" +import json import os import typing import flask import yaml +from geopy.distance import geodesic -from .types import Event +from .types import Event, StrDict Leg = dict[str, str] @@ -63,3 +65,47 @@ def flight_number(flight: Leg) -> str: def all_events(data_dir: str) -> list[Event]: """Get all flights and rail journeys.""" return get_trains(data_dir) + get_flights(data_dir) + + +RouteDistances = dict[tuple[str, str], float] + + +def train_leg_distance(geojson_data: StrDict) -> float: + """Calculate the total length of a LineString in kilometers from GeoJSON data.""" + # Extract coordinates + first_object = geojson_data["features"][0]["geometry"] + assert first_object["type"] in ("LineString", "MultiLineString") + + if first_object["type"] == "LineString": + coord_list = [first_object["coordinates"]] + else: + first_object["type"] == "MultiLineString" + coord_list = first_object["coordinates"] + # pprint(coordinates) + + total_length_km = 0.0 + + for coordinates in coord_list: + total_length_km += sum( + float(geodesic(coordinates[i], coordinates[i + 1]).km) + for i in range(len(coordinates) - 1) + ) + + return total_length_km + + +def load_route_distances(data_dir: str) -> RouteDistances: + """Load cache of route distances.""" + route_distances: RouteDistances = {} + with open(os.path.join(data_dir, "route_distances.json")) as f: + for s1, s2, dist in json.load(f): + route_distances[(s1, s2)] = dist + + return route_distances + + +def add_leg_route_distance(leg: StrDict, route_distances: RouteDistances) -> None: + s1, s2 = sorted([leg["from"], leg["to"]]) + dist = route_distances.get((s1, s2)) + if dist: + leg["distance"] = dist diff --git a/templates/macros.html b/templates/macros.html index d1d42a8..c52531b 100644 --- a/templates/macros.html +++ b/templates/macros.html @@ -98,6 +98,8 @@ | FlightAware | radarbox
    +
    +
    {% endmacro %} {% macro train_row(item) %} @@ -131,4 +133,9 @@ {% endif %} {% endfor %}
    +
    + {% if item.distance %} + {{ "{:.1f} km / {:.1f} miles".format(item.distance, item.distance / 1.60934) }} + {% endif %} +
    {% endmacro %} diff --git a/templates/travel.html b/templates/travel.html index e0d8161..87dd16b 100644 --- a/templates/travel.html +++ b/templates/travel.html @@ -5,14 +5,14 @@ +{% endblock %} + +{% macro section(heading, item_list) %} + {% if item_list %} + {% set items = item_list | list %} +

    {{ heading }}

    +

    {{ items | count }} trips

    + {% for trip in items %} + {% set total_distance = trip.total_distance() %} + {% set end = trip.end %} +
    +

    + {{ trip_link(trip) }} + ({{ display_date(trip.start) }})

    +
    Countries: {{ trip.countries_str }}
    + {% if end %} +
    Dates: {{ display_date_no_year(trip.start) }} to {{ display_date_no_year(end) }}
    + {% else %} +
    Start: {{ display_date_no_year(trip.start) }} (end date missing)
    + {% endif %} + {% if total_distance %} +
    Total distance: + {{ "{:,.0f} km / {:,.0f} miles".format(total_distance, total_distance / 1.60934) }} +
    + {% endif %} + + {# + {% for day in trip.days() %} +

    {{ display_date_no_year(day) }}

    + {% endfor %} + #} + + {% for item in trip.conferences %} + {% set country = get_country(item.country) if item.country else None %} +
    +
    +
    + {{ item.name }} + + {{ display_date_no_year(item.start) }} to {{ display_date_no_year(item.end) }} + +
    +

    + Topic: {{ item.topic }} + | Venue: {{ item.venue }} + | Location: {{ item.location }} + {% if country %} + {{ country.flag }} + {% elif item.online %} + 💻 Online + {% else %} + + country code {{ item.country }} not found + + {% endif %} + {% if item.free %} + | free to attend + {% elif item.price and item.currency %} + | price: {{ item.price }} {{ item.currency }} + {% endif %} +

    +
    +
    + {% endfor %} + + + + {% set date_heading = None %} + {% for day, elements in trip.elements_grouped_by_day() %} +

    {{ display_date_no_year(day) }}

    + {% for e in elements %} +
    +
    + {{ display_time(e.when) }} + — + {{ e.element_type }} + — + {{ e.title }} +
    +
    + {% endfor %} + {% endfor %} + +
    + {% endfor %} + {% endif %} +{% endmacro %} + + +{% block content %} +
    +
    +
    +
    +
    + {{ section(heading, trips) }} +
    +
    +{% endblock %} + +{% block scripts %} + + + + + + + +{% endblock %} diff --git a/web_view.py b/web_view.py index c2f1142..2034fe4 100755 --- a/web_view.py +++ b/web_view.py @@ -280,6 +280,54 @@ def trip_list() -> str: ) +@app.route("/trip/past") +def trip_past_list() -> str: + """Page showing a list of past trips.""" + route_distances = agenda.travel.load_route_distances(app.config["DATA_DIR"]) + trip_list = get_trip_list(route_distances) + today = date.today() + + past = [item for item in trip_list if (item.end or item.start) < today] + + coordinates, routes = agenda.trip.get_coordinates_and_routes(past) + + return flask.render_template( + "trip/list.html", + heading="Past trips", + trips=reversed(past), + coordinates=coordinates, + routes=routes, + today=today, + get_country=agenda.get_country, + format_list_with_ampersand=format_list_with_ampersand, + fx_rate=agenda.fx.get_rates(app.config), + ) + + +@app.route("/trip/future") +def trip_future_list() -> str: + """Page showing a list of future trips.""" + route_distances = agenda.travel.load_route_distances(app.config["DATA_DIR"]) + trip_list = get_trip_list(route_distances) + today = date.today() + + future = [item for item in trip_list if item.start > today] + + coordinates, routes = agenda.trip.get_coordinates_and_routes(future) + + return flask.render_template( + "trip/list.html", + heading="Future trips", + trips=future, + coordinates=coordinates, + routes=routes, + today=today, + get_country=agenda.get_country, + format_list_with_ampersand=format_list_with_ampersand, + fx_rate=agenda.fx.get_rates(app.config), + ) + + @app.route("/trip/text") def trip_list_text() -> str: """Page showing a list of trips.""" From afb96bc855a3a9c2cbb53f43070d944fab3cb343 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sat, 18 May 2024 12:04:28 +0200 Subject: [PATCH 177/449] Bug fix validate_yaml.py --- validate_yaml.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/validate_yaml.py b/validate_yaml.py index c61a488..44b9925 100755 --- a/validate_yaml.py +++ b/validate_yaml.py @@ -33,5 +33,5 @@ today = date.today() last_year = today - timedelta(days=365) next_year = today + timedelta(days=2 * 365) -events = agenda.data.read_events_yaml(data_dir, last_year, next_year) +events = agenda.events_yaml.read(data_dir, last_year, next_year) print(len(events), "events") From c9fcf1d5e7ed1570ea1ed492a31d45a1ce3f4660 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sat, 18 May 2024 12:14:34 +0200 Subject: [PATCH 178/449] Include current trip in future list --- web_view.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/web_view.py b/web_view.py index 2034fe4..293cba4 100755 --- a/web_view.py +++ b/web_view.py @@ -311,6 +311,12 @@ def trip_future_list() -> str: trip_list = get_trip_list(route_distances) today = date.today() + current = [ + item + for item in trip_list + if item.start <= today and (item.end or item.start) >= today + ] + future = [item for item in trip_list if item.start > today] coordinates, routes = agenda.trip.get_coordinates_and_routes(future) @@ -318,7 +324,7 @@ def trip_future_list() -> str: return flask.render_template( "trip/list.html", heading="Future trips", - trips=future, + trips=current + future, coordinates=coordinates, routes=routes, today=today, From 2e1cf0ce84d0819438f0065da00b7818e79701ad Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sat, 18 May 2024 12:42:41 +0200 Subject: [PATCH 179/449] Show current trip on future trip map --- web_view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_view.py b/web_view.py index 293cba4..8131935 100755 --- a/web_view.py +++ b/web_view.py @@ -319,7 +319,7 @@ def trip_future_list() -> str: future = [item for item in trip_list if item.start > today] - coordinates, routes = agenda.trip.get_coordinates_and_routes(future) + coordinates, routes = agenda.trip.get_coordinates_and_routes(current + future) return flask.render_template( "trip/list.html", From a253f720dd5f05c7ea375cb462b438b6b45017a6 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sat, 18 May 2024 12:44:23 +0200 Subject: [PATCH 180/449] Switch to singulr for navbar --- templates/navbar.html | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/templates/navbar.html b/templates/navbar.html index 3bc28ff..04fa3f9 100644 --- a/templates/navbar.html +++ b/templates/navbar.html @@ -2,15 +2,15 @@ {% set pages = [ {"endpoint": "index", "label": "Home" }, - {"endpoint": "trip_future_list", "label": "Future trips" }, - {"endpoint": "trip_past_list", "label": "Past trips" }, + {"endpoint": "trip_future_list", "label": "Future trip" }, + {"endpoint": "trip_past_list", "label": "Past trip" }, {"endpoint": "conference_list", "label": "Conference" }, {"endpoint": "travel_list", "label": "Travel" }, {"endpoint": "accommodation_list", "label": "Accommodation" }, - {"endpoint": "gaps_page", "label": "Gaps" }, - {"endpoint": "weekends", "label": "Weekends" }, - {"endpoint": "launch_list", "label": "Space launches" }, - {"endpoint": "holiday_list", "label": "Holidays" }, + {"endpoint": "gaps_page", "label": "Gap" }, + {"endpoint": "weekends", "label": "Weekend" }, + {"endpoint": "launch_list", "label": "Space launch" }, + {"endpoint": "holiday_list", "label": "Holiday" }, ] %} From d5a92c9a8e2f9b9bc54ef1b30baf7fd4ba9e2ad4 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sat, 18 May 2024 12:46:32 +0200 Subject: [PATCH 181/449] Trip list URL to redirect --- web_view.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/web_view.py b/web_view.py index 8131935..286d4ad 100755 --- a/web_view.py +++ b/web_view.py @@ -245,8 +245,8 @@ def get_trip_list( ] -@app.route("/trip") -def trip_list() -> str: +@app.route("/trip/old") +def trip_old_list() -> str: """Page showing a list of trips.""" route_distances = agenda.travel.load_route_distances(app.config["DATA_DIR"]) trip_list = get_trip_list(route_distances) @@ -280,6 +280,12 @@ def trip_list() -> str: ) +@app.route("/trip") +def trip_list() -> werkzeug.Response: + """Trip list to redirect to future trip list.""" + return flask.redirect(flask.url_for("trip_future_list")) + + @app.route("/trip/past") def trip_past_list() -> str: """Page showing a list of past trips.""" From f1a472a944f444a6bc5924a369d99a8784341d1c Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sat, 18 May 2024 14:22:08 +0200 Subject: [PATCH 182/449] Better airport labels --- agenda/types.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/agenda/types.py b/agenda/types.py index 0f4c086..6770b46 100644 --- a/agenda/types.py +++ b/agenda/types.py @@ -43,6 +43,12 @@ class TripElement: detail: StrDict +def airport_label(airport: StrDict) -> str: + """Airport label: name and iata.""" + name = airport.get("alt_name") or airport["city"] + return f"{name} ({airport['iata']})" + + @dataclass class Trip: """Trip.""" @@ -204,12 +210,9 @@ class Trip: for item in self.travel: if item["type"] == "flight": - flight_from = item["from_airport"] - flight_to = item["to_airport"] name = ( - "✈️ " - + f"{flight_from['name']} ({flight_from['iata']}) -> " - + f"{flight_to['name']} ({flight_to['iata']})" + f"✈️ {airport_label(item['from_airport'])} → " + + f"{airport_label(item['to_airport'])}" ) elements.append( From 5b2d2489553eb26b364df3da906be4ccadbcdf4b Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sat, 18 May 2024 14:22:35 +0200 Subject: [PATCH 183/449] Use proper arrow in ferry title --- agenda/types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agenda/types.py b/agenda/types.py index 6770b46..0bd4e36 100644 --- a/agenda/types.py +++ b/agenda/types.py @@ -234,7 +234,7 @@ class Trip: ) ) if item["type"] == "ferry": - name = f"{item['from']} -> {item['to']}" + name = f"{item['from']} → {item['to']}" elements.append( TripElement( when=item["depart"], From 85ebaf7c841806bf7562460e2e0552c8a26635d1 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sat, 18 May 2024 14:23:00 +0200 Subject: [PATCH 184/449] Show indivudal train legs --- agenda/types.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/agenda/types.py b/agenda/types.py index 0bd4e36..50a2612 100644 --- a/agenda/types.py +++ b/agenda/types.py @@ -224,15 +224,16 @@ class Trip: ) ) if item["type"] == "train": - name = f"{item['from']} -> {item['to']}" - elements.append( - TripElement( - when=item["depart"], - title=name, - detail=item, - element_type="train", + for leg in item["legs"]: + name = f"{leg['from']} → {leg['to']}" + elements.append( + TripElement( + when=leg["depart"], + title=name, + detail=leg, + element_type="train", + ) ) - ) if item["type"] == "ferry": name = f"{item['from']} → {item['to']}" elements.append( From ab74ebab34705e5ba875690b34f5b30178a18ea1 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sat, 18 May 2024 14:59:09 +0200 Subject: [PATCH 185/449] Remove unused imports --- templates/trip/list.html | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/templates/trip/list.html b/templates/trip/list.html index f8c228e..7b8ee09 100644 --- a/templates/trip/list.html +++ b/templates/trip/list.html @@ -1,8 +1,6 @@ {% extends "base.html" %} -{% from "macros.html" import trip_link, display_date_no_year, display_date, display_datetime, display_time, conference_row, accommodation_row, flight_row, train_row, ferry_row with context %} - -{% set row = { "flight": flight_row, "train": train_row, "ferry": ferry_row } %} +{% from "macros.html" import trip_link, display_date_no_year, display_date, display_datetime, display_time with context %} {% block title %}Trips - Edward Betts{% endblock %} From 7d376b38f369edf5894ab3883ac684f23cc34754 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sat, 18 May 2024 16:43:06 +0200 Subject: [PATCH 186/449] Use heading in trip list page title --- templates/trip/list.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/trip/list.html b/templates/trip/list.html index 7b8ee09..7b2a3ab 100644 --- a/templates/trip/list.html +++ b/templates/trip/list.html @@ -2,7 +2,7 @@ {% from "macros.html" import trip_link, display_date_no_year, display_date, display_datetime, display_time with context %} -{% block title %}Trips - Edward Betts{% endblock %} +{% block title %}{{ heading }} - Edward Betts{% endblock %} {% block style %} From 455528125c5baf69589ddf4361cf747b68c1babd Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sat, 18 May 2024 16:44:18 +0200 Subject: [PATCH 187/449] Improvements to trip list pages --- agenda/types.py | 23 ++++++++++ requirements.txt | 1 + templates/trip/list.html | 99 ++++++++++++++++++++++------------------ 3 files changed, 79 insertions(+), 44 deletions(-) diff --git a/agenda/types.py b/agenda/types.py index 50a2612..4642566 100644 --- a/agenda/types.py +++ b/agenda/types.py @@ -6,6 +6,7 @@ import typing from collections import Counter from dataclasses import dataclass, field +import emoji from pycountry.db import Country import agenda @@ -42,6 +43,18 @@ class TripElement: element_type: str detail: StrDict + def get_emoji(self) -> str | None: + """Emjoji for trip element.""" + if self.element_type in ("check-in", "check-out"): + return emoji.emojize(":hotel:", language="alias") + if self.element_type == "train": + return emoji.emojize(":train:", language="alias") + if self.element_type == "flight": + return emoji.emojize(":airplane:", language="alias") + if self.element_type == "ferry": + return emoji.emojize(":ferry:", language="alias") + return None + def airport_label(airport: StrDict) -> str: """Airport label: name and iata.""" @@ -259,6 +272,16 @@ class Trip: day = as_date(element.when) grouped_elements[day].append(element) + # Sort elements within each day + for day in grouped_elements: + grouped_elements[day].sort( + key=lambda e: ( + e.element_type == "check-in", # check-out elements last + e.element_type != "check-out", # check-in elements first + as_datetime(e.when), # then sort by time + ) + ) + # Convert the dictionary to a sorted list of tuples grouped_elements_list = sorted(grouped_elements.items()) diff --git a/requirements.txt b/requirements.txt index ba7a169..3ed2d57 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,3 +9,4 @@ dateutil ephem flask requests +emoji diff --git a/templates/trip/list.html b/templates/trip/list.html index 7b2a3ab..662c020 100644 --- a/templates/trip/list.html +++ b/templates/trip/list.html @@ -82,13 +82,21 @@

    {{ heading }}

    {{ items | count }} trips

    {% for trip in items %} + {% set distances_by_transport_type = trip.distances_by_transport_type() %} {% set total_distance = trip.total_distance() %} {% set end = trip.end %}

    {{ trip_link(trip) }} ({{ display_date(trip.start) }})

    -
    Countries: {{ trip.countries_str }}
    +
      + {% for c in trip.countries %} +
    • + {{ c.name }} + {{ c.flag }} +
    • + {% endfor %} +
    {% if end %}
    Dates: {{ display_date_no_year(trip.start) }} to {{ display_date_no_year(end) }}
    {% else %} @@ -100,60 +108,63 @@
    {% endif %} - {# - {% for day in trip.days() %} -

    {{ display_date_no_year(day) }}

    + {% if distances_by_transport_type %} + {% for transport_type, distance in distances_by_transport_type %} +
    {{ transport_type | title }} distance: + {{ "{:,.0f} km / {:,.0f} miles".format(distance, distance / 1.60934) }} +
    + {% endfor %} + {% endif %} + + {% for item in trip.conferences %} + {% set country = get_country(item.country) if item.country else None %} +
    +
    +
    + {{ item.name }} + + {{ display_date_no_year(item.start) }} to {{ display_date_no_year(item.end) }} + +
    +

    + Topic: {{ item.topic }} + | Venue: {{ item.venue }} + | Location: {{ item.location }} + {% if country %} + {{ country.flag }} + {% elif item.online %} + 💻 Online + {% else %} + + country code {{ item.country }} not found + + {% endif %} + {% if item.free %} + | free to attend + {% elif item.price and item.currency %} + | price: {{ item.price }} {{ item.currency }} + {% endif %} +

    +
    +
    {% endfor %} - #} - - {% for item in trip.conferences %} - {% set country = get_country(item.country) if item.country else None %} -
    -
    -
    - {{ item.name }} - - {{ display_date_no_year(item.start) }} to {{ display_date_no_year(item.end) }} - -
    -

    - Topic: {{ item.topic }} - | Venue: {{ item.venue }} - | Location: {{ item.location }} - {% if country %} - {{ country.flag }} - {% elif item.online %} - 💻 Online - {% else %} - - country code {{ item.country }} not found - - {% endif %} - {% if item.free %} - | free to attend - {% elif item.price and item.currency %} - | price: {{ item.price }} {{ item.currency }} - {% endif %} -

    -
    -
    - {% endfor %} - - {% set date_heading = None %} {% for day, elements in trip.elements_grouped_by_day() %}

    {{ display_date_no_year(day) }}

    {% for e in elements %} -
    + {% if e.element_type == "check-out" %} +
    {{ e.get_emoji() }} {{ e.title }} (check-out)
    + {% elif e.element_type == "check-in" %} +
    {{ e.get_emoji() }} {{ e.title }} (check-in)
    + {% else %}
    + {{ e.get_emoji() }} {{ display_time(e.when) }} — - {{ e.element_type }} - — {{ e.title }}
    -
    + {% endif %} {% endfor %} {% endfor %} From 7e8d1561261acc5b52b177271d96263c3c43a408 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sat, 18 May 2024 16:53:38 +0200 Subject: [PATCH 188/449] Show check-in and check-out times --- templates/trip/list.html | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/templates/trip/list.html b/templates/trip/list.html index 662c020..18fd6b9 100644 --- a/templates/trip/list.html +++ b/templates/trip/list.html @@ -153,10 +153,16 @@ {% for day, elements in trip.elements_grouped_by_day() %}

    {{ display_date_no_year(day) }}

    {% for e in elements %} - {% if e.element_type == "check-out" %} -
    {{ e.get_emoji() }} {{ e.title }} (check-out)
    - {% elif e.element_type == "check-in" %} -
    {{ e.get_emoji() }} {{ e.title }} (check-in)
    + {% if e.element_type == "check-in" %} +
    + {{ e.get_emoji() }} {{ e.title }} + (check-in from {{ display_time(e.when) }}) +
    + {% elif e.element_type == "check-out" %} +
    + {{ e.get_emoji() }} {{ e.title }} + (check-out by {{ display_time(e.when) }}) +
    {% else %}
    {{ e.get_emoji() }} From 34d7655acefc867d2491f23db8549233e8cf82de Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sat, 18 May 2024 18:02:14 +0200 Subject: [PATCH 189/449] Remove duplicate emoji --- agenda/types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agenda/types.py b/agenda/types.py index 4642566..e4bd7e4 100644 --- a/agenda/types.py +++ b/agenda/types.py @@ -224,7 +224,7 @@ class Trip: for item in self.travel: if item["type"] == "flight": name = ( - f"✈️ {airport_label(item['from_airport'])} → " + f"{airport_label(item['from_airport'])} → " + f"{airport_label(item['to_airport'])}" ) From 448d59514b070eee12f7771ab8cd9be02be5fc76 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sat, 18 May 2024 18:02:35 +0200 Subject: [PATCH 190/449] Use ndash instead of mdash --- templates/trip/list.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/trip/list.html b/templates/trip/list.html index 18fd6b9..a117384 100644 --- a/templates/trip/list.html +++ b/templates/trip/list.html @@ -167,7 +167,7 @@
    {{ e.get_emoji() }} {{ display_time(e.when) }} - — + – {{ e.title }}
    {% endif %} From d4dda44768c1ea12d38aad9c2a36146f748f1668 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sat, 18 May 2024 20:29:29 +0200 Subject: [PATCH 191/449] Add more country flags on the trip list. --- agenda/types.py | 4 +++- templates/trip/list.html | 13 +++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/agenda/types.py b/agenda/types.py index e4bd7e4..be5232f 100644 --- a/agenda/types.py +++ b/agenda/types.py @@ -59,7 +59,9 @@ class TripElement: def airport_label(airport: StrDict) -> str: """Airport label: name and iata.""" name = airport.get("alt_name") or airport["city"] - return f"{name} ({airport['iata']})" + country = agenda.get_country(airport["country"]) + assert country and country.flag + return f"{name} ({airport['iata']}) {country.flag}" @dataclass diff --git a/templates/trip/list.html b/templates/trip/list.html index a117384..07dad88 100644 --- a/templates/trip/list.html +++ b/templates/trip/list.html @@ -152,16 +152,13 @@ {% set date_heading = None %} {% for day, elements in trip.elements_grouped_by_day() %}

    {{ display_date_no_year(day) }}

    + {% set accommodation_label = {"check-in": "check-in from", "check-out": "check-out by"} %} {% for e in elements %} - {% if e.element_type == "check-in" %} + {% if e.element_type in accommodation_label %} + {% set c = get_country(e.detail.country) %}
    - {{ e.get_emoji() }} {{ e.title }} - (check-in from {{ display_time(e.when) }}) -
    - {% elif e.element_type == "check-out" %} -
    - {{ e.get_emoji() }} {{ e.title }} - (check-out by {{ display_time(e.when) }}) + {{ e.get_emoji() }} {{ e.title }} {{ c.flag }} + ({{ accommodation_label[e.element_type] }} {{ display_time(e.when) }})
    {% else %}
    From 277e991869ca1366760e8b737cceb15cc05742d2 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sat, 18 May 2024 20:37:10 +0200 Subject: [PATCH 192/449] Validate airport and station YAML --- validate_yaml.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/validate_yaml.py b/validate_yaml.py index 44b9925..d23fd0e 100755 --- a/validate_yaml.py +++ b/validate_yaml.py @@ -2,12 +2,15 @@ """Load YAML data to ensure validity.""" import os +import typing from datetime import date, timedelta +import agenda import agenda.conference import agenda.data import agenda.travel import agenda.trip +import agenda.types config = __import__("config.default", fromlist=[""]) @@ -35,3 +38,18 @@ next_year = today + timedelta(days=2 * 365) events = agenda.events_yaml.read(data_dir, last_year, next_year) print(len(events), "events") + +airports = typing.cast( + dict[str, agenda.types.StrDict], agenda.travel.parse_yaml("airports", data_dir) +) +print(len(airports), "airports") +for airport in airports.values(): + assert "country" in airport + assert agenda.get_country(airport["country"]) + + +stations = agenda.travel.parse_yaml("stations", data_dir) +print(len(stations), "stations") +for station in stations: + assert "country" in station + assert agenda.get_country(station["country"]) From 1948ab8ff53e03595f82678c92977b8ef71d2d8f Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sun, 19 May 2024 08:59:27 +0200 Subject: [PATCH 193/449] Rewrite TripElement.get_emoji() to use dict lookup --- agenda/types.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/agenda/types.py b/agenda/types.py index be5232f..04c3970 100644 --- a/agenda/types.py +++ b/agenda/types.py @@ -44,16 +44,17 @@ class TripElement: detail: StrDict def get_emoji(self) -> str | None: - """Emjoji for trip element.""" - if self.element_type in ("check-in", "check-out"): - return emoji.emojize(":hotel:", language="alias") - if self.element_type == "train": - return emoji.emojize(":train:", language="alias") - if self.element_type == "flight": - return emoji.emojize(":airplane:", language="alias") - if self.element_type == "ferry": - return emoji.emojize(":ferry:", language="alias") - return None + """Emoji for trip element.""" + emoji_map = { + "check-in": ":hotel:", + "check-out": ":hotel:", + "train": ":train:", + "flight": ":airplane:", + "ferry": ":ferry:", + } + + alias = emoji_map.get(self.element_type) + return emoji.emojize(alias, language="alias") if alias else None def airport_label(airport: StrDict) -> str: From 5758d3f1d0ec19c162b416bd6deba6ac9801a16b Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sun, 19 May 2024 13:33:04 +0200 Subject: [PATCH 194/449] Add more flags on trip list --- agenda/types.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/agenda/types.py b/agenda/types.py index 04c3970..1b4a27a 100644 --- a/agenda/types.py +++ b/agenda/types.py @@ -241,7 +241,13 @@ class Trip: ) if item["type"] == "train": for leg in item["legs"]: - name = f"{leg['from']} → {leg['to']}" + from_country = agenda.get_country(leg["from_station"]["country"]) + to_country = agenda.get_country(leg["to_station"]["country"]) + + assert from_country and to_country + from_flag = from_country.flag + to_flag = to_country.flag + name = f"{leg['from']} {from_flag} → {leg['to']} {to_flag}" elements.append( TripElement( when=leg["depart"], @@ -251,7 +257,14 @@ class Trip: ) ) if item["type"] == "ferry": - name = f"{item['from']} → {item['to']}" + from_country = agenda.get_country(item["from_terminal"]["country"]) + to_country = agenda.get_country(item["to_terminal"]["country"]) + + assert from_country and to_country + from_flag = from_country.flag + to_flag = to_country.flag + + name = f"{item['from']} {from_flag} → {item['to']} {to_flag}" elements.append( TripElement( when=item["depart"], From a96aefe22b10b267b610a5b2fc60660b668dc539 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Mon, 20 May 2024 18:32:49 +0200 Subject: [PATCH 195/449] Improve trip list template --- templates/trip/list.html | 71 +++++++++++++++++++++------------------- 1 file changed, 38 insertions(+), 33 deletions(-) diff --git a/templates/trip/list.html b/templates/trip/list.html index 07dad88..a0f3f48 100644 --- a/templates/trip/list.html +++ b/templates/trip/list.html @@ -116,40 +116,8 @@ {% endfor %} {% endif %} - {% for item in trip.conferences %} - {% set country = get_country(item.country) if item.country else None %} -
    -
    -
    - {{ item.name }} - - {{ display_date_no_year(item.start) }} to {{ display_date_no_year(item.end) }} - -
    -

    - Topic: {{ item.topic }} - | Venue: {{ item.venue }} - | Location: {{ item.location }} - {% if country %} - {{ country.flag }} - {% elif item.online %} - 💻 Online - {% else %} - - country code {{ item.country }} not found - - {% endif %} - {% if item.free %} - | free to attend - {% elif item.price and item.currency %} - | price: {{ item.price }} {{ item.currency }} - {% endif %} -

    -
    -
    - {% endfor %} + {{ conference_list(trip) }} - {% set date_heading = None %} {% for day, elements in trip.elements_grouped_by_day() %}

    {{ display_date_no_year(day) }}

    {% set accommodation_label = {"check-in": "check-in from", "check-out": "check-out by"} %} @@ -176,6 +144,43 @@ {% endif %} {% endmacro %} +{% macro conference_list(trip) %} + {% for item in trip.conferences %} + {% set country = get_country(item.country) if item.country else None %} +
    +
    +
    + {{ item.name }} + + {{ display_date_no_year(item.start) }} to {{ display_date_no_year(item.end) }} + +
    +

    + Topic: {{ item.topic }} + | Venue: {{ item.venue }} + | Location: {{ item.location }} + {% if country %} + {{ country.flag }} + {% elif item.online %} + 💻 Online + {% else %} + + country code {{ item.country }} not found + + {% endif %} + {% if item.free %} + | free to attend + {% elif item.price and item.currency %} + | price: {{ item.price }} {{ item.currency }} + {% endif %} +

    +
    +
    + {% endfor %} + + +{% endmacro %} + {% block content %}
    From 8181dfbe3b25a9493ece75a095d6ba8cd0f6a119 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Mon, 20 May 2024 18:33:53 +0200 Subject: [PATCH 196/449] Remove unused code from trip list HTML --- templates/trip/list.html | 97 ++++++++++++++-------------------------- 1 file changed, 34 insertions(+), 63 deletions(-) diff --git a/templates/trip/list.html b/templates/trip/list.html index a0f3f48..b08a3cb 100644 --- a/templates/trip/list.html +++ b/templates/trip/list.html @@ -8,71 +8,42 @@ -{% set conference_column_count = 8 %} -{% set accommodation_column_count = 8 %} -{% set travel_column_count = 10 %} {% endblock %} From 093000bbc35271c95aa631a97f3aa77b5e39deba Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Mon, 20 May 2024 20:56:14 +0200 Subject: [PATCH 197/449] Show more detail for flights --- templates/trip/list.html | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/templates/trip/list.html b/templates/trip/list.html index b08a3cb..a35ac5b 100644 --- a/templates/trip/list.html +++ b/templates/trip/list.html @@ -105,6 +105,12 @@ {{ display_time(e.when) }} – {{ e.title }} + {% if e.element_type == "flight" %} + airline: {{ e.detail.airline_name }} + flight number: {{ e.detail.airline }}{{ e.detail.flight_number }} + duration: {{ e.detail.duration }} + {#
    {{ e.detail | pprint }}
    #} + {% endif %}
    {% endif %} {% endfor %} From f8c523c674113ad1dd4e4764bb0323031d4f12d1 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Mon, 27 May 2024 10:12:58 +0200 Subject: [PATCH 198/449] Add format_distance macro --- templates/macros.html | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/templates/macros.html b/templates/macros.html index fec5da9..50e55cf 100644 --- a/templates/macros.html +++ b/templates/macros.html @@ -3,6 +3,10 @@ {% macro display_date(dt) %}{{ dt.strftime("%a %-d %b %Y") }}{% endmacro %} {% macro display_date_no_year(dt) %}{{ dt.strftime("%a %-d %b") }}{% endmacro %} +{% macro format_distance(distance) %} + {{ "{:,.0f} km / {:,.0f} miles".format(distance, distance / 1.60934) }} +{% endmacro %} + {% macro trip_link(trip) %} {{ trip.title }} {% endmacro %} From cd8dfb74a4d60b257fa86eaed1ff54fb65cc1057 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Mon, 27 May 2024 10:13:24 +0200 Subject: [PATCH 199/449] Use defaultdict, not Counter for travel distances --- agenda/types.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/agenda/types.py b/agenda/types.py index 1b4a27a..0e5bfc4 100644 --- a/agenda/types.py +++ b/agenda/types.py @@ -3,7 +3,7 @@ import collections import datetime import typing -from collections import Counter +from collections import defaultdict from dataclasses import dataclass, field import emoji @@ -190,12 +190,12 @@ class Trip: Any travel item with a missing or None 'distance' field is ignored. """ - transport_distances: Counter[float] = Counter() + transport_distances: defaultdict[str, float] = defaultdict(float) for item in self.travel: distance = item.get("distance") if distance: - transport_type = item.get("type", "unknown") + transport_type: str = item.get("type", "unknown") transport_distances[transport_type] += distance return list(transport_distances.items()) From 38f2e10c6df06fe12adf7456e68d4ca9cbb697b6 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Mon, 27 May 2024 10:16:03 +0200 Subject: [PATCH 200/449] Show distances for all past and future trips. --- templates/trip/list.html | 31 +++++++++++++++++++++++++------ web_view.py | 26 ++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/templates/trip/list.html b/templates/trip/list.html index a35ac5b..c39a516 100644 --- a/templates/trip/list.html +++ b/templates/trip/list.html @@ -1,6 +1,6 @@ {% extends "base.html" %} -{% from "macros.html" import trip_link, display_date_no_year, display_date, display_datetime, display_time with context %} +{% from "macros.html" import trip_link, display_date_no_year, display_date, display_datetime, display_time, format_distance with context %} {% block title %}{{ heading }} - Edward Betts{% endblock %} @@ -52,6 +52,18 @@ {% set items = item_list | list %}

    {{ heading }}

    {{ items | count }} trips

    + +
    Total distance: {{ format_distance(total_distance) }}
    + + {% for transport_type, distance in distances_by_transport_type %} +
    + {{ transport_type | title }} + distance: {{format_distance(distance) }} +
    + {% endfor %} + + + {% for trip in items %} {% set distances_by_transport_type = trip.distances_by_transport_type() %} {% set total_distance = trip.total_distance() %} @@ -74,15 +86,17 @@
    Start: {{ display_date_no_year(trip.start) }} (end date missing)
    {% endif %} {% if total_distance %} -
    Total distance: - {{ "{:,.0f} km / {:,.0f} miles".format(total_distance, total_distance / 1.60934) }} +
    + Total distance: + {{ format_distance(total_distance) }}
    {% endif %} {% if distances_by_transport_type %} {% for transport_type, distance in distances_by_transport_type %} -
    {{ transport_type | title }} distance: - {{ "{:,.0f} km / {:,.0f} miles".format(distance, distance / 1.60934) }} +
    + {{ transport_type | title }} + distance: {{format_distance(distance) }}
    {% endfor %} {% endif %} @@ -108,9 +122,14 @@ {% if e.element_type == "flight" %} airline: {{ e.detail.airline_name }} flight number: {{ e.detail.airline }}{{ e.detail.flight_number }} - duration: {{ e.detail.duration }} + {% if e.detail.duration %} + duration: {{ e.detail.duration }} + {% endif %} {#
    {{ e.detail | pprint }}
    #} {% endif %} + {% if e.detail.distance %} + distance: {{ format_distance(e.detail.distance) }} + {% endif %}
    {% endif %} {% endfor %} diff --git a/web_view.py b/web_view.py index 286d4ad..8c30bbd 100755 --- a/web_view.py +++ b/web_view.py @@ -8,6 +8,7 @@ import operator import os.path import sys import traceback +from collections import defaultdict from datetime import date, datetime, timedelta import flask @@ -286,6 +287,27 @@ def trip_list() -> werkzeug.Response: return flask.redirect(flask.url_for("trip_future_list")) +def calc_total_distance(trips: list[Trip]) -> float: + """Total distance for trips.""" + total = 0.0 + for item in trips: + dist = item.total_distance() + if dist: + total += dist + + return total + + +def sum_distances_by_transport_type(trips: list[Trip]) -> list[tuple[str, float]]: + """Sum distances by transport type.""" + distances_by_transport_type: defaultdict[str, float] = defaultdict(float) + for trip in trips: + for transport_type, dist in trip.distances_by_transport_type(): + distances_by_transport_type[transport_type] += dist + + return list(distances_by_transport_type.items()) + + @app.route("/trip/past") def trip_past_list() -> str: """Page showing a list of past trips.""" @@ -307,6 +329,8 @@ def trip_past_list() -> str: get_country=agenda.get_country, format_list_with_ampersand=format_list_with_ampersand, fx_rate=agenda.fx.get_rates(app.config), + total_distance=calc_total_distance(past), + distances_by_transport_type=sum_distances_by_transport_type(past), ) @@ -337,6 +361,8 @@ def trip_future_list() -> str: get_country=agenda.get_country, format_list_with_ampersand=format_list_with_ampersand, fx_rate=agenda.fx.get_rates(app.config), + total_distance=calc_total_distance(current + future), + distances_by_transport_type=sum_distances_by_transport_type(current + future), ) From 75242c2952900576b9da3e577e0e7f4eb5639cdd Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Mon, 27 May 2024 10:57:46 +0200 Subject: [PATCH 201/449] Show end times for travel --- agenda/types.py | 30 +++++++++++++++++++++--------- templates/macros.html | 4 +++- templates/trip/list.html | 10 +++++++--- 3 files changed, 31 insertions(+), 13 deletions(-) diff --git a/agenda/types.py b/agenda/types.py index 0e5bfc4..472db94 100644 --- a/agenda/types.py +++ b/agenda/types.py @@ -38,10 +38,13 @@ def as_datetime(d: DateOrDateTime) -> datetime.datetime: class TripElement: """Trip element.""" - when: DateOrDateTime + start_time: DateOrDateTime title: str element_type: str detail: StrDict + end_time: DateOrDateTime | None = None + start_loc: str | None = None + end_loc: str | None = None def get_emoji(self) -> str | None: """Emoji for trip element.""" @@ -207,7 +210,7 @@ class Trip: for item in self.accommodation: title = "Airbnb" if item.get("operator") == "airbnb" else item["name"] start = TripElement( - when=item["from"], + start_time=item["from"], title=title, detail=item, element_type="check-in", @@ -216,7 +219,7 @@ class Trip: elements.append(start) end = TripElement( - when=item["to"], + start_time=item["to"], title=title, detail=item, element_type="check-out", @@ -233,10 +236,13 @@ class Trip: elements.append( TripElement( - when=item["depart"], + start_time=item["depart"], + end_time=item.get("arrive"), title=name, detail=item, element_type="flight", + start_loc=airport_label(item["from_airport"]), + end_loc=airport_label(item["to_airport"]), ) ) if item["type"] == "train": @@ -250,10 +256,13 @@ class Trip: name = f"{leg['from']} {from_flag} → {leg['to']} {to_flag}" elements.append( TripElement( - when=leg["depart"], + start_time=leg["depart"], + end_time=leg["arrive"], title=name, detail=leg, element_type="train", + start_loc=f"{leg['from']} {from_flag}", + end_loc=f"{leg['to']} {to_flag}", ) ) if item["type"] == "ferry": @@ -267,14 +276,17 @@ class Trip: name = f"{item['from']} {from_flag} → {item['to']} {to_flag}" elements.append( TripElement( - when=item["depart"], + start_time=item["depart"], + end_time=item["arrive"], title=name, detail=item, element_type="ferry", + start_loc=f"{item['from']} {from_flag}", + end_loc=f"{item['to']} {to_flag}", ) ) - return sorted(elements, key=lambda e: as_datetime(e.when)) + return sorted(elements, key=lambda e: as_datetime(e.start_time)) def elements_grouped_by_day(self) -> list[tuple[datetime.date, list[TripElement]]]: """Group trip elements by day.""" @@ -285,7 +297,7 @@ class Trip: for element in self.elements(): # Extract the date part of the 'when' attribute - day = as_date(element.when) + day = as_date(element.start_time) grouped_elements[day].append(element) # Sort elements within each day @@ -294,7 +306,7 @@ class Trip: key=lambda e: ( e.element_type == "check-in", # check-out elements last e.element_type != "check-out", # check-in elements first - as_datetime(e.when), # then sort by time + as_datetime(e.start_time), # then sort by time ) ) diff --git a/templates/macros.html b/templates/macros.html index 50e55cf..ae77489 100644 --- a/templates/macros.html +++ b/templates/macros.html @@ -1,5 +1,7 @@ {% macro display_datetime(dt) %}{{ dt.strftime("%a, %d, %b %Y %H:%M %z") }}{% endmacro %} -{% macro display_time(dt) %}{{ dt.strftime("%H:%M %z") }}{% endmacro %} +{% macro display_time(dt) %} + {% if dt %}{{ dt.strftime("%H:%M %z") }}{% endif %} +{% endmacro %} {% macro display_date(dt) %}{{ dt.strftime("%a %-d %b %Y") }}{% endmacro %} {% macro display_date_no_year(dt) %}{{ dt.strftime("%a %-d %b") }}{% endmacro %} diff --git a/templates/trip/list.html b/templates/trip/list.html index c39a516..346f186 100644 --- a/templates/trip/list.html +++ b/templates/trip/list.html @@ -111,14 +111,18 @@ {% set c = get_country(e.detail.country) %}
    {{ e.get_emoji() }} {{ e.title }} {{ c.flag }} - ({{ accommodation_label[e.element_type] }} {{ display_time(e.when) }}) + ({{ accommodation_label[e.element_type] }} {{ display_time(e.start_time) }})
    {% else %}
    {{ e.get_emoji() }} - {{ display_time(e.when) }} + {{ display_time(e.start_time) }} – - {{ e.title }} + {{ e.start_loc }} + → + {{ display_time(e.end_time) }} + – + {{ e.end_loc }} {% if e.element_type == "flight" %} airline: {{ e.detail.airline_name }} flight number: {{ e.detail.airline }}{{ e.detail.flight_number }} From 537a84ff67d5203f18ceb4831bca2adc6e0ada9b Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Mon, 27 May 2024 11:10:53 +0200 Subject: [PATCH 202/449] Move flag display to trip list template --- agenda/types.py | 33 ++++++++++++++++++--------------- templates/trip/list.html | 4 ++-- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/agenda/types.py b/agenda/types.py index 472db94..2b759b6 100644 --- a/agenda/types.py +++ b/agenda/types.py @@ -45,6 +45,8 @@ class TripElement: end_time: DateOrDateTime | None = None start_loc: str | None = None end_loc: str | None = None + start_country: Country | None = None + end_country: Country | None = None def get_emoji(self) -> str | None: """Emoji for trip element.""" @@ -63,9 +65,7 @@ class TripElement: def airport_label(airport: StrDict) -> str: """Airport label: name and iata.""" name = airport.get("alt_name") or airport["city"] - country = agenda.get_country(airport["country"]) - assert country and country.flag - return f"{name} ({airport['iata']}) {country.flag}" + return f"{name} ({airport['iata']})" @dataclass @@ -234,6 +234,9 @@ class Trip: + f"{airport_label(item['to_airport'])}" ) + from_country = agenda.get_country(item["from_airport"]["country"]) + to_country = agenda.get_country(item["to_airport"]["country"]) + elements.append( TripElement( start_time=item["depart"], @@ -243,6 +246,8 @@ class Trip: element_type="flight", start_loc=airport_label(item["from_airport"]), end_loc=airport_label(item["to_airport"]), + start_country=from_country, + end_country=to_country, ) ) if item["type"] == "train": @@ -251,9 +256,7 @@ class Trip: to_country = agenda.get_country(leg["to_station"]["country"]) assert from_country and to_country - from_flag = from_country.flag - to_flag = to_country.flag - name = f"{leg['from']} {from_flag} → {leg['to']} {to_flag}" + name = f"{leg['from']} → {leg['to']}" elements.append( TripElement( start_time=leg["depart"], @@ -261,19 +264,17 @@ class Trip: title=name, detail=leg, element_type="train", - start_loc=f"{leg['from']} {from_flag}", - end_loc=f"{leg['to']} {to_flag}", + start_loc=leg["from"], + end_loc=leg["to"], + start_country=from_country, + end_country=to_country, ) ) if item["type"] == "ferry": from_country = agenda.get_country(item["from_terminal"]["country"]) to_country = agenda.get_country(item["to_terminal"]["country"]) - assert from_country and to_country - from_flag = from_country.flag - to_flag = to_country.flag - - name = f"{item['from']} {from_flag} → {item['to']} {to_flag}" + name = f"{item['from']} → {item['to']}" elements.append( TripElement( start_time=item["depart"], @@ -281,8 +282,10 @@ class Trip: title=name, detail=item, element_type="ferry", - start_loc=f"{item['from']} {from_flag}", - end_loc=f"{item['to']} {to_flag}", + start_loc=item["from"], + end_loc=item["to"], + start_country=from_country, + end_country=to_country, ) ) diff --git a/templates/trip/list.html b/templates/trip/list.html index 346f186..eb0900b 100644 --- a/templates/trip/list.html +++ b/templates/trip/list.html @@ -118,11 +118,11 @@ {{ e.get_emoji() }} {{ display_time(e.start_time) }} – - {{ e.start_loc }} + {{ e.start_loc }} {{ e.start_country.flag }} → {{ display_time(e.end_time) }} – - {{ e.end_loc }} + {{ e.end_loc }} {{ e.end_country.flag }} {% if e.element_type == "flight" %} airline: {{ e.detail.airline_name }} flight number: {{ e.detail.airline }}{{ e.detail.flight_number }} From ade69893004304c02feb5a2ca0f140e09ceb5d5f Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sun, 2 Jun 2024 19:01:18 +0100 Subject: [PATCH 203/449] Show links to flight data web sites --- templates/trip/list.html | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/templates/trip/list.html b/templates/trip/list.html index eb0900b..f7a0580 100644 --- a/templates/trip/list.html +++ b/templates/trip/list.html @@ -124,6 +124,8 @@ – {{ e.end_loc }} {{ e.end_country.flag }} {% if e.element_type == "flight" %} + {% set full_flight_number = e.detail.airline + e.detail.flight_number %} + {% set radarbox_url = "https://www.radarbox.com/data/flights/" + full_flight_number %} airline: {{ e.detail.airline_name }} flight number: {{ e.detail.airline }}{{ e.detail.flight_number }} {% if e.detail.duration %} @@ -134,6 +136,11 @@ {% if e.detail.distance %} distance: {{ format_distance(e.detail.distance) }} {% endif %} + {% if e.element_type == "flight" %} + flightradar24 + | FlightAware + | radarbox + {% endif %}
    {% endif %} {% endfor %} From 5de5e2288318a270fbb84788e56ca9394a7760bc Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sun, 2 Jun 2024 19:04:37 +0100 Subject: [PATCH 204/449] Fix docstrings --- agenda/accommodation.py | 4 ++-- agenda/carnival.py | 2 +- agenda/domains.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/agenda/accommodation.py b/agenda/accommodation.py index f5b7e83..59eb111 100644 --- a/agenda/accommodation.py +++ b/agenda/accommodation.py @@ -1,4 +1,4 @@ -"""Accomodation""" +"""Accommodation.""" import yaml @@ -6,7 +6,7 @@ from .types import Event def get_events(filepath: str) -> list[Event]: - """Get accomodation from YAML.""" + """Get accommodation from YAML.""" with open(filepath) as f: return [ Event( diff --git a/agenda/carnival.py b/agenda/carnival.py index 41a995b..c6f429d 100644 --- a/agenda/carnival.py +++ b/agenda/carnival.py @@ -1,4 +1,4 @@ -"""Calcuate the date for carnival.""" +"""Calculate the date for carnival.""" from datetime import date, timedelta diff --git a/agenda/domains.py b/agenda/domains.py index 99c614e..89aae32 100644 --- a/agenda/domains.py +++ b/agenda/domains.py @@ -1,4 +1,4 @@ -"""Accomodation.""" +"""Domain renewal dates.""" import csv import os From 733608bc2f26b5c4f22cea13c5e1ef467b57fef6 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sun, 2 Jun 2024 19:05:04 +0100 Subject: [PATCH 205/449] Fix spelling --- agenda/data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agenda/data.py b/agenda/data.py index a18d74d..d640557 100644 --- a/agenda/data.py +++ b/agenda/data.py @@ -42,7 +42,7 @@ here = dateutil.tz.tzlocal() # deadline to file tax return # credit card expiry dates # morzine ski lifts -# chalet availablity calendar +# chalet availability calendar # starlink visible From 4c651198f34777f930b9f363998cb31a5717f20a Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Mon, 3 Jun 2024 19:30:14 +0100 Subject: [PATCH 206/449] Add birthday list page --- templates/birthday_list.html | 17 +++++++++++++++++ templates/navbar.html | 4 +++- web_view.py | 13 +++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 templates/birthday_list.html diff --git a/templates/birthday_list.html b/templates/birthday_list.html new file mode 100644 index 0000000..7346bae --- /dev/null +++ b/templates/birthday_list.html @@ -0,0 +1,17 @@ +{% extends "base.html" %} +{% from "macros.html" import display_date %} +{% block title %}Birthdays - Edward Betts{% endblock %} + +{% block content %} +
    +

    Birthdays

    + + {% for event in items %} + + + + + {% endfor %} +
    {{event.as_date.strftime("%a, %d, %b")}}{{ event.title }}
    +
    +{% endblock %} diff --git a/templates/navbar.html b/templates/navbar.html index 04fa3f9..42626fd 100644 --- a/templates/navbar.html +++ b/templates/navbar.html @@ -11,7 +11,9 @@ {"endpoint": "weekends", "label": "Weekend" }, {"endpoint": "launch_list", "label": "Space launch" }, {"endpoint": "holiday_list", "label": "Holiday" }, -] %} + ] + ([{"endpoint": "birthday_list", "label": "Birthdays" }] + if g.user.is_authenticated else []) + %}