agenda/web_view.py

395 lines
11 KiB
Python
Executable file

#!/usr/bin/python3
"""Web page to show upcoming events."""
import inspect
import operator
import os.path
import sys
import traceback
import typing
from datetime import date, datetime, timedelta
import flask
import UniAuth.auth
import werkzeug
import werkzeug.debug.tbtools
import yaml
import agenda.data
import agenda.error_mail
import agenda.holidays
import agenda.thespacedevs
import agenda.trip
from agenda import format_list_with_ampersand, travel
from agenda.types import StrDict
app = flask.Flask(__name__)
app.debug = False
app.config.from_object("config.default")
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."""
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,
)
@app.route("/")
async def index() -> str:
"""Index page."""
now = datetime.now()
data = await agenda.data.get_data(now, app.config)
return flask.render_template("index.html", today=now.date(), **data)
@app.route("/launches")
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 = agenda.thespacedevs.get_launches(rocket_dir, limit=100)
return flask.render_template(
"launches.html", rockets=rockets, now=now, get_country=agenda.get_country
)
@app.route("/gaps")
async def gaps_page() -> 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)
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."""
data_dir = app.config["PERSONAL_DATA"]
flights = travel.parse_yaml("flights", 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)
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"]
filepath = os.path.join(data_dir, "conferences.yaml")
item_list = yaml.safe_load(open(filepath))
today = date.today()
conference_trip_lookup = {}
for trip in agenda.trip.build_trip_list():
for trip_conf in trip.conferences:
key = (trip_conf["start"], trip_conf["name"])
conference_trip_lookup[key] = trip
for conf in item_list:
conf["start_date"] = as_date(conf["start"])
conf["end_date"] = as_date(conf["end"])
key = (conf["start"], conf["name"])
if this_trip := conference_trip_lookup.get(key):
conf["linked_trip"] = this_trip
item_list.sort(key=operator.itemgetter("start_date"))
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]
return flask.render_template(
"conference_list.html",
current=current,
past=past,
future=future,
today=today,
get_country=agenda.get_country,
)
@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)
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"
)
trip_lookup = {}
for trip in agenda.trip.build_trip_list():
for trip_stay in trip.accommodation:
key = (trip_stay["from"], trip_stay["name"])
trip_lookup[key] = trip
for item in items:
key = (item["from"], item["name"])
if this_trip := trip_lookup.get(key):
item["linked_trip"] = this_trip
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,
)
@app.route("/trip")
def trip_list() -> str:
"""Page showing a list of trips."""
trip_list = agenda.trip.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]
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",
current=current,
past=past,
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,
)
@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.
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/<start>")
def trip_page(start: str) -> str:
"""Individual trip page."""
trip_iter = iter(agenda.trip.build_trip_list())
today = date.today()
data_dir = flask.current_app.config["PERSONAL_DATA"]
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)
coordinates = agenda.trip.collect_trip_coordinates(trip)
routes = agenda.trip.get_trip_routes(trip)
if any(route["type"] == "unbooked_flight" for route in routes) and not any(
pin["type"] == "airport" for pin in coordinates
):
airports = typing.cast(
dict[str, StrDict], travel.parse_yaml("airports", data_dir)
)
lhr = airports["LHR"]
coordinates.append(
{
"name": lhr["name"],
"type": "airport",
"latitude": lhr["latitude"],
"longitude": lhr["longitude"],
}
)
for route in routes:
if "geojson_filename" in route:
route["geojson"] = agenda.trip.read_geojson(route.pop("geojson_filename"))
if trip.end:
countries = {c.alpha_2 for c in trip.countries}
holidays = [
hol
for hol in agenda.holidays.get_all(
trip.start, trip.end, app.config["DATA_DIR"]
)
if hol.country.upper() in countries
]
holidays.sort(key=lambda item: (item.date, item.country))
else:
holidays = []
return flask.render_template(
"trip_page.html",
trip=trip,
prev_trip=prev_trip,
next_trip=next_trip,
today=today,
coordinates=coordinates,
routes=routes,
get_country=agenda.get_country,
format_list_with_ampersand=format_list_with_ampersand,
holidays=holidays,
human_readable_delta=human_readable_delta,
)
@app.route("/holidays")
def holiday_list() -> str:
"""List of holidays."""
today = date.today()
data_dir = app.config["DATA_DIR"]
next_year = today + timedelta(days=1 * 365)
items = agenda.holidays.get_all(today - timedelta(days=2), next_year, data_dir)
items.sort(key=lambda item: (item.date, item.country))
return flask.render_template(
"holiday_list.html", items=items, get_country=agenda.get_country, today=today
)
@app.route("/callback")
def auth_callback() -> tuple[str, int] | werkzeug.Response:
"""Process the authentication callback."""
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")