Edward Betts
e2afe0ffa4
Trip prices are visible on trip list, accommodation list, conference list and travel list. Prices are hidden if not logged in, except conference prices. Still need to show prices on individual trip page.
439 lines
13 KiB
Python
Executable file
439 lines
13 KiB
Python
Executable file
#!/usr/bin/python3
|
|
|
|
"""Web page to show upcoming events."""
|
|
|
|
import decimal
|
|
import inspect
|
|
import operator
|
|
import os.path
|
|
import sys
|
|
import traceback
|
|
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, uk_tz
|
|
from agenda.types import Trip
|
|
|
|
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 = agenda.trip.load_flights(data_dir)
|
|
trains = [
|
|
item
|
|
for item in travel.parse_yaml("trains", data_dir)
|
|
if isinstance(item["depart"], datetime)
|
|
]
|
|
|
|
route_distances = agenda.travel.load_route_distances(app.config["DATA_DIR"])
|
|
|
|
for train in trains:
|
|
for leg in train["legs"]:
|
|
agenda.travel.add_leg_route_distance(leg, route_distances)
|
|
|
|
if all("distance" in leg for leg in train["legs"]):
|
|
train["distance"] = sum(leg["distance"] for leg in train["legs"])
|
|
|
|
for travel_type in flights, trains:
|
|
for item in travel_type:
|
|
price = item.get("price")
|
|
if price:
|
|
item["price"] = decimal.Decimal(price)
|
|
|
|
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")
|
|
items = 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 items:
|
|
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
|
|
|
|
items.sort(key=operator.itemgetter("start_date"))
|
|
|
|
past = [conf for conf in items if conf["end_date"] < today]
|
|
current = [
|
|
conf
|
|
for conf in items
|
|
if conf["start_date"] <= today and conf["end_date"] >= today
|
|
]
|
|
future = [conf for conf in items 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)
|
|
for item in items:
|
|
price = item.get("price")
|
|
if price:
|
|
item["price"] = decimal.Decimal(price)
|
|
|
|
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
|
|
|
|
now = uk_tz.localize(datetime.now())
|
|
|
|
past = [conf for conf in items if conf["to"] < now]
|
|
current = [conf for conf in items if conf["from"] <= now and conf["to"] >= now]
|
|
future = [conf for conf in items if conf["from"] > now]
|
|
|
|
return flask.render_template(
|
|
"accommodation.html",
|
|
past=past,
|
|
current=current,
|
|
future=future,
|
|
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."""
|
|
route_distances = agenda.travel.load_route_distances(app.config["DATA_DIR"])
|
|
|
|
trip_list = [
|
|
trip
|
|
for trip in agenda.trip.build_trip_list(route_distances=route_distances)
|
|
if flask.g.user.is_authenticated or not trip.private
|
|
]
|
|
|
|
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 = [
|
|
trip
|
|
for trip in agenda.trip.build_trip_list()
|
|
if flask.g.user.is_authenticated or not trip.private
|
|
]
|
|
|
|
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
|
|
|
|
|
|
def get_trip_list(route_distances: agenda.travel.RouteDistances) -> list[Trip]:
|
|
"""Get list of trips respecting current authentication status."""
|
|
return [
|
|
trip
|
|
for trip in agenda.trip.build_trip_list(route_distances=route_distances)
|
|
if flask.g.user.is_authenticated or not trip.private
|
|
]
|
|
|
|
|
|
def get_prev_current_and_next_trip(
|
|
start: str, trip_list: list[Trip]
|
|
) -> tuple[Trip | None, Trip | None, Trip | None]:
|
|
"""Get previous trip, this trip and next trip."""
|
|
trip_iter = iter(trip_list)
|
|
prev_trip = None
|
|
for trip in trip_iter:
|
|
if trip.start.isoformat() == start:
|
|
break
|
|
prev_trip = trip
|
|
next_trip = next(trip_iter, None)
|
|
|
|
return (prev_trip, trip, next_trip)
|
|
|
|
|
|
@app.route("/trip/<start>")
|
|
def trip_page(start: str) -> str:
|
|
"""Individual trip page."""
|
|
route_distances = agenda.travel.load_route_distances(app.config["DATA_DIR"])
|
|
trip_list = get_trip_list(route_distances)
|
|
|
|
prev_trip, trip, next_trip = get_prev_current_and_next_trip(start, trip_list)
|
|
if not trip:
|
|
flask.abort(404)
|
|
|
|
today = date.today()
|
|
|
|
coordinates = agenda.trip.collect_trip_coordinates(trip)
|
|
routes = agenda.trip.get_trip_routes(trip)
|
|
|
|
agenda.trip.add_coordinates_for_unbooked_flights(routes, coordinates)
|
|
|
|
for route in routes:
|
|
if "geojson_filename" in route:
|
|
route["geojson"] = agenda.trip.read_geojson(
|
|
app.config["PERSONAL_DATA"], 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")
|