Compare commits

..

No commits in common. "3dddc52430a27f20eb8d8fc885f32546946a5422" and "d9b1d77872d2bf08039969440f9ed32cfadf3e5b" have entirely different histories.

9 changed files with 120 additions and 138 deletions

View file

@ -2,7 +2,6 @@
from datetime import date, datetime, time from datetime import date, datetime, time
import pycountry
import pytz import pytz
uk_tz = pytz.timezone("Europe/London") uk_tz = pytz.timezone("Europe/London")
@ -11,23 +10,3 @@ uk_tz = pytz.timezone("Europe/London")
def uk_time(d: date, t: time) -> datetime: def uk_time(d: date, t: time) -> datetime:
"""Combine time and date for UK timezone.""" """Combine time and date for UK timezone."""
return uk_tz.localize(datetime.combine(d, t)) return uk_tz.localize(datetime.combine(d, t))
def format_list_with_ampersand(items: list[str]) -> str:
"""Join a list of strings with commas and an ampersand."""
if len(items) > 1:
return ", ".join(items[:-1]) + " & " + items[-1]
elif items:
return items[0]
return ""
def get_country(alpha_2: str) -> pycountry.db.Country | None:
"""Lookup country by alpha-2 country code."""
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())
return country

View file

@ -18,7 +18,6 @@ class Conference:
location: str location: str
start: date | datetime start: date | datetime
end: date | datetime end: date | datetime
trip: date | None = None
country: str | None = None country: str | None = None
venue: str | None = None venue: str | None = None
address: str | None = None address: str | None = None

View file

@ -36,7 +36,9 @@ from . import (
uk_tz, uk_tz,
waste_schedule, waste_schedule,
) )
from .types import Event, Holiday, StrDict from .types import Event, Holiday
StrDict = dict[str, typing.Any]
here = dateutil.tz.tzlocal() here = dateutil.tz.tzlocal()

View file

@ -2,53 +2,6 @@
import dataclasses import dataclasses
import datetime import datetime
import typing
from pycountry.db import Country
import agenda
from agenda import format_list_with_ampersand
StrDict = dict[str, typing.Any]
@dataclasses.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)
@property
def title(self) -> str:
"""Trip title."""
names = (
format_list_with_ampersand([conf["name"] for conf in self.conferences])
or "[no conferences in trip]"
)
return f'{names} ({self.date.strftime("%b %Y")})'
@property
def countries(self) -> set[Country]:
"""Trip countries."""
found: set[Country] = set()
for item in self.conferences + self.accommodation:
if "country" not in item:
continue
country = agenda.get_country(item["country"])
assert country
found.add(country)
return found
@property
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]
)
@dataclasses.dataclass @dataclasses.dataclass

View file

@ -1,5 +1,4 @@
{% extends "base.html" %} {% extends "base.html" %}
{% from "macros.html" import accommodation_row with context %}
{% block style %} {% block style %}
{% set column_count = 7 %} {% set column_count = 7 %}
<style> <style>
@ -20,10 +19,36 @@
</style> </style>
{% endblock %} {% endblock %}
{% macro row(item, badge) %}
{% set country = get_country(item.country) %}
{% set nights = (item.to.date() - item.from.date()).days %}
<div class="grid-item text-end">{{ item.from.strftime("%a, %d %b %Y") }}</div>
<div class="grid-item text-end">{{ item.to.strftime("%a, %d %b") }}</div>
<div class="grid-item text-end">{% if nights == 1 %}1 night{% else %}{{ nights }} nights{% endif %}</div>
<div class="grid-item">{{ item.operator }}</div>
<div class="grid-item">{{ item.location }}</div>
<div class="grid-item">
{% if country %}
{{ country.flag }} {{ country.name }}
{% else %}
<span class="text-bg-danger p-2">
country code <strong>{{ item.country }}</strong> not found
</span>
{% endif %}
</div>
<div class="grid-item">
{% if item.url %}
<a href="{{ item.url }}">{{ item.name }}</a>
{% else %}
{{ item.name }}
{% endif %}
</div>
{% endmacro %}
{% macro section(heading, item_list, badge) %} {% macro section(heading, item_list, badge) %}
{% if item_list %} {% if item_list %}
<div class="heading"><h2>{{heading}}</h2></div> <div class="heading"><h2>{{heading}}</h2></div>
{% for item in item_list %}{{ accommodation_row(item, badge) }}{% endfor %} {% for item in item_list %}{{ row(item, badge) }}{% endfor %}
{% endif %} {% endif %}
{% endmacro %} {% endmacro %}

View file

@ -1,7 +1,5 @@
{% extends "base.html" %} {% extends "base.html" %}
{% from "macros.html" import conference_row with context %}
{% block style %} {% block style %}
{% set column_count = 6 %} {% set column_count = 6 %}
<style> <style>
@ -22,17 +20,56 @@
</style> </style>
{% endblock %} {% endblock %}
{% macro row(item, badge) %}
{% set country = get_country(item.country) if item.country else None %}
<div class="grid-item text-end">{{ item.start.strftime("%a, %d %b %Y") }}</div>
<div class="grid-item text-end">{{ item.end.strftime("%a, %d %b") }}</div>
<div class="grid-item">
{% if item.url %}
<a href="{{ item.url }}">{{ item.name }}</a>
{% else %}
{{ item.name }}
{% endif %}
{% if item.going and not (item.accommodation_booked or item.travel_booked) %}
<span class="badge text-bg-primary">
{{ badge }}
</span>
{% endif %}
{% if item.accommodation_booked %}
<span class="badge text-bg-success">accommodation</span>
{% endif %}
{% if item.transport_booked %}
<span class="badge text-bg-success">transport</span>
{% endif %}
</div>
<div class="grid-item">{{ item.topic }}</div>
<div class="grid-item">{{ item.location }}</div>
<div class="grid-item">
{% if country %}
{{ country.flag }} {{ country.name }}
{% elif item.online %}
💻 Online
{% else %}
<span class="text-bg-danger p-2">
country code <strong>{{ item.country }}</strong> not found
</span>
{% endif %}
</div>
{% endmacro %}
{% macro section(heading, item_list, badge) %} {% macro section(heading, item_list, badge) %}
{% if item_list %} {% if item_list %}
<div class="heading"><h2>{{heading}}</h2></div> <div class="heading"><h2>{{heading}}</h2></div>
{% for item in item_list %}{{ conference_row(item, badge) }}{% endfor %} {% for item in item_list %}{{ row(item, badge) }}{% endfor %}
{% endif %} {% endif %}
{% endmacro %} {% endmacro %}
{% block content %} {% block content %}
<div class="container-fluid mt-2"> <div class="container-fluid mt-2">
<h1>Conferences</h1> <h1>Conferences</h1>
<div class="grid-container"> <div class="grid-container">
{{ section("Current", current, "attending") }} {{ section("Current", current, "attending") }}
{{ section("Future", future, "going") }} {{ section("Future", future, "going") }}

View file

@ -2,7 +2,6 @@
{% set pages = [ {% set pages = [
{"endpoint": "index", "label": "Home" }, {"endpoint": "index", "label": "Home" },
{"endpoint": "trip_list", "label": "Trips" },
{"endpoint": "conference_list", "label": "Conference" }, {"endpoint": "conference_list", "label": "Conference" },
{"endpoint": "travel_list", "label": "Travel" }, {"endpoint": "travel_list", "label": "Travel" },
{"endpoint": "accommodation_list", "label": "Accommodation" }, {"endpoint": "accommodation_list", "label": "Accommodation" },

View file

@ -1,5 +1,10 @@
{% extends "base.html" %} {% extends "base.html" %}
{% from "macros.html" import flight_row, train_row with context %}
{% 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 %}
{% block style %} {% block style %}
<style> <style>
@ -12,7 +17,7 @@
.train-grid-container { .train-grid-container {
display: grid; display: grid;
grid-template-columns: repeat(7, auto); /* 7 columns for each piece of information */ grid-template-columns: repeat(6, auto); /* 7 columns for each piece of information */
gap: 10px; gap: 10px;
justify-content: start; justify-content: start;
} }
@ -40,8 +45,19 @@
<div class="grid-item">flight</div> <div class="grid-item">flight</div>
<div class="grid-item">reference</div> <div class="grid-item">reference</div>
{% for item in flights | sort(attribute="depart") %} {% for item in flights | sort(attribute="depart") if item.arrive %}
{{ flight_row(item) }} <div class="grid-item text-end">{{ item.depart.strftime("%a, %d %b %Y") }}</div>
<div class="grid-item">{{ item.from }} &rarr; {{ item.to }}</div>
<div class="grid-item">{{ item.depart.strftime("%H:%M") }}</div>
<div class="grid-item">
{% if item.arrive %}
{{ item.arrive.strftime("%H:%M") }}
{% if item.arrive.date() != item.depart.date() %}+1 day{% endif %}
{% endif %}
</div>
<div class="grid-item">{{ item.duration }}</div>
<div class="grid-item">{{ item.airline }}{{ item.flight_number }}</div>
<div class="grid-item">{{ item.booking_reference }}</div>
{% endfor %} {% endfor %}
</div> </div>
@ -52,12 +68,21 @@
<div class="grid-item">route</div> <div class="grid-item">route</div>
<div class="grid-item">depart</div> <div class="grid-item">depart</div>
<div class="grid-item">arrive</div> <div class="grid-item">arrive</div>
<div class="grid-item">duration</div>
<div class="grid-item">operator</div> <div class="grid-item">operator</div>
<div class="grid-item">reference</div> <div class="grid-item">reference</div>
{% for item in trains | sort(attribute="depart") %} {% for item in trains | sort(attribute="depart") if item.arrive %}
{{ train_row(item) }} <div class="grid-item text-end">{{ item.depart.strftime("%a, %d %b %Y") }}</div>
<div class="grid-item">{{ item.from }} &rarr; {{ item.to }}</div>
<div class="grid-item">{{ item.depart.strftime("%H:%M") }}</div>
<div class="grid-item">
{% if item.arrive %}
{{ item.arrive.strftime("%H:%M") }}
{% if item.arrive.date() != item.depart.date() %}+1 day{% endif %}
{% endif %}
</div>
<div class="grid-item">{{ item.operator }}</div>
<div class="grid-item">{{ item.booking_reference }}</div>
{% endfor %} {% endfor %}
</div> </div>

View file

@ -7,9 +7,11 @@ import operator
import os.path import os.path
import sys import sys
import traceback import traceback
import typing
from datetime import date, datetime from datetime import date, datetime
import flask import flask
import pycountry
import werkzeug import werkzeug
import werkzeug.debug.tbtools import werkzeug.debug.tbtools
import yaml import yaml
@ -17,8 +19,7 @@ import yaml
import agenda.data import agenda.data
import agenda.error_mail import agenda.error_mail
import agenda.thespacedevs import agenda.thespacedevs
from agenda import format_list_with_ampersand, travel import agenda.travel
from agenda.types import StrDict, Trip
app = flask.Flask(__name__) app = flask.Flask(__name__)
app.debug = False app.debug = False
@ -86,8 +87,8 @@ async def gaps_page() -> str:
def travel_list() -> str: def travel_list() -> str:
"""Page showing a list of upcoming travel.""" """Page showing a list of upcoming travel."""
data_dir = app.config["PERSONAL_DATA"] data_dir = app.config["PERSONAL_DATA"]
flights = travel.parse_yaml("flights", data_dir) flights = agenda.travel.parse_yaml("flights", data_dir)
trains = travel.parse_yaml("trains", data_dir) trains = agenda.travel.parse_yaml("trains", data_dir)
return flask.render_template("travel.html", flights=flights, trains=trains) return flask.render_template("travel.html", flights=flights, trains=trains)
@ -97,6 +98,13 @@ def as_date(d: date | datetime) -> date:
return d.date() if isinstance(d, datetime) else d 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") @app.route("/conference")
def conference_list() -> str: def conference_list() -> str:
"""Page showing a list of conferences.""" """Page showing a list of conferences."""
@ -125,7 +133,7 @@ def conference_list() -> str:
past=past, past=past,
future=future, future=future,
today=today, today=today,
get_country=agenda.get_country, get_country=get_country,
) )
@ -133,7 +141,7 @@ def conference_list() -> str:
def accommodation_list() -> str: def accommodation_list() -> str:
"""Page showing a list of past, present and future accommodation.""" """Page showing a list of past, present and future accommodation."""
data_dir = app.config["PERSONAL_DATA"] data_dir = app.config["PERSONAL_DATA"]
items = travel.parse_yaml("accommodation", data_dir) items = agenda.travel.parse_yaml("accommodation", data_dir)
stays_in_2024 = [item for item in items if item["from"].year == 2024] stays_in_2024 = [item for item in items if item["from"].year == 2024]
total_nights_2024 = sum( total_nights_2024 = sum(
@ -151,52 +159,7 @@ def accommodation_list() -> str:
items=items, items=items,
total_nights_2024=total_nights_2024, total_nights_2024=total_nights_2024,
nights_abroad_2024=nights_abroad_2024, nights_abroad_2024=nights_abroad_2024,
get_country=agenda.get_country, get_country=get_country,
)
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
@app.route("/trip")
def trip_list() -> str:
"""Page showing a list of trips."""
trips: dict[date, Trip] = {}
data_dir = app.config["PERSONAL_DATA"]
travel_items = sorted(
load_travel("flight") + load_travel("train"), key=operator.itemgetter("depart")
)
data = {
"travel": travel_items,
"accommodation": travel.parse_yaml("accommodation", data_dir),
"conferences": travel.parse_yaml("conferences", data_dir),
}
for key, item_list in data.items():
assert isinstance(item_list, list)
for item in item_list:
if not (trip_id := item.get("trip")):
continue
if trip_id not in trips:
trips[trip_id] = Trip(date=trip_id)
getattr(trips[trip_id], key).append(item)
trip_list = [trip for _, trip in sorted(trips.items(), reverse=True)]
return flask.render_template(
"trips.html",
trips=trip_list,
get_country=agenda.get_country,
format_list_with_ampersand=format_list_with_ampersand,
) )