Update
This commit is contained in:
parent
cbc681ddbc
commit
ebe672b972
285
main.py
285
main.py
|
@ -2,44 +2,60 @@
|
||||||
"""Check prices of ferries to France."""
|
"""Check prices of ferries to France."""
|
||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
from datetime import date, datetime
|
import json
|
||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
import re
|
||||||
|
from datetime import date, datetime, timedelta
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import configparser
|
|
||||||
|
|
||||||
import flask
|
import flask
|
||||||
import requests
|
import pytz
|
||||||
|
import routes
|
||||||
import werkzeug.exceptions
|
import werkzeug.exceptions
|
||||||
from werkzeug.debug.tbtools import get_current_traceback
|
from werkzeug.debug.tbtools import get_current_traceback
|
||||||
|
from werkzeug.wrappers import Response
|
||||||
|
|
||||||
import pytz
|
from ferry import ports
|
||||||
|
from ferry.api import get_accommodations, get_prices
|
||||||
|
from ferry.read_config import ferry_config, vehicle_from_config
|
||||||
|
|
||||||
app = flask.Flask(__name__)
|
app = flask.Flask(__name__)
|
||||||
app.debug = True
|
app.debug = False
|
||||||
|
|
||||||
ports = {
|
|
||||||
"PORTSMOUTH": "GBPME",
|
|
||||||
"PLYMOUTH": "GBPLY",
|
|
||||||
"POOLE": "GBPOO",
|
|
||||||
"CAEN": "FROUI",
|
|
||||||
"CHERBOURG": "FRCER",
|
|
||||||
"ST MALO": "FRSML",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
ferry_config = configparser.ConfigParser()
|
def cache_location() -> str:
|
||||||
ferry_config.read("/home/edward/.config/brittany-ferries/config")
|
return os.path.expanduser(ferry_config.get("cache", "location"))
|
||||||
|
|
||||||
|
|
||||||
def get_vehicle() -> dict[str, str | int | list[str] | dict[str, None]]:
|
def cache_filename(params: str) -> str:
|
||||||
"""Return vehicle detail in the format for the Brittany Ferries API."""
|
"""Get a filename to use for caching."""
|
||||||
return {
|
now_str = datetime.utcnow().strftime("%Y-%m-%d_%H%M")
|
||||||
"type": ferry_config.get("vehicle", "type"),
|
params = params.replace(":", "_").replace(".", "_")
|
||||||
"registrations": [ferry_config.get("vehicle", "registration")],
|
|
||||||
"height": ferry_config.getint("vehicle", "height"),
|
return os.path.join(cache_location(), now_str + "_" + params + ".json")
|
||||||
"length": ferry_config.getint("vehicle", "length"),
|
|
||||||
"extras": {"rearMountedBikeCarrier": None},
|
|
||||||
}
|
def time_to_minutes(t: str) -> int:
|
||||||
|
"""Convert time (22:50) into minutes since midnight."""
|
||||||
|
m = re.match(r"^(\d\d):(\d\d)$", t)
|
||||||
|
assert m
|
||||||
|
hours, minutes = [int(j) for j in m.groups()]
|
||||||
|
|
||||||
|
return (60 * hours) + minutes
|
||||||
|
|
||||||
|
|
||||||
|
def get_duration(depart: str, arrive: str, time_delta: int) -> str:
|
||||||
|
"""Given two times calculate the duration and return as string."""
|
||||||
|
depart_min = time_to_minutes(depart)
|
||||||
|
arrive_min = time_to_minutes(arrive) + time_delta
|
||||||
|
|
||||||
|
duration = arrive_min - depart_min
|
||||||
|
|
||||||
|
if depart_min > arrive_min:
|
||||||
|
duration += 60 * 24
|
||||||
|
|
||||||
|
return f"{duration // 60}h{ duration % 60:02d}m"
|
||||||
|
|
||||||
|
|
||||||
@app.errorhandler(werkzeug.exceptions.InternalServerError)
|
@app.errorhandler(werkzeug.exceptions.InternalServerError)
|
||||||
|
@ -59,56 +75,10 @@ def exception_handler(e):
|
||||||
|
|
||||||
|
|
||||||
def parse_date(d: str) -> date:
|
def parse_date(d: str) -> date:
|
||||||
"""Parse an ISO date."""
|
"""Parse a date from a string in ISO format."""
|
||||||
return datetime.strptime(d, "%Y-%m-%d").date()
|
return datetime.strptime(d, "%Y-%m-%d").date()
|
||||||
|
|
||||||
|
|
||||||
def get_accommodations(
|
|
||||||
departure_port: str, arrival_port: str, departure_date: str, ticket_tier: str
|
|
||||||
):
|
|
||||||
url = "https://www.brittany-ferries.co.uk/api/ferry/v1/crossing/accommodations"
|
|
||||||
post_data = {
|
|
||||||
"bookingReference": None,
|
|
||||||
"departurePort": departure_port,
|
|
||||||
"arrivalPort": arrival_port,
|
|
||||||
"departureDate": departure_date,
|
|
||||||
"passengers": {"adults": 2, "children": 0, "infants": 0},
|
|
||||||
"disability": None,
|
|
||||||
"vehicle": get_vehicle(),
|
|
||||||
"petCabinsNeeded": False,
|
|
||||||
"ticketTier": ticket_tier,
|
|
||||||
"pets": {"smallDogs": 0, "largeDogs": 0, "cats": 0},
|
|
||||||
"sponsor": None,
|
|
||||||
}
|
|
||||||
|
|
||||||
r = requests.post(url, json=post_data)
|
|
||||||
return r.json()
|
|
||||||
|
|
||||||
|
|
||||||
def get_prices(
|
|
||||||
departure_port: str, arrival_port: str, from_date: str, to_date: str
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""Call Brittany Ferries API to get details of crossings."""
|
|
||||||
url = "https://www.brittany-ferries.co.uk/api/ferry/v1/crossing/prices"
|
|
||||||
|
|
||||||
post_data = {
|
|
||||||
"bookingReference": None,
|
|
||||||
"pets": {"smallDogs": 0, "largeDogs": 0, "cats": 0},
|
|
||||||
"passengers": {"adults": 2, "children": 0, "infants": 0},
|
|
||||||
"vehicle": get_vehicle(),
|
|
||||||
"departurePort": departure_port,
|
|
||||||
"arrivalPort": arrival_port,
|
|
||||||
"disability": None,
|
|
||||||
"sponsor": None,
|
|
||||||
"fromDate": f"{from_date}T00:00:00",
|
|
||||||
"toDate": f"{to_date}T23:59:59",
|
|
||||||
}
|
|
||||||
|
|
||||||
r = requests.post(url, json=post_data)
|
|
||||||
data: dict[str, Any] = r.json()
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/route/<departure_port>/<arrival_port>/<from_date>/<to_date>")
|
@app.route("/route/<departure_port>/<arrival_port>/<from_date>/<to_date>")
|
||||||
def show_route(
|
def show_route(
|
||||||
departure_port: str, arrival_port: str, from_date: str, to_date: str
|
departure_port: str, arrival_port: str, from_date: str, to_date: str
|
||||||
|
@ -128,8 +98,9 @@ def show_route(
|
||||||
|
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
def start():
|
def start() -> Response:
|
||||||
"""Start page."""
|
"""Start page."""
|
||||||
|
return flask.render_template("index.html")
|
||||||
return flask.redirect(flask.url_for("outbound_page"))
|
return flask.redirect(flask.url_for("outbound_page"))
|
||||||
|
|
||||||
|
|
||||||
|
@ -146,81 +117,183 @@ def cabins_url(dep, arr, crossing, ticket_tier):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_days_until_start():
|
def get_days_until_start() -> int:
|
||||||
|
"""How long until the travel date."""
|
||||||
start = date.fromisoformat(ferry_config.get("dates", "start"))
|
start = date.fromisoformat(ferry_config.get("dates", "start"))
|
||||||
return (start - date.today()).days
|
return (start - date.today()).days
|
||||||
|
|
||||||
|
|
||||||
@app.route("/outbound")
|
def get_prices_with_cache(
|
||||||
def outbound_page() -> str:
|
name: str,
|
||||||
"""Show all routes on one page."""
|
start: str,
|
||||||
selection = [
|
end: str,
|
||||||
["PORTSMOUTH", "CAEN"],
|
selection: list[tuple[str, str]],
|
||||||
["PORTSMOUTH", "CHERBOURG"],
|
refresh: bool = False,
|
||||||
["PORTSMOUTH", "ST MALO"],
|
) -> list[tuple[str, str, dict[str, Any]]]:
|
||||||
["POOLE", "CHERBOURG"],
|
|
||||||
]
|
|
||||||
|
|
||||||
from_date = ferry_config.get("outbound", "from")
|
params = f"{name}_{start}_{end}"
|
||||||
to_date = ferry_config.get("outbound", "to")
|
existing_files = os.listdir(cache_location())
|
||||||
|
existing = [f for f in existing_files if f.endswith(params + ".json")]
|
||||||
|
if not refresh and existing:
|
||||||
|
recent_filename = max(existing)
|
||||||
|
recent = datetime.strptime(recent_filename, f"%Y-%m-%d_%H%M_{params}.json")
|
||||||
|
|
||||||
|
now = datetime.utcnow()
|
||||||
|
|
||||||
|
delta = now - recent
|
||||||
|
if delta < timedelta(hours=1):
|
||||||
|
full = os.path.join(cache_location(), recent_filename)
|
||||||
|
data = json.load(open(full))
|
||||||
|
return data
|
||||||
|
|
||||||
|
vehicle = vehicle_from_config(ferry_config)
|
||||||
|
filename = cache_filename(params)
|
||||||
|
|
||||||
all_data = [
|
all_data = [
|
||||||
(dep, arr, get_prices(ports[dep], ports[arr], from_date, to_date)["crossings"])
|
(dep, arr, get_prices(ports[dep], ports[arr], start, end, vehicle)["crossings"])
|
||||||
for dep, arr in selection
|
for dep, arr in selection
|
||||||
]
|
]
|
||||||
|
|
||||||
|
with open(filename, "w") as out:
|
||||||
|
print(filename)
|
||||||
|
json.dump(all_data, out, indent=2)
|
||||||
|
|
||||||
|
return all_data
|
||||||
|
|
||||||
|
|
||||||
|
def build_outbound(section: str) -> str:
|
||||||
|
"""Show all routes on one page."""
|
||||||
|
selection = [
|
||||||
|
("PORTSMOUTH", "CAEN"),
|
||||||
|
("PORTSMOUTH", "CHERBOURG"),
|
||||||
|
("PORTSMOUTH", "ST MALO"),
|
||||||
|
# ("POOLE", "CHERBOURG"),
|
||||||
|
]
|
||||||
|
|
||||||
|
start = ferry_config.get(section, "from")
|
||||||
|
end = ferry_config.get(section, "to")
|
||||||
|
refresh = bool(flask.request.args.get("refresh"))
|
||||||
|
|
||||||
|
all_data = get_prices_with_cache(section, start, end, selection, refresh)
|
||||||
|
|
||||||
return flask.render_template(
|
return flask.render_template(
|
||||||
"all_routes.html",
|
"all_routes.html",
|
||||||
data=all_data,
|
data=all_data,
|
||||||
days_until_start=get_days_until_start(),
|
days_until_start=get_days_until_start(),
|
||||||
ports=ports,
|
ports=ports,
|
||||||
parse_date=parse_date,
|
parse_date=parse_date,
|
||||||
from_date=from_date,
|
from_date=start,
|
||||||
to_date=to_date,
|
to_date=end,
|
||||||
other="return",
|
|
||||||
cabins_url=cabins_url,
|
cabins_url=cabins_url,
|
||||||
|
get_duration=get_duration,
|
||||||
|
time_delta=-60,
|
||||||
|
format_pet_options=format_pet_options,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/outbound1")
|
||||||
|
def outbound1_page() -> str:
|
||||||
|
return build_outbound("outbound1")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/outbound2")
|
||||||
|
def outbound2_page() -> str:
|
||||||
|
return build_outbound("outbound2")
|
||||||
|
|
||||||
|
|
||||||
|
def format_pet_options(o: dict[str, bool]) -> list[str]:
|
||||||
|
ret = []
|
||||||
|
if o.get("petCabinAvailable"):
|
||||||
|
ret.append("pet cabins")
|
||||||
|
if o.get("smallKennelAvailable"):
|
||||||
|
ret.append("small kennel")
|
||||||
|
if o.get("stayInCarAvailable"):
|
||||||
|
ret.append("pets stay in car")
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
@app.route("/return")
|
@app.route("/return")
|
||||||
def return_page() -> str:
|
def return_page() -> str:
|
||||||
"""Show all routes on one page."""
|
"""Show all routes on one page."""
|
||||||
selection = [
|
selection = [
|
||||||
["CAEN", "PORTSMOUTH"],
|
("CAEN", "PORTSMOUTH"),
|
||||||
["CHERBOURG", "PORTSMOUTH"],
|
("CHERBOURG", "PORTSMOUTH"),
|
||||||
["ST MALO", "PORTSMOUTH"],
|
("ST MALO", "PORTSMOUTH"),
|
||||||
["CHERBOURG", "POOLE"],
|
# ("CHERBOURG", "POOLE"),
|
||||||
]
|
]
|
||||||
|
|
||||||
from_date = ferry_config.get("return", "from")
|
start = ferry_config.get("return", "from")
|
||||||
to_date = ferry_config.get("return", "to")
|
end = ferry_config.get("return", "to")
|
||||||
|
refresh = bool(flask.request.args.get("refresh"))
|
||||||
|
|
||||||
|
all_data = get_prices_with_cache("return", start, end, selection, refresh)
|
||||||
|
|
||||||
all_data = [
|
|
||||||
(dep, arr, get_prices(ports[dep], ports[arr], from_date, to_date)["crossings"])
|
|
||||||
for dep, arr in selection
|
|
||||||
]
|
|
||||||
return flask.render_template(
|
return flask.render_template(
|
||||||
"all_routes.html",
|
"all_routes.html",
|
||||||
data=all_data,
|
data=all_data,
|
||||||
ports=ports,
|
ports=ports,
|
||||||
days_until_start=get_days_until_start(),
|
days_until_start=get_days_until_start(),
|
||||||
parse_date=parse_date,
|
parse_date=parse_date,
|
||||||
from_date=from_date,
|
from_date=start,
|
||||||
to_date=to_date,
|
to_date=end,
|
||||||
other="outbound",
|
|
||||||
cabins_url=cabins_url,
|
cabins_url=cabins_url,
|
||||||
|
get_duration=get_duration,
|
||||||
|
time_delta=60,
|
||||||
|
format_pet_options=format_pet_options,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_accommodations_with_cache(
|
||||||
|
dep: str, arr: str, d: str, ticket_tier: str, refresh: bool = False
|
||||||
|
) -> dict[str, list[dict[str, Any]]]:
|
||||||
|
params = f"{dep}_{arr}_{d}_{ticket_tier}"
|
||||||
|
existing_files = os.listdir(cache_location())
|
||||||
|
existing = [f for f in existing_files if f.endswith(params + ".json")]
|
||||||
|
if not refresh and existing:
|
||||||
|
recent_filename = max(existing)
|
||||||
|
recent = datetime.strptime(recent_filename, f"%Y-%m-%d_%H%M_{params}.json")
|
||||||
|
|
||||||
|
now = datetime.utcnow()
|
||||||
|
|
||||||
|
delta = now - recent
|
||||||
|
if delta < timedelta(hours=1):
|
||||||
|
full = os.path.join(cache_location(), recent_filename)
|
||||||
|
data = json.load(open(full))
|
||||||
|
return data
|
||||||
|
|
||||||
|
vehicle = vehicle_from_config(ferry_config)
|
||||||
|
filename = cache_filename(params)
|
||||||
|
data = get_accommodations(dep, arr, d, ticket_tier, vehicle)
|
||||||
|
|
||||||
|
with open(filename, "w") as out:
|
||||||
|
print(filename)
|
||||||
|
json.dump(data, out, indent=2)
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
@app.route("/cabins/<departure_port>/<arrival_port>/<departure_date>/<ticket_tier>")
|
@app.route("/cabins/<departure_port>/<arrival_port>/<departure_date>/<ticket_tier>")
|
||||||
def cabins(departure_port, arrival_port, departure_date, ticket_tier):
|
def cabins(
|
||||||
data = get_accommodations(departure_port, arrival_port, departure_date, ticket_tier)
|
departure_port: str, arrival_port: str, departure_date: str, ticket_tier: str
|
||||||
|
) -> str:
|
||||||
|
cabin_data = get_accommodations_with_cache(
|
||||||
|
departure_port, arrival_port, departure_date, ticket_tier
|
||||||
|
)
|
||||||
|
accommodations = [
|
||||||
|
a
|
||||||
|
for a in cabin_data["accommodations"]
|
||||||
|
if a["quantityAvailable"] > 0 and a["code"] != "RS"
|
||||||
|
# and "Inside" not in a["description"]
|
||||||
|
]
|
||||||
return flask.render_template(
|
return flask.render_template(
|
||||||
"cabins.html",
|
"cabins.html",
|
||||||
departure_port=departure_port,
|
departure_port=departure_port,
|
||||||
arrival_port=arrival_port,
|
arrival_port=arrival_port,
|
||||||
departure_date=departure_date,
|
departure_date=departure_date,
|
||||||
ticket_tier=ticket_tier,
|
ticket_tier=ticket_tier,
|
||||||
accommodations=data["accommodations"],
|
accommodations=accommodations,
|
||||||
|
pet_accommodations=cabin_data["petAccommodations"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,24 @@
|
||||||
<title>Ferries to France</title>
|
<title>Ferries to France</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx" crossorigin="anonymous">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx" crossorigin="anonymous">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background: black;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table { color: white; }
|
||||||
|
|
||||||
|
a:link {
|
||||||
|
color: rgb(127, 127, 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
-->
|
||||||
|
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
{% from "individual_route.html" import route_table with context %}
|
{% from "individual_route.html" import route_table with context %}
|
||||||
|
@ -12,9 +30,11 @@
|
||||||
<body>
|
<body>
|
||||||
<div class="m-3">
|
<div class="m-3">
|
||||||
|
|
||||||
<p>{{ days_until_start }} days until start of Dodainville week: Friday 16 September 2022</p>
|
<p>{{ days_until_start }} days / {{ (days_until_start / 7) | int }} weeks / {{ "{:.1f}".format(days_until_start / 30.5) }} months until start of Dodainville week: Friday 17 March 2022</p>
|
||||||
|
|
||||||
|
{#
|
||||||
<p><a href="{{ url_for(other + "_page") }}">{{ other }}</a></p>
|
<p><a href="{{ url_for(other + "_page") }}">{{ other }}</a></p>
|
||||||
|
#}
|
||||||
|
|
||||||
{% if extra_routes %}
|
{% if extra_routes %}
|
||||||
<ul>
|
<ul>
|
||||||
|
@ -28,6 +48,10 @@
|
||||||
<h4>{{ dep.title() }} to {{ arr.title() }}</h4>
|
<h4>{{ dep.title() }} to {{ arr.title() }}</h4>
|
||||||
{{ route_table(dep, arr, days) }}
|
{{ route_table(dep, arr, days) }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
|
<!--
|
||||||
|
{{ data | pprint | safe }}
|
||||||
|
-->
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
29
templates/base.html
Normal file
29
templates/base.html
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx" crossorigin="anonymous">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
|
||||||
|
<title>
|
||||||
|
{% block title %}{% endblock %}
|
||||||
|
</title>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* body {
|
||||||
|
background: black;
|
||||||
|
color: white;
|
||||||
|
} */
|
||||||
|
</style>
|
||||||
|
|
||||||
|
{% block style %}{% endblock %}
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-A3rJD856KowSb7dwlZdYEkO39Gagi7vIsF0jrRAoQmDKKtQBHUuLZ9AsSv4jD4Xa" crossorigin="anonymous"></script>
|
||||||
|
|
||||||
|
{% block script %}{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -5,6 +5,25 @@
|
||||||
<title>Ferries to France</title>
|
<title>Ferries to France</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx" crossorigin="anonymous">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx" crossorigin="anonymous">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background: black;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table { color: white; }
|
||||||
|
|
||||||
|
a:link {
|
||||||
|
color: rgb(127, 127, 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
-->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
@ -14,12 +33,12 @@
|
||||||
|
|
||||||
<p>{{ departure_date }} {{ ticket_tier }}</p>
|
<p>{{ departure_date }} {{ ticket_tier }}</p>
|
||||||
|
|
||||||
<table class="table">
|
<table class="table w-auto">
|
||||||
<tr>
|
<tr>
|
||||||
<th>code</th>
|
<th>code</th>
|
||||||
<th>description</th>
|
<th>description</th>
|
||||||
<th>max adults</th>
|
<th>births</th>
|
||||||
<th>quantity available</th>
|
<th>quantity<br/>available</th>
|
||||||
<th>price</th>
|
<th>price</th>
|
||||||
</tr>
|
</tr>
|
||||||
{% for a in accommodations if a.quantityAvailable > 0 %}
|
{% for a in accommodations if a.quantityAvailable > 0 %}
|
||||||
|
@ -27,12 +46,28 @@
|
||||||
<td>{{ a.code }}</td>
|
<td>{{ a.code }}</td>
|
||||||
<td>{{ a.description }}</td>
|
<td>{{ a.description }}</td>
|
||||||
<td>{{ a.maxAdults }}</td>
|
<td>{{ a.maxAdults }}</td>
|
||||||
<td>{{ a.quantityAvailable }}</td>
|
<td>
|
||||||
|
{% if a.quantityAvailable == 10 %}
|
||||||
|
10+
|
||||||
|
{% else %}
|
||||||
|
{{ a.quantityAvailable }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
<td>£{{ a.unitCost.amount }}</td>
|
<td>£{{ a.unitCost.amount }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
<h4>Pet accommodations</h4>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
o.petStayInCar = 'G',<br>
|
||||||
|
o.petLargeKennel = 'B',<br>
|
||||||
|
o.petSmallKennel = 'K',<br>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre>{{ pet_accommodations | tojson }}</pre>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -11,9 +11,9 @@
|
||||||
<div class="m-3">
|
<div class="m-3">
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
{% for dep, arr in routes %}
|
<li><a href="{{ url_for("outbound1_page") }}">Outbound: 10 March</a>
|
||||||
<li><a href="{{ url_for("show_route", departure_port=ports[dep], arrival_port=ports[arr]) }}">{{ dep }} - {{ arr }}</a></li>
|
<li><a href="{{ url_for("outbound2_page") }}">Outbound: 17 March</a>
|
||||||
{% endfor %}
|
<li><a href="{{ url_for("return_page") }}">Return: 24 March</a>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
{% macro headings() %}
|
{% macro headings() %}
|
||||||
<tr>
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
<th>day</th>
|
<th>day</th>
|
||||||
<th>depart</th>
|
<th>depart</th>
|
||||||
<th>arrive</th>
|
<th>arrive</th>
|
||||||
|
<th>duration</th>
|
||||||
<th>ship</th>
|
<th>ship</th>
|
||||||
<th>economy</th>
|
<th>economy</th>
|
||||||
<th>standard</th>
|
<th>standard</th>
|
||||||
|
@ -17,10 +19,12 @@
|
||||||
{{ headings() }}
|
{{ headings() }}
|
||||||
{% for day in days %}
|
{% for day in days %}
|
||||||
{% set date = parse_date(day.date) %}
|
{% set date = parse_date(day.date) %}
|
||||||
{% for crossing in day.prices %}
|
{% for i in day.prices if i.crossingPrices.sailingId not in ("385413", "384486", "386181", "386191", "388752", "385445", "384550") %}
|
||||||
|
{% set crossing = i.crossingPrices %}
|
||||||
|
|
||||||
{# <tr><td colspan="7">{{ crossing }}</td></tr> #}
|
{# <tr><td colspan="7">{{ crossing }}</td></tr> #}
|
||||||
<tr>
|
<tr>
|
||||||
|
<td class="text-nowrap">{{ crossing.sailingId }}</td>
|
||||||
<td class="text-nowrap text-end">{{ date.strftime("%a, %d %b") }}</td>
|
<td class="text-nowrap text-end">{{ date.strftime("%a, %d %b") }}</td>
|
||||||
<td class="text-nowrap">
|
<td class="text-nowrap">
|
||||||
{{ crossing.departureDateTime.time }}
|
{{ crossing.departureDateTime.time }}
|
||||||
|
@ -28,6 +32,9 @@
|
||||||
<td class="text-nowrap">
|
<td class="text-nowrap">
|
||||||
{{ crossing.arrivalDateTime.time }}
|
{{ crossing.arrivalDateTime.time }}
|
||||||
</td>
|
</td>
|
||||||
|
<td class="text-nowrap">
|
||||||
|
{{ get_duration(crossing.departureDateTime.time, crossing.arrivalDateTime.time, time_delta) }}
|
||||||
|
</td>
|
||||||
<td class="text-nowrap">
|
<td class="text-nowrap">
|
||||||
{{ crossing.shipName }}
|
{{ crossing.shipName }}
|
||||||
</td>
|
</td>
|
||||||
|
@ -46,6 +53,9 @@
|
||||||
£{{ crossing.flexiPrice.amount }}
|
£{{ crossing.flexiPrice.amount }}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ format_pet_options(crossing.petAvailabilities) | join(", ") }}
|
||||||
|
</td>
|
||||||
<td class="text-nowrap">
|
<td class="text-nowrap">
|
||||||
{% if crossing.full %}full |{% endif %}
|
{% if crossing.full %}full |{% endif %}
|
||||||
{% if crossing.isCabinSpaceFull %}no cabin space |{% endif %}
|
{% if crossing.isCabinSpaceFull %}no cabin space |{% endif %}
|
||||||
|
|
|
@ -18,8 +18,6 @@ URL: {{ request.url }}
|
||||||
{{ tb.plaintext | safe }}
|
{{ tb.plaintext | safe }}
|
||||||
{% endset %}
|
{% endset %}
|
||||||
|
|
||||||
<p><a class="btn btn-primary btn-lg" role="button" href="https://github.com/EdwardBetts/dab-mechanic/issues/new?title={{ tb.exception + " " + request.url | urlencode }}&body={{ body | urlencode }}">Submit as an issue on GitHub</a> (requires an account with GitHub)</p>
|
|
||||||
|
|
||||||
<h2 class="traceback">Traceback <em>(most recent call last)</em></h2>
|
<h2 class="traceback">Traceback <em>(most recent call last)</em></h2>
|
||||||
{{ tb.render_summary(include_title=False) | safe }}
|
{{ tb.render_summary(include_title=False) | safe }}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue