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
|
2024-01-16 17:07:59 +00:00
|
|
|
import typing
|
2024-01-16 12:06:46 +00:00
|
|
|
from datetime import date, datetime, timedelta
|
2023-10-02 20:35:30 +01:00
|
|
|
|
2023-10-06 23:29:22 +01:00
|
|
|
import flask
|
2024-01-23 10:49:58 +00:00
|
|
|
import UniAuth.auth
|
2023-10-06 23:29:22 +01:00
|
|
|
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
|
2024-01-16 12:06:46 +00:00
|
|
|
import agenda.holidays
|
2024-01-03 09:13:58 +00:00
|
|
|
import agenda.thespacedevs
|
2024-01-14 10:14:05 +00:00
|
|
|
import agenda.trip
|
2024-01-04 22:55:19 +00:00
|
|
|
from agenda import format_list_with_ampersand, travel
|
2024-01-16 17:07:59 +00:00
|
|
|
from agenda.types import StrDict
|
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
|
2023-12-07 15:52:48 +00:00
|
|
|
app.config.from_object("config.default")
|
2023-10-02 20:35:30 +01:00
|
|
|
|
2024-01-23 10:49:58 +00:00
|
|
|
app.before_request(UniAuth.auth.require_authentication)
|
2024-01-21 15:55:31 +00: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("/")
|
2023-11-05 14:40:02 +00:00
|
|
|
async def index() -> str:
|
2023-10-02 20:35:30 +01:00
|
|
|
"""Index page."""
|
|
|
|
now = datetime.now()
|
2023-12-07 15:52:48 +00:00
|
|
|
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
|
|
|
|
|
|
|
|
2024-01-03 09:13:58 +00:00
|
|
|
@app.route("/launches")
|
2024-01-21 15:56:18 +00:00
|
|
|
def launch_list() -> str:
|
2024-01-03 09:13:58 +00:00
|
|
|
"""Web page showing List of space launches."""
|
|
|
|
now = datetime.now()
|
|
|
|
data_dir = app.config["DATA_DIR"]
|
|
|
|
rocket_dir = os.path.join(data_dir, "thespacedevs")
|
2024-01-21 15:56:18 +00:00
|
|
|
rockets = agenda.thespacedevs.get_launches(rocket_dir, limit=100)
|
2024-01-03 09:13:58 +00:00
|
|
|
|
2024-01-19 20:35:52 +00:00
|
|
|
return flask.render_template(
|
|
|
|
"launches.html", rockets=rockets, now=now, get_country=agenda.get_country
|
|
|
|
)
|
2024-01-03 09:13:58 +00:00
|
|
|
|
|
|
|
|
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"])
|
|
|
|
|
|
|
|
|
2023-11-19 15:22:12 +00:00
|
|
|
@app.route("/travel")
|
2023-11-20 19:48:42 +00:00
|
|
|
def travel_list() -> str:
|
2023-11-19 15:22:12 +00:00
|
|
|
"""Page showing a list of upcoming travel."""
|
2023-12-07 15:52:48 +00:00
|
|
|
data_dir = app.config["PERSONAL_DATA"]
|
2024-01-04 22:55:19 +00:00
|
|
|
flights = travel.parse_yaml("flights", data_dir)
|
|
|
|
trains = travel.parse_yaml("trains", data_dir)
|
2023-11-19 15:22:12 +00:00
|
|
|
|
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."""
|
2023-12-07 15:52:48 +00:00
|
|
|
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()
|
2024-01-14 17:57:02 +00:00
|
|
|
|
2024-01-14 17:23:50 +00:00
|
|
|
conference_trip_lookup = {}
|
2024-01-14 17:57:02 +00:00
|
|
|
for trip in agenda.trip.build_trip_list():
|
2024-01-14 17:23:50 +00:00
|
|
|
for trip_conf in trip.conferences:
|
|
|
|
key = (trip_conf["start"], trip_conf["name"])
|
|
|
|
conference_trip_lookup[key] = trip
|
|
|
|
|
2023-12-04 21:53:33 +00:00
|
|
|
for conf in item_list:
|
2023-12-04 23:01:14 +00:00
|
|
|
conf["start_date"] = as_date(conf["start"])
|
|
|
|
conf["end_date"] = as_date(conf["end"])
|
2023-11-20 19:48:42 +00:00
|
|
|
|
2024-01-14 17:23:50 +00:00
|
|
|
key = (conf["start"], conf["name"])
|
|
|
|
if this_trip := conference_trip_lookup.get(key):
|
2024-01-14 17:57:02 +00:00
|
|
|
conf["linked_trip"] = this_trip
|
2024-01-14 17:23:50 +00:00
|
|
|
|
2023-12-04 23:01:14 +00:00
|
|
|
item_list.sort(key=operator.itemgetter("start_date"))
|
2023-11-20 19:48:42 +00:00
|
|
|
|
2023-12-04 23:01:14 +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(
|
2024-01-03 15:52:24 +00:00
|
|
|
"conference_list.html",
|
|
|
|
current=current,
|
|
|
|
past=past,
|
|
|
|
future=future,
|
|
|
|
today=today,
|
2024-01-04 22:55:19 +00:00
|
|
|
get_country=agenda.get_country,
|
2023-12-04 21:53:33 +00:00
|
|
|
)
|
2023-11-19 15:22:12 +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"]
|
2024-01-04 22:55:19 +00:00
|
|
|
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"
|
|
|
|
)
|
|
|
|
|
2024-01-14 17:57:02 +00:00
|
|
|
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
|
|
|
|
|
2024-01-01 21:26:39 +00:00
|
|
|
return flask.render_template(
|
|
|
|
"accommodation.html",
|
|
|
|
items=items,
|
|
|
|
total_nights_2024=total_nights_2024,
|
|
|
|
nights_abroad_2024=nights_abroad_2024,
|
2024-01-04 22:55:19 +00:00
|
|
|
get_country=agenda.get_country,
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2024-01-06 09:17:34 +00:00
|
|
|
@app.route("/trip")
|
|
|
|
def trip_list() -> str:
|
|
|
|
"""Page showing a list of trips."""
|
2024-01-14 10:14:05 +00:00
|
|
|
trip_list = agenda.trip.build_trip_list()
|
2024-01-06 09:17:34 +00:00
|
|
|
|
|
|
|
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]
|
2024-01-04 22:55:19 +00:00
|
|
|
|
2024-01-14 12:17:22 +00:00
|
|
|
future_coordinates, future_routes = agenda.trip.get_coordinates_and_routes(future)
|
|
|
|
past_coordinates, past_routes = agenda.trip.get_coordinates_and_routes(past)
|
2024-01-14 10:31:51 +00:00
|
|
|
|
2024-01-04 22:55:19 +00:00
|
|
|
return flask.render_template(
|
2024-01-14 10:32:52 +00:00
|
|
|
"trip_list.html",
|
2024-01-06 09:17:34 +00:00
|
|
|
current=current,
|
|
|
|
past=past,
|
|
|
|
future=future,
|
2024-01-14 10:31:51 +00:00
|
|
|
future_coordinates=future_coordinates,
|
|
|
|
future_routes=future_routes,
|
2024-01-14 12:17:22 +00:00
|
|
|
past_coordinates=past_coordinates,
|
|
|
|
past_routes=past_routes,
|
2024-01-06 09:17:34 +00:00
|
|
|
today=today,
|
2024-01-04 22:55:19 +00:00
|
|
|
get_country=agenda.get_country,
|
|
|
|
format_list_with_ampersand=format_list_with_ampersand,
|
2024-01-01 21:26:39 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
2024-01-22 13:12:33 +00:00
|
|
|
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
|
|
|
|
|
|
|
|
|
2024-01-12 14:04:06 +00:00
|
|
|
@app.route("/trip/<start>")
|
|
|
|
def trip_page(start: str) -> str:
|
2024-01-12 14:08:36 +00:00
|
|
|
"""Individual trip page."""
|
2024-01-14 12:29:39 +00:00
|
|
|
trip_iter = iter(agenda.trip.build_trip_list())
|
2024-01-12 14:04:06 +00:00
|
|
|
today = date.today()
|
2024-01-16 17:07:59 +00:00
|
|
|
data_dir = flask.current_app.config["PERSONAL_DATA"]
|
2024-01-12 14:04:06 +00:00
|
|
|
|
2024-01-14 12:29:39 +00:00
|
|
|
prev_trip = None
|
|
|
|
for trip in trip_iter:
|
|
|
|
if trip.start.isoformat() == start:
|
|
|
|
break
|
|
|
|
prev_trip = trip
|
|
|
|
next_trip = next(trip_iter, None)
|
2024-01-12 14:08:36 +00:00
|
|
|
|
|
|
|
if not trip:
|
|
|
|
flask.abort(404)
|
|
|
|
|
2024-01-14 10:14:05 +00:00
|
|
|
coordinates = agenda.trip.collect_trip_coordinates(trip)
|
|
|
|
routes = agenda.trip.get_trip_routes(trip)
|
2024-01-16 17:11:55 +00:00
|
|
|
if any(route["type"] == "unbooked_flight" for route in routes) and not any(
|
2024-01-16 17:07:59 +00:00
|
|
|
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"],
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
2024-01-14 10:31:51 +00:00
|
|
|
for route in routes:
|
|
|
|
if "geojson_filename" in route:
|
|
|
|
route["geojson"] = agenda.trip.read_geojson(route.pop("geojson_filename"))
|
2024-01-12 15:04:08 +00:00
|
|
|
|
2024-01-16 18:08:50 +00:00
|
|
|
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 = []
|
|
|
|
|
2024-01-12 14:04:06 +00:00
|
|
|
return flask.render_template(
|
|
|
|
"trip_page.html",
|
|
|
|
trip=trip,
|
2024-01-14 12:29:39 +00:00
|
|
|
prev_trip=prev_trip,
|
|
|
|
next_trip=next_trip,
|
2024-01-12 14:04:06 +00:00
|
|
|
today=today,
|
2024-01-12 16:54:52 +00:00
|
|
|
coordinates=coordinates,
|
2024-01-12 17:17:12 +00:00
|
|
|
routes=routes,
|
2024-01-12 14:04:06 +00:00
|
|
|
get_country=agenda.get_country,
|
|
|
|
format_list_with_ampersand=format_list_with_ampersand,
|
2024-01-16 18:08:50 +00:00
|
|
|
holidays=holidays,
|
2024-01-22 13:12:33 +00:00
|
|
|
human_readable_delta=human_readable_delta,
|
2024-01-12 14:04:06 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
2024-01-16 12:06:46 +00:00
|
|
|
@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(
|
2024-01-19 20:47:03 +00:00
|
|
|
"holiday_list.html", items=items, get_country=agenda.get_country, today=today
|
2024-01-16 12:06:46 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
2023-10-02 20:35:30 +01:00
|
|
|
if __name__ == "__main__":
|
|
|
|
app.run(host="0.0.0.0")
|