Compare commits
No commits in common. "3dddc52430a27f20eb8d8fc885f32546946a5422" and "d9b1d77872d2bf08039969440f9ed32cfadf3e5b" have entirely different histories.
3dddc52430
...
d9b1d77872
|
@ -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
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
||||||
|
|
|
@ -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 section(heading, item_list, badge) %}
|
{% macro row(item, badge) %}
|
||||||
{% if item_list %}
|
{% set country = get_country(item.country) if item.country else None %}
|
||||||
<div class="heading"><h2>{{ heading }}</h2></div>
|
<div class="grid-item text-end">{{ item.start.strftime("%a, %d %b %Y") }}</div>
|
||||||
{% for item in item_list %}{{ conference_row(item, badge) }}{% endfor %}
|
<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 %}
|
{% 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) %}
|
||||||
|
{% if item_list %}
|
||||||
|
<div class="heading"><h2>{{heading}}</h2></div>
|
||||||
|
{% for item in item_list %}{{ row(item, badge) }}{% endfor %}
|
||||||
|
{% 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") }}
|
||||||
|
|
|
@ -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" },
|
||||||
|
|
|
@ -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 }} → {{ 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 }} → {{ 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>
|
||||||
|
|
||||||
|
|
67
web_view.py
67
web_view.py
|
@ -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,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue