The accommodation page has been updated to provide a more comprehensive and dynamic view of travel statistics. Previously, the page only displayed hard-coded statistics for total nights away and abroad for the year 2024. This required manual updates each year and didn't provide historical context. This commit introduces the following changes: * **Dynamically Calculate Yearly Stats:** The `accommodation_list` view in `web_view.py` now calculates statistics for every year found in the accommodation data. It correctly handles stays that span across multiple years. * **Display All Years:** The `accommodation.html` template now iterates through a list of all calculated yearly stats, displaying a summary for each year automatically. * **Add Percentage of Year:** The template also calculates and displays what percentage of the year the "total nights" and "nights abroad" represent. This includes logic to correctly account for leap years (366 days) for an accurate calculation. These changes make the statistics more informative and ensure the page remains relevant over time without needing further manual code adjustments.
760 lines
23 KiB
Python
Executable file
760 lines
23 KiB
Python
Executable file
#!/usr/bin/python3
|
|
|
|
"""Web page to show upcoming events."""
|
|
|
|
import decimal
|
|
import inspect
|
|
import json
|
|
import operator
|
|
import os.path
|
|
import sys
|
|
import time
|
|
import traceback
|
|
from collections import defaultdict
|
|
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.fx
|
|
import agenda.holidays
|
|
import agenda.meteors
|
|
import agenda.stats
|
|
import agenda.thespacedevs
|
|
import agenda.trip
|
|
import agenda.trip_schengen
|
|
import agenda.utils
|
|
from agenda import calendar, format_list_with_ampersand, travel, uk_tz
|
|
from agenda.types import StrDict, 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 authentication 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,
|
|
)
|
|
|
|
|
|
def get_current_trip(today: date) -> Trip | None:
|
|
"""Get current trip."""
|
|
trip_list = get_trip_list(route_distances=None)
|
|
|
|
current = [
|
|
item
|
|
for item in trip_list
|
|
if item.start <= today and (item.end or item.start) >= today
|
|
]
|
|
assert len(current) < 2
|
|
return current[0] if current else None
|
|
|
|
|
|
@app.route("/")
|
|
async def index() -> str:
|
|
"""Index page."""
|
|
t0 = time.time()
|
|
now = datetime.now()
|
|
data = await agenda.data.get_data(now, app.config)
|
|
|
|
events = data.pop("events")
|
|
today = now.date()
|
|
|
|
markets_arg = flask.request.args.get("markets")
|
|
if markets_arg == "hide":
|
|
events = [e for e in events if e.name != "market"]
|
|
if markets_arg != "show":
|
|
agenda.data.hide_markets_while_away(events, data["accommodation_events"])
|
|
|
|
return flask.render_template(
|
|
"event_list.html",
|
|
today=today,
|
|
events=events,
|
|
get_country=agenda.get_country,
|
|
current_trip=get_current_trip(today),
|
|
fullcalendar_events=calendar.build_events(events),
|
|
start_event_list=date.today() - timedelta(days=1),
|
|
end_event_list=date.today() + timedelta(days=365 * 2),
|
|
render_time=(time.time() - t0),
|
|
**data,
|
|
)
|
|
|
|
|
|
@app.route("/calendar")
|
|
async def calendar_page() -> str:
|
|
"""Index page."""
|
|
now = datetime.now()
|
|
data = await agenda.data.get_data(now, app.config)
|
|
|
|
events = data.pop("events")
|
|
|
|
markets_arg = flask.request.args.get("markets")
|
|
if markets_arg == "hide":
|
|
events = [e for e in events if e.name != "market"]
|
|
if markets_arg != "show":
|
|
agenda.data.hide_markets_while_away(events, data["accommodation_events"])
|
|
|
|
return flask.render_template(
|
|
"calendar.html",
|
|
today=now.date(),
|
|
events=events,
|
|
fullcalendar_events=calendar.build_events(events),
|
|
**data,
|
|
)
|
|
|
|
|
|
@app.route("/recent")
|
|
async def recent() -> str:
|
|
"""Index page."""
|
|
t0 = time.time()
|
|
now = datetime.now()
|
|
data = await agenda.data.get_data(now, app.config)
|
|
|
|
events = data.pop("events")
|
|
|
|
markets_arg = flask.request.args.get("markets")
|
|
if markets_arg == "hide":
|
|
events = [e for e in events if e.name != "market"]
|
|
if markets_arg != "show":
|
|
agenda.data.hide_markets_while_away(events, data["accommodation_events"])
|
|
|
|
return flask.render_template(
|
|
"event_list.html",
|
|
today=now.date(),
|
|
events=events,
|
|
fullcalendar_events=calendar.build_events(events),
|
|
start_event_list=date.today() - timedelta(days=14),
|
|
end_event_list=date.today(),
|
|
render_time=(time.time() - t0),
|
|
**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")
|
|
launches = agenda.thespacedevs.get_launches(rocket_dir, limit=100)
|
|
assert launches
|
|
|
|
mission_type_filter = flask.request.args.get("type")
|
|
rocket_filter = flask.request.args.get("rocket")
|
|
orbit_filter = flask.request.args.get("orbit")
|
|
|
|
mission_types = {
|
|
launch["mission"]["type"] for launch in launches if launch["mission"]
|
|
}
|
|
|
|
orbits = {
|
|
(launch["orbit"]["name"], launch["orbit"]["abbrev"])
|
|
for launch in launches
|
|
if launch.get("orbit")
|
|
}
|
|
rockets = {launch["rocket"]["full_name"] for launch in launches}
|
|
|
|
launches = [
|
|
launch
|
|
for launch in launches
|
|
if (
|
|
not mission_type_filter
|
|
or (launch["mission"] and launch["mission"]["type"] == mission_type_filter)
|
|
)
|
|
and (not rocket_filter or launch["rocket"]["full_name"] == rocket_filter)
|
|
and (
|
|
not orbit_filter
|
|
or (launch.get("orbit") and launch["orbit"]["abbrev"] == orbit_filter)
|
|
)
|
|
]
|
|
|
|
return flask.render_template(
|
|
"launches.html",
|
|
launches=launches,
|
|
rockets=rockets,
|
|
now=now,
|
|
get_country=agenda.get_country,
|
|
mission_types=mission_types,
|
|
orbits=orbits,
|
|
)
|
|
|
|
|
|
@app.route("/meteors")
|
|
def meteor_list() -> str:
|
|
"""Web page showing meteor shower information."""
|
|
meteors = agenda.meteors.get_meteor_data()
|
|
|
|
return flask.render_template(
|
|
"meteors.html",
|
|
meteors=meteors,
|
|
today=date.today(),
|
|
)
|
|
|
|
|
|
@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.busy.get_busy_events(now.date(), app.config, trip_list)
|
|
gaps = agenda.busy.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 weekends using an optional date, week, or year parameter."""
|
|
today = datetime.now().date()
|
|
date_str = flask.request.args.get("date")
|
|
week_str = flask.request.args.get("week")
|
|
year_str = flask.request.args.get("year")
|
|
|
|
if date_str:
|
|
try:
|
|
start = datetime.strptime(date_str, "%Y-%m-%d").date()
|
|
except ValueError:
|
|
return flask.abort(400, description="Invalid date format. Use YYYY-MM-DD.")
|
|
elif week_str:
|
|
try:
|
|
week = int(week_str)
|
|
year = int(year_str) if year_str else today.year
|
|
if week < 1 or week > 53:
|
|
return flask.abort(
|
|
400, description="Week number must be between 1 and 53."
|
|
)
|
|
# Calculate the date of the first day of the given week
|
|
jan_1 = date(year, 1, 1)
|
|
week_1_start = jan_1 - timedelta(days=jan_1.weekday())
|
|
start = week_1_start + timedelta(weeks=week - 1)
|
|
except ValueError:
|
|
return flask.abort(
|
|
400, description="Invalid week or year format. Use integers."
|
|
)
|
|
else:
|
|
start = date(today.year, 1, 1)
|
|
|
|
current_week_number = today.isocalendar().week
|
|
|
|
trip_list = agenda.trip.build_trip_list()
|
|
busy_events = agenda.busy.get_busy_events(start, app.config, trip_list)
|
|
weekends = agenda.busy.weekends(
|
|
start, busy_events, trip_list, app.config["PERSONAL_DATA"]
|
|
)
|
|
return flask.render_template(
|
|
"weekends.html",
|
|
items=weekends,
|
|
current_week_number=current_week_number,
|
|
today=today,
|
|
)
|
|
|
|
|
|
@app.route("/travel")
|
|
def travel_list() -> str:
|
|
"""Page showing a list of upcoming travel."""
|
|
data_dir = app.config["PERSONAL_DATA"]
|
|
flights = agenda.trip.load_flight_bookings(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"])
|
|
|
|
return flask.render_template(
|
|
"travel.html",
|
|
flights=flights,
|
|
trains=trains,
|
|
fx_rate=agenda.fx.get_rates(app.config),
|
|
)
|
|
|
|
|
|
def build_conference_list() -> list[StrDict]:
|
|
"""Build conference list."""
|
|
data_dir = app.config["PERSONAL_DATA"]
|
|
filepath = os.path.join(data_dir, "conferences.yaml")
|
|
items: list[StrDict] = yaml.safe_load(open(filepath))
|
|
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"] = agenda.utils.as_date(conf["start"])
|
|
conf["end_date"] = agenda.utils.as_date(conf["end"])
|
|
|
|
price = conf.get("price")
|
|
if price:
|
|
conf["price"] = decimal.Decimal(price)
|
|
|
|
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"))
|
|
return items
|
|
|
|
|
|
@app.route("/conference")
|
|
def conference_list() -> str:
|
|
"""Page showing a list of conferences."""
|
|
today = date.today()
|
|
items = build_conference_list()
|
|
|
|
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,
|
|
future=future,
|
|
today=today,
|
|
get_country=agenda.get_country,
|
|
fx_rate=agenda.fx.get_rates(app.config),
|
|
)
|
|
|
|
|
|
@app.route("/conference/past")
|
|
def past_conference_list() -> str:
|
|
"""Page showing a list of conferences."""
|
|
today = date.today()
|
|
return flask.render_template(
|
|
"conference_list.html",
|
|
past=[conf for conf in build_conference_list() if conf["end_date"] < today],
|
|
today=today,
|
|
get_country=agenda.get_country,
|
|
fx_rate=agenda.fx.get_rates(app.config),
|
|
)
|
|
|
|
@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)
|
|
|
|
# Create a dictionary to hold stats for each year
|
|
year_stats = defaultdict(lambda: {"total_nights": 0, "nights_abroad": 0})
|
|
|
|
# Calculate stats for each year
|
|
for stay in items:
|
|
current_date = stay["from"].date()
|
|
end_date = stay["to"].date()
|
|
while current_date < end_date:
|
|
year = current_date.year
|
|
year_stats[year]["total_nights"] += 1
|
|
if stay.get("country") != "gb":
|
|
year_stats[year]["nights_abroad"] += 1
|
|
current_date += timedelta(days=1)
|
|
|
|
# Sort the stats by year in descending order
|
|
sorted_year_stats = sorted(
|
|
year_stats.items(), key=lambda item: item[0], reverse=True
|
|
)
|
|
|
|
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,
|
|
year_stats=sorted_year_stats,
|
|
get_country=agenda.get_country,
|
|
fx_rate=agenda.fx.get_rates(app.config),
|
|
)
|
|
|
|
|
|
def get_trip_list(
|
|
route_distances: agenda.travel.RouteDistances | None = None,
|
|
) -> list[Trip]:
|
|
"""Get list of trips respecting current authentication status."""
|
|
trips = [
|
|
trip
|
|
for trip in agenda.trip.build_trip_list(route_distances=route_distances)
|
|
if flask.g.user.is_authenticated or not trip.private
|
|
]
|
|
|
|
# Add Schengen compliance information to each trip
|
|
for trip in trips:
|
|
agenda.trip_schengen.add_schengen_compliance_to_trip(trip)
|
|
|
|
return trips
|
|
|
|
|
|
@app.route("/trip")
|
|
def trip_list() -> werkzeug.Response:
|
|
"""Trip list to redirect to future trip list."""
|
|
return flask.redirect(flask.url_for("trip_future_list"))
|
|
|
|
|
|
def calc_total_distance(trips: list[Trip]) -> float:
|
|
"""Total distance for trips."""
|
|
total = 0.0
|
|
for item in trips:
|
|
if dist := item.total_distance():
|
|
total += dist
|
|
|
|
return total
|
|
|
|
|
|
def calc_total_co2_kg(trips: list[Trip]) -> float:
|
|
"""Total CO₂ for trips."""
|
|
return sum(item.total_co2_kg() for item in trips)
|
|
|
|
|
|
def sum_distances_by_transport_type(trips: list[Trip]) -> list[tuple[str, float]]:
|
|
"""Sum distances by transport type."""
|
|
distances_by_transport_type: defaultdict[str, float] = defaultdict(float)
|
|
for trip in trips:
|
|
for transport_type, dist in trip.distances_by_transport_type():
|
|
distances_by_transport_type[transport_type] += dist
|
|
|
|
return list(distances_by_transport_type.items())
|
|
|
|
|
|
@app.route("/trip/past")
|
|
def trip_past_list() -> str:
|
|
"""Page showing a list of past trips."""
|
|
route_distances = agenda.travel.load_route_distances(app.config["DATA_DIR"])
|
|
trip_list = get_trip_list(route_distances)
|
|
today = date.today()
|
|
|
|
past = [item for item in trip_list if (item.end or item.start) < today]
|
|
|
|
coordinates, routes = agenda.trip.get_coordinates_and_routes(past)
|
|
|
|
return flask.render_template(
|
|
"trip/list.html",
|
|
heading="Past trips",
|
|
trips=reversed(past),
|
|
coordinates=coordinates,
|
|
routes=routes,
|
|
today=today,
|
|
get_country=agenda.get_country,
|
|
format_list_with_ampersand=format_list_with_ampersand,
|
|
fx_rate=agenda.fx.get_rates(app.config),
|
|
total_distance=calc_total_distance(past),
|
|
total_co2_kg=calc_total_co2_kg(past),
|
|
distances_by_transport_type=sum_distances_by_transport_type(past),
|
|
)
|
|
|
|
|
|
@app.route("/trip/future")
|
|
def trip_future_list() -> str:
|
|
"""Page showing a list of future trips."""
|
|
route_distances = agenda.travel.load_route_distances(app.config["DATA_DIR"])
|
|
trip_list = get_trip_list(route_distances)
|
|
today = date.today()
|
|
|
|
current = [
|
|
item
|
|
for item in trip_list
|
|
if item.start <= today and (item.end or item.start) >= today
|
|
]
|
|
|
|
future = [item for item in trip_list if item.start > today]
|
|
|
|
coordinates, routes = agenda.trip.get_coordinates_and_routes(current + future)
|
|
|
|
return flask.render_template(
|
|
"trip/list.html",
|
|
heading="Future trips",
|
|
trips=current + future,
|
|
coordinates=coordinates,
|
|
routes=routes,
|
|
today=today,
|
|
get_country=agenda.get_country,
|
|
format_list_with_ampersand=format_list_with_ampersand,
|
|
fx_rate=agenda.fx.get_rates(app.config),
|
|
total_distance=calc_total_distance(current + future),
|
|
total_co2_kg=calc_total_co2_kg(current + future),
|
|
distances_by_transport_type=sum_distances_by_transport_type(current + future),
|
|
)
|
|
|
|
|
|
@app.route("/trip/text")
|
|
def trip_list_text() -> str:
|
|
"""Page showing a list of trips."""
|
|
trip_list = get_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 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
|
|
current_trip = None
|
|
for trip in trip_iter:
|
|
if trip.start.isoformat() == start:
|
|
current_trip = trip
|
|
break
|
|
prev_trip = trip
|
|
next_trip = next(trip_iter, None)
|
|
|
|
return (prev_trip, current_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)
|
|
|
|
# Add Schengen compliance information
|
|
trip = agenda.trip_schengen.add_schengen_compliance_to_trip(trip)
|
|
|
|
coordinates = agenda.trip.collect_trip_coordinates(trip)
|
|
routes = agenda.trip.get_trip_routes(trip, app.config["PERSONAL_DATA"])
|
|
|
|
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")
|
|
)
|
|
|
|
return flask.render_template(
|
|
"trip_page.html",
|
|
trip=trip,
|
|
prev_trip=prev_trip,
|
|
next_trip=next_trip,
|
|
today=date.today(),
|
|
coordinates=coordinates,
|
|
routes=routes,
|
|
get_country=agenda.get_country,
|
|
format_list_with_ampersand=format_list_with_ampersand,
|
|
holidays=agenda.holidays.get_trip_holidays(trip),
|
|
human_readable_delta=agenda.utils.human_readable_delta,
|
|
)
|
|
|
|
|
|
@app.route("/trip/<start>/debug")
|
|
def trip_debug_page(start: str) -> str:
|
|
"""Trip debug page showing raw trip object data."""
|
|
|
|
if not flask.g.user.is_authenticated:
|
|
flask.abort(401)
|
|
|
|
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)
|
|
|
|
# Add Schengen compliance information
|
|
trip = agenda.trip_schengen.add_schengen_compliance_to_trip(trip)
|
|
|
|
# Convert trip object to dictionary for display
|
|
trip_dict = {
|
|
"start": trip.start.isoformat(),
|
|
"name": trip.name,
|
|
"private": trip.private,
|
|
"travel": trip.travel,
|
|
"accommodation": trip.accommodation,
|
|
"conferences": trip.conferences,
|
|
"events": trip.events,
|
|
"flight_bookings": trip.flight_bookings,
|
|
"computed_properties": {
|
|
"title": trip.title,
|
|
"end": trip.end.isoformat() if trip.end else None,
|
|
"countries": [
|
|
{"name": c.name, "alpha_2": c.alpha_2, "flag": c.flag}
|
|
for c in trip.countries
|
|
],
|
|
"locations": [
|
|
{
|
|
"location": loc,
|
|
"country": {"name": country.name, "alpha_2": country.alpha_2},
|
|
}
|
|
for loc, country in trip.locations()
|
|
],
|
|
"total_distance": trip.total_distance(),
|
|
"total_co2_kg": trip.total_co2_kg(),
|
|
"distances_by_transport_type": trip.distances_by_transport_type(),
|
|
"co2_by_transport_type": trip.co2_by_transport_type(),
|
|
},
|
|
"schengen_compliance": (
|
|
{
|
|
"total_days_used": trip.schengen_compliance.total_days_used,
|
|
"days_remaining": trip.schengen_compliance.days_remaining,
|
|
"is_compliant": trip.schengen_compliance.is_compliant,
|
|
"current_180_day_period": [
|
|
trip.schengen_compliance.current_180_day_period[0].isoformat(),
|
|
trip.schengen_compliance.current_180_day_period[1].isoformat(),
|
|
],
|
|
"days_over_limit": trip.schengen_compliance.days_over_limit,
|
|
}
|
|
if trip.schengen_compliance
|
|
else None
|
|
),
|
|
}
|
|
|
|
# Convert to JSON for pretty printing
|
|
trip_json = json.dumps(trip_dict, indent=2, default=str)
|
|
|
|
return flask.render_template(
|
|
"trip_debug.html",
|
|
trip=trip,
|
|
trip_json=trip_json,
|
|
start=start,
|
|
)
|
|
|
|
|
|
@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("/birthdays")
|
|
def birthday_list() -> str:
|
|
"""List of birthdays."""
|
|
today = date.today()
|
|
if not flask.g.user.is_authenticated:
|
|
flask.abort(401)
|
|
data_dir = app.config["PERSONAL_DATA"]
|
|
entities_file = os.path.join(data_dir, "entities.yaml")
|
|
items = agenda.birthday.get_birthdays(today - timedelta(days=2), entities_file)
|
|
items.sort(key=lambda item: item.date)
|
|
return flask.render_template("birthday_list.html", items=items, today=today)
|
|
|
|
|
|
@app.route("/trip/stats")
|
|
def trip_stats() -> str:
|
|
"""Travel stats: distance and price by year and travel type."""
|
|
route_distances = agenda.travel.load_route_distances(app.config["DATA_DIR"])
|
|
trip_list = get_trip_list(route_distances)
|
|
|
|
conferences = sum(len(item.conferences) for item in trip_list)
|
|
|
|
yearly_stats = agenda.stats.calculate_yearly_stats(trip_list)
|
|
|
|
return flask.render_template(
|
|
"trip/stats.html",
|
|
count=len(trip_list),
|
|
total_distance=calc_total_distance(trip_list),
|
|
distances_by_transport_type=sum_distances_by_transport_type(trip_list),
|
|
yearly_stats=yearly_stats,
|
|
conferences=conferences,
|
|
previously_visited=app.config.get("PREVIOUSLY_VISITED", set),
|
|
)
|
|
|
|
|
|
@app.route("/schengen")
|
|
def schengen_report() -> str:
|
|
"""Schengen compliance report."""
|
|
return agenda.trip_schengen.flask_route_schengen_report()
|
|
|
|
|
|
@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")
|