agenda/web_view.py

324 lines
9.1 KiB
Python
Raw Normal View History

2023-10-02 20:35:30 +01:00
#!/usr/bin/python3
"""Web page to show upcoming events."""
2023-10-06 23:29:22 +01:00
import inspect
2023-12-04 21:53:33 +00:00
import operator
2023-11-20 19:48:42 +00:00
import os.path
2023-10-06 23:29:22 +01:00
import sys
import traceback
2023-11-20 19:48:42 +00:00
from datetime import date, datetime
2023-10-02 20:35:30 +01:00
2023-10-06 23:29:22 +01:00
import flask
import werkzeug
import werkzeug.debug.tbtools
2023-11-20 19:48:42 +00:00
import yaml
2023-10-02 20:35:30 +01:00
2023-11-07 15:55:05 +00:00
import agenda.data
2023-12-07 17:15:58 +00:00
import agenda.error_mail
import agenda.thespacedevs
from agenda import format_list_with_ampersand, travel
from agenda.types import StrDict, Trip
2023-10-02 20:35:30 +01:00
2023-10-06 23:29:22 +01:00
app = flask.Flask(__name__)
2023-11-07 15:55:05 +00:00
app.debug = False
app.config.from_object("config.default")
2023-10-02 20:35:30 +01:00
2023-12-07 17:15:58 +00:00
agenda.error_mail.setup_error_mail(app)
2023-10-02 20:35:30 +01:00
2023-10-06 23:29:22 +01:00
@app.errorhandler(werkzeug.exceptions.InternalServerError)
def exception_handler(e: werkzeug.exceptions.InternalServerError) -> tuple[str, int]:
"""Handle exception."""
exec_type, exc_value, current_traceback = sys.exc_info()
assert exc_value
tb = werkzeug.debug.tbtools.DebugTraceback(exc_value)
summary = tb.render_traceback_html(include_title=False)
exc_lines = "".join(tb._te.format_exception_only())
last_frame = list(traceback.walk_tb(current_traceback))[-1][0]
last_frame_args = inspect.getargs(last_frame.f_code)
return (
flask.render_template(
"show_error.html",
plaintext=tb.render_traceback_text(),
exception=exc_lines,
exception_type=tb._te.exc_type.__name__,
summary=summary,
last_frame=last_frame,
last_frame_args=last_frame_args,
),
500,
)
2023-10-02 20:35:30 +01:00
@app.route("/")
async def index() -> str:
2023-10-02 20:35:30 +01:00
"""Index page."""
now = datetime.now()
data = await agenda.data.get_data(now, app.config)
2023-10-02 20:35:30 +01:00
2023-10-21 22:58:44 +01:00
return flask.render_template("index.html", today=now.date(), **data)
2023-10-02 20:35:30 +01:00
@app.route("/launches")
async 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)
return flask.render_template("launches.html", rockets=rockets, now=now)
2023-12-27 08:22:53 +00:00
@app.route("/gaps")
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"])
@app.route("/travel")
2023-11-20 19:48:42 +00:00
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)
2023-11-20 19:48:42 +00:00
return flask.render_template("travel.html", flights=flights, trains=trains)
def as_date(d: date | datetime) -> date:
"""Date of event."""
return d.date() if isinstance(d, datetime) else d
@app.route("/conference")
def conference_list() -> str:
"""Page showing a list of conferences."""
data_dir = app.config["PERSONAL_DATA"]
2023-11-20 19:48:42 +00:00
filepath = os.path.join(data_dir, "conferences.yaml")
2024-01-04 15:08:12 +00:00
item_list = yaml.safe_load(open(filepath))
2023-12-04 21:53:33 +00:00
today = date.today()
for conf in item_list:
conf["start_date"] = as_date(conf["start"])
conf["end_date"] = as_date(conf["end"])
2023-11-20 19:48:42 +00:00
item_list.sort(key=operator.itemgetter("start_date"))
2023-11-20 19:48:42 +00:00
current = [
conf
for conf in item_list
if conf["start_date"] <= today and conf["end_date"] >= today
]
past = [conf for conf in item_list if conf["end_date"] < today]
future = [conf for conf in item_list if conf["start_date"] > today]
2023-12-04 21:53:33 +00:00
return flask.render_template(
"conference_list.html",
current=current,
past=past,
future=future,
today=today,
get_country=agenda.get_country,
2023-12-04 21:53:33 +00:00
)
2024-01-01 21:26:39 +00:00
@app.route("/accommodation")
def accommodation_list() -> str:
"""Page showing a list of past, present and future accommodation."""
data_dir = app.config["PERSONAL_DATA"]
items = travel.parse_yaml("accommodation", data_dir)
2024-01-01 21:26:39 +00:00
stays_in_2024 = [item for item in items if item["from"].year == 2024]
total_nights_2024 = sum(
(stay["to"].date() - stay["from"].date()).days for stay in stays_in_2024
)
nights_abroad_2024 = sum(
(stay["to"].date() - stay["from"].date()).days
for stay in stays_in_2024
if stay["country"] != "gb"
)
return flask.render_template(
"accommodation.html",
items=items,
total_nights_2024=total_nights_2024,
nights_abroad_2024=nights_abroad_2024,
get_country=agenda.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
2024-01-12 16:20:36 +00:00
def load_trains() -> list[StrDict]:
"""Load trains."""
data_dir = app.config["PERSONAL_DATA"]
2024-01-12 16:20:36 +00:00
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"]]
2024-01-12 16:20:36 +00:00
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(
2024-01-12 16:20:36 +00:00
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),
2024-01-10 13:27:25 +00:00
"events": travel.parse_yaml("events", data_dir),
}
for key, item_list in data.items():
assert isinstance(item_list, list)
for item in item_list:
2024-01-05 09:35:56 +00:00
if not (start := item.get("trip")):
continue
2024-01-05 09:35:56 +00:00
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()
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",
current=current,
past=past,
future=future,
today=today,
get_country=agenda.get_country,
format_list_with_ampersand=format_list_with_ampersand,
2024-01-01 21:26:39 +00:00
)
def collect_trip_coordinates(trip: Trip) -> list[StrDict]:
"""Extract and deduplicate airport and station coordinates from trip."""
2024-01-12 15:04:08 +00:00
stations = {}
station_list = []
2024-01-12 16:20:36 +00:00
airports = {}
2024-01-12 15:04:08 +00:00
for t in trip.travel:
2024-01-12 16:20:36 +00:00
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]
2024-01-12 15:04:08 +00:00
for s in station_list:
if s["uic"] in stations:
continue
stations[s["uic"]] = s
2024-01-12 16:20:36 +00:00
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()
2024-01-12 16:20:36 +00:00
]
2024-01-12 15:04:08 +00:00
@app.route("/trip/<start>")
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)
coordinates = collect_trip_coordinates(trip)
2024-01-12 15:04:08 +00:00
return flask.render_template(
"trip_page.html",
trip=trip,
today=today,
coordinates=coordinates,
get_country=agenda.get_country,
format_list_with_ampersand=format_list_with_ampersand,
)
2023-10-02 20:35:30 +01:00
if __name__ == "__main__":
app.run(host="0.0.0.0")