Update
This commit is contained in:
parent
c26383c9c0
commit
a5195cba1a
15
ferry/api.py
15
ferry/api.py
|
@ -7,6 +7,9 @@ import requests
|
||||||
from . import Vehicle
|
from . import Vehicle
|
||||||
|
|
||||||
api_root_url = "https://www.brittany-ferries.co.uk/api/ferry/v1/"
|
api_root_url = "https://www.brittany-ferries.co.uk/api/ferry/v1/"
|
||||||
|
ua = "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/109.0"
|
||||||
|
|
||||||
|
headers = {"User-Agent": ua}
|
||||||
|
|
||||||
|
|
||||||
class VehicleDict(TypedDict):
|
class VehicleDict(TypedDict):
|
||||||
|
@ -36,14 +39,16 @@ def get_prices(
|
||||||
from_date: str,
|
from_date: str,
|
||||||
to_date: str,
|
to_date: str,
|
||||||
vehicle: Vehicle,
|
vehicle: Vehicle,
|
||||||
|
adults: int = 2,
|
||||||
|
small_dogs: int = 1,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Call Brittany Ferries API to get details of crossings."""
|
"""Call Brittany Ferries API to get details of crossings."""
|
||||||
url = api_root_url + "crossing/prices"
|
url = api_root_url + "crossing/prices"
|
||||||
|
|
||||||
post_data = {
|
post_data = {
|
||||||
"bookingReference": None,
|
"bookingReference": None,
|
||||||
"pets": {"smallDogs": 1, "largeDogs": 0, "cats": 0},
|
"pets": {"smallDogs": small_dogs, "largeDogs": 0, "cats": 0},
|
||||||
"passengers": {"adults": 2, "children": 0, "infants": 0},
|
"passengers": {"adults": adults, "children": 0, "infants": 0},
|
||||||
"vehicle": vehicle_dict(vehicle),
|
"vehicle": vehicle_dict(vehicle),
|
||||||
"departurePort": departure_port,
|
"departurePort": departure_port,
|
||||||
"arrivalPort": arrival_port,
|
"arrivalPort": arrival_port,
|
||||||
|
@ -53,7 +58,7 @@ def get_prices(
|
||||||
"toDate": f"{to_date}T23:59:59",
|
"toDate": f"{to_date}T23:59:59",
|
||||||
}
|
}
|
||||||
|
|
||||||
r = requests.post(url, json=post_data)
|
r = requests.post(url, json=post_data, headers=headers)
|
||||||
data: dict[str, Any] = r.json()
|
data: dict[str, Any] = r.json()
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
@ -82,5 +87,7 @@ def get_accommodations(
|
||||||
"offerType": "NONE",
|
"offerType": "NONE",
|
||||||
}
|
}
|
||||||
|
|
||||||
json_data: dict[str, Any] = requests.post(url, json=post_data).json()
|
json_data: dict[str, Any] = requests.post(
|
||||||
|
url, json=post_data, headers=headers
|
||||||
|
).json()
|
||||||
return json_data
|
return json_data
|
||||||
|
|
271
main.py
271
main.py
|
@ -6,22 +6,46 @@ import json
|
||||||
import os
|
import os
|
||||||
import os.path
|
import os.path
|
||||||
import re
|
import re
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
from datetime import date, datetime, timedelta
|
from datetime import date, datetime, timedelta
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import dateutil.parser
|
import dateutil.parser
|
||||||
import flask
|
import flask
|
||||||
import pytz
|
import pytz
|
||||||
import werkzeug.exceptions
|
import werkzeug
|
||||||
from werkzeug.debug.tbtools import get_current_traceback
|
from werkzeug.debug.tbtools import DebugTraceback
|
||||||
from werkzeug.wrappers import Response
|
from werkzeug.wrappers import Response
|
||||||
|
|
||||||
import ferry
|
import ferry
|
||||||
from ferry.api import get_accommodations, get_prices
|
from ferry.api import get_accommodations, get_prices
|
||||||
from ferry.read_config import ferry_config, vehicle_from_config
|
from ferry.read_config import ferry_config, vehicle_from_config
|
||||||
|
|
||||||
|
# import werkzeug.exceptions
|
||||||
|
# from werkzeug.debug.tbtools import DebugTraceback
|
||||||
|
# from werkzeug.debug.tbtools import get_current_traceback
|
||||||
|
|
||||||
|
|
||||||
app = flask.Flask(__name__)
|
app = flask.Flask(__name__)
|
||||||
app.debug = False
|
app.debug = True
|
||||||
|
|
||||||
|
routes = {
|
||||||
|
"outbound": [
|
||||||
|
("PORTSMOUTH", "CAEN"),
|
||||||
|
("PORTSMOUTH", "CHERBOURG"),
|
||||||
|
("PORTSMOUTH", "ST MALO"),
|
||||||
|
("PORTSMOUTH", "LE HAVRE"),
|
||||||
|
("POOLE", "CHERBOURG"),
|
||||||
|
],
|
||||||
|
"return": [
|
||||||
|
("CAEN", "PORTSMOUTH"),
|
||||||
|
("CHERBOURG", "PORTSMOUTH"),
|
||||||
|
("ST MALO", "PORTSMOUTH"),
|
||||||
|
("LE HAVRE", "PORTSMOUTH"),
|
||||||
|
("CHERBOURG", "POOLE"),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def cache_location() -> str:
|
def cache_location() -> str:
|
||||||
|
@ -60,14 +84,26 @@ def get_duration(depart: str, arrive: str, time_delta: int) -> str:
|
||||||
|
|
||||||
|
|
||||||
@app.errorhandler(werkzeug.exceptions.InternalServerError)
|
@app.errorhandler(werkzeug.exceptions.InternalServerError)
|
||||||
def exception_handler(e):
|
def exception_handler(e: Exception) -> tuple[str, int]:
|
||||||
tb = get_current_traceback()
|
"""Show error page."""
|
||||||
last_frame = next(frame for frame in reversed(tb.frames) if not frame.is_library)
|
exec_type, exc_value, current_traceback = sys.exc_info()
|
||||||
last_frame_args = inspect.getargs(last_frame.code)
|
assert exc_value
|
||||||
|
tb = DebugTraceback(exc_value)
|
||||||
|
|
||||||
|
summary = tb.render_traceback_html(include_title=False)
|
||||||
|
exc_lines = list(tb._te.format_exception_only())
|
||||||
|
|
||||||
|
frames = [f for f, lineno in traceback.walk_tb(current_traceback)]
|
||||||
|
last_frame = frames[-1]
|
||||||
|
last_frame_args = inspect.getargs(last_frame.f_code)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
flask.render_template(
|
flask.render_template(
|
||||||
"show_error.html",
|
"show_error.html",
|
||||||
tb=tb,
|
plaintext=tb.render_traceback_text(),
|
||||||
|
exception="".join(exc_lines),
|
||||||
|
exception_type=tb._te.exc_type.__name__,
|
||||||
|
summary=summary,
|
||||||
last_frame=last_frame,
|
last_frame=last_frame,
|
||||||
last_frame_args=last_frame_args,
|
last_frame_args=last_frame_args,
|
||||||
),
|
),
|
||||||
|
@ -75,6 +111,23 @@ def exception_handler(e):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# @app.errorhandler(werkzeug.exceptions.InternalServerError)
|
||||||
|
# def exception_handler(e):
|
||||||
|
# # tb = get_current_traceback()
|
||||||
|
# tb = DebugTraceback()
|
||||||
|
# last_frame = next(frame for frame in reversed(tb.frames) if not frame.is_library)
|
||||||
|
# last_frame_args = inspect.getargs(last_frame.code)
|
||||||
|
# return (
|
||||||
|
# flask.render_template(
|
||||||
|
# "show_error.html",
|
||||||
|
# tb=tb,
|
||||||
|
# last_frame=last_frame,
|
||||||
|
# last_frame_args=last_frame_args,
|
||||||
|
# ),
|
||||||
|
# 500,
|
||||||
|
# )
|
||||||
|
|
||||||
|
|
||||||
def parse_date(d: str) -> date:
|
def parse_date(d: str) -> date:
|
||||||
"""Parse a date from a string in ISO format."""
|
"""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()
|
||||||
|
@ -93,6 +146,7 @@ def cabins_url(dep: str, arr: str, crossing: dict[str, Any], ticket_tier: str) -
|
||||||
|
|
||||||
return flask.url_for(
|
return flask.url_for(
|
||||||
"cabins",
|
"cabins",
|
||||||
|
sailing_id=int(crossing["sailingId"]),
|
||||||
departure_port=ferry.ports[dep],
|
departure_port=ferry.ports[dep],
|
||||||
arrival_port=ferry.ports[arr],
|
arrival_port=ferry.ports[arr],
|
||||||
departure_date=utc_dt.strftime("%Y-%m-%dT%H:%M:%S.000Z"),
|
departure_date=utc_dt.strftime("%Y-%m-%dT%H:%M:%S.000Z"),
|
||||||
|
@ -106,7 +160,7 @@ def get_days_until_start() -> int:
|
||||||
return (start - date.today()).days
|
return (start - date.today()).days
|
||||||
|
|
||||||
|
|
||||||
PriceData = list[tuple[str, str, dict[str, Any]]]
|
PriceData = list[tuple[str, str, list[dict[str, Any]]]]
|
||||||
|
|
||||||
|
|
||||||
def check_cache_for_prices(params: str) -> PriceData | None:
|
def check_cache_for_prices(params: str) -> PriceData | None:
|
||||||
|
@ -130,14 +184,16 @@ def check_cache_for_prices(params: str) -> PriceData | None:
|
||||||
|
|
||||||
|
|
||||||
def get_prices_with_cache(
|
def get_prices_with_cache(
|
||||||
name: str,
|
direction: str,
|
||||||
start: str,
|
start: str,
|
||||||
end: str,
|
end: str,
|
||||||
selection: list[tuple[str, str]],
|
selection: list[tuple[str, str]],
|
||||||
refresh: bool = False,
|
refresh: bool = False,
|
||||||
|
adults: int = 2,
|
||||||
|
small_dogs: int = 1,
|
||||||
) -> PriceData:
|
) -> PriceData:
|
||||||
"""Get price data using cache."""
|
"""Get price data using cache."""
|
||||||
params = f"{name}_{start}_{end}"
|
params = f"{direction}_{start}_{end}_{adults}_{small_dogs}"
|
||||||
if not refresh:
|
if not refresh:
|
||||||
data = check_cache_for_prices(params)
|
data = check_cache_for_prices(params)
|
||||||
if data:
|
if data:
|
||||||
|
@ -149,9 +205,15 @@ def get_prices_with_cache(
|
||||||
(
|
(
|
||||||
dep,
|
dep,
|
||||||
arr,
|
arr,
|
||||||
get_prices(ferry.ports[dep], ferry.ports[arr], start, end, vehicle)[
|
get_prices(
|
||||||
"crossings"
|
ferry.ports[dep],
|
||||||
],
|
ferry.ports[arr],
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
vehicle,
|
||||||
|
adults,
|
||||||
|
small_dogs,
|
||||||
|
)["crossings"],
|
||||||
)
|
)
|
||||||
for dep, arr in selection
|
for dep, arr in selection
|
||||||
]
|
]
|
||||||
|
@ -164,18 +226,21 @@ def get_prices_with_cache(
|
||||||
|
|
||||||
def build_outbound(section: str) -> str:
|
def build_outbound(section: str) -> str:
|
||||||
"""Show all routes on one page."""
|
"""Show all routes on one page."""
|
||||||
selection = [
|
|
||||||
("PORTSMOUTH", "CAEN"),
|
|
||||||
("PORTSMOUTH", "CHERBOURG"),
|
|
||||||
("PORTSMOUTH", "ST MALO"),
|
|
||||||
# ("POOLE", "CHERBOURG"),
|
|
||||||
]
|
|
||||||
|
|
||||||
start = ferry_config.get(section, "from")
|
start = ferry_config.get(section, "from")
|
||||||
end = ferry_config.get(section, "to")
|
end = ferry_config.get(section, "to")
|
||||||
refresh = bool(flask.request.args.get("refresh"))
|
refresh = bool(flask.request.args.get("refresh"))
|
||||||
|
|
||||||
all_data = get_prices_with_cache(section, start, end, selection, refresh)
|
adults_str = flask.request.args.get("adults")
|
||||||
|
adults = int(adults_str) if adults_str else 2
|
||||||
|
|
||||||
|
small_dogs_str = flask.request.args.get("small_dogs")
|
||||||
|
small_dogs = int(small_dogs_str) if small_dogs_str else 2
|
||||||
|
|
||||||
|
direction = section[:-1] if section[-1].isdigit() else section
|
||||||
|
|
||||||
|
all_data = get_prices_with_cache(
|
||||||
|
direction, start, end, routes["outbound"], refresh, adults, small_dogs
|
||||||
|
)
|
||||||
|
|
||||||
return flask.render_template(
|
return flask.render_template(
|
||||||
"all_routes.html",
|
"all_routes.html",
|
||||||
|
@ -192,43 +257,23 @@ def build_outbound(section: str) -> str:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/outbound1")
|
def build_return(section: str) -> str:
|
||||||
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")
|
|
||||||
def return_page() -> str:
|
|
||||||
"""Show all routes on one page."""
|
"""Show all routes on one page."""
|
||||||
selection = [
|
start = ferry_config.get(section, "from")
|
||||||
("CAEN", "PORTSMOUTH"),
|
end = ferry_config.get(section, "to")
|
||||||
("CHERBOURG", "PORTSMOUTH"),
|
|
||||||
("ST MALO", "PORTSMOUTH"),
|
|
||||||
# ("CHERBOURG", "POOLE"),
|
|
||||||
]
|
|
||||||
|
|
||||||
start = ferry_config.get("return", "from")
|
|
||||||
end = ferry_config.get("return", "to")
|
|
||||||
refresh = bool(flask.request.args.get("refresh"))
|
refresh = bool(flask.request.args.get("refresh"))
|
||||||
|
|
||||||
all_data = get_prices_with_cache("return", start, end, selection, refresh)
|
direction = section[:-1] if section[-1].isdigit() else section
|
||||||
|
|
||||||
|
adults_str = flask.request.args.get("adults")
|
||||||
|
adults = int(adults_str) if adults_str else 2
|
||||||
|
|
||||||
|
small_dogs_str = flask.request.args.get("small_dogs")
|
||||||
|
small_dogs = int(small_dogs_str) if small_dogs_str else 2
|
||||||
|
|
||||||
|
all_data = get_prices_with_cache(
|
||||||
|
direction, start, end, routes["return"], refresh, adults, small_dogs
|
||||||
|
)
|
||||||
|
|
||||||
return flask.render_template(
|
return flask.render_template(
|
||||||
"all_routes.html",
|
"all_routes.html",
|
||||||
|
@ -245,6 +290,44 @@ def return_page() -> str:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/outbound1")
|
||||||
|
def outbound1_page() -> str:
|
||||||
|
return build_outbound("outbound1")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/outbound2")
|
||||||
|
def outbound2_page() -> str:
|
||||||
|
return build_outbound("outbound2")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/outbound3")
|
||||||
|
def outbound3_page() -> str:
|
||||||
|
return build_outbound("outbound3")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/return1")
|
||||||
|
def return1_page() -> str:
|
||||||
|
return build_return("return1")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/return2")
|
||||||
|
def return2_page() -> str:
|
||||||
|
return build_return("return2")
|
||||||
|
|
||||||
|
|
||||||
|
def format_pet_options(o: dict[str, bool]) -> list[str]:
|
||||||
|
"""Read pet options from API and format for display."""
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
def get_accommodations_with_cache(
|
def get_accommodations_with_cache(
|
||||||
dep: str, arr: str, d: str, ticket_tier: str, refresh: bool = False
|
dep: str, arr: str, d: str, ticket_tier: str, refresh: bool = False
|
||||||
) -> dict[str, list[dict[str, Any]]]:
|
) -> dict[str, list[dict[str, Any]]]:
|
||||||
|
@ -274,10 +357,77 @@ def get_accommodations_with_cache(
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
@app.route("/cabins/<departure_port>/<arrival_port>/<departure_date>/<ticket_tier>")
|
def get_start_end(section: str) -> tuple[str, str]:
|
||||||
|
start = ferry_config.get(section, "from")
|
||||||
|
end = ferry_config.get(section, "to")
|
||||||
|
return start, end
|
||||||
|
|
||||||
|
|
||||||
|
def get_return_section(departure_date: str) -> tuple[str, str, str]:
|
||||||
|
section = "return1"
|
||||||
|
start, end = get_start_end(section)
|
||||||
|
dep = departure_date[:10]
|
||||||
|
if start <= dep <= end:
|
||||||
|
return section, start, end
|
||||||
|
|
||||||
|
section = "return2"
|
||||||
|
start, end = get_start_end(section)
|
||||||
|
if start <= dep <= end:
|
||||||
|
return section, start, end
|
||||||
|
|
||||||
|
|
||||||
|
def get_outbound_section(departure_date: str) -> tuple[str, str, str]:
|
||||||
|
section = "outbound1"
|
||||||
|
start, end = get_start_end(section)
|
||||||
|
dep = departure_date[:10]
|
||||||
|
if start <= dep <= end:
|
||||||
|
return section, start, end
|
||||||
|
|
||||||
|
section = "outbound2"
|
||||||
|
start, end = get_start_end(section)
|
||||||
|
if start <= dep <= end:
|
||||||
|
return section, start, end
|
||||||
|
|
||||||
|
section = "outbound3"
|
||||||
|
start, end = get_start_end(section)
|
||||||
|
if start <= dep <= end:
|
||||||
|
return section, start, end
|
||||||
|
|
||||||
|
|
||||||
|
def lookup_sailing_id(prices: PriceData, sailing_id: int) -> dict[str, Any] | None:
|
||||||
|
for dep, arr, price_list in prices:
|
||||||
|
for i in price_list:
|
||||||
|
for j in i["prices"]:
|
||||||
|
crossing: dict[str, Any] = j["crossingPrices"]
|
||||||
|
if int(crossing["sailingId"]) == sailing_id:
|
||||||
|
return crossing
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@app.route(
|
||||||
|
"/cabins/<int:sailing_id>/<departure_port>/<arrival_port>/<departure_date>/<ticket_tier>"
|
||||||
|
)
|
||||||
def cabins(
|
def cabins(
|
||||||
departure_port: str, arrival_port: str, departure_date: str, ticket_tier: str
|
sailing_id: int,
|
||||||
|
departure_port: str,
|
||||||
|
arrival_port: str,
|
||||||
|
departure_date: str,
|
||||||
|
ticket_tier: str,
|
||||||
) -> str:
|
) -> str:
|
||||||
|
"""Get details of cabins."""
|
||||||
|
direction = {"GB": "outbound", "FR": "return"}[departure_port[:2]]
|
||||||
|
if direction == "return":
|
||||||
|
section, start, end = get_return_section(departure_date[:10])
|
||||||
|
start, end = get_start_end(section)
|
||||||
|
time_delta = 60
|
||||||
|
else:
|
||||||
|
section, start, end = get_outbound_section(departure_date[:10])
|
||||||
|
time_delta = -60
|
||||||
|
|
||||||
|
prices = get_prices_with_cache(direction, start, end, routes[direction])
|
||||||
|
crossing = lookup_sailing_id(prices, sailing_id)
|
||||||
|
|
||||||
cabin_data = get_accommodations_with_cache(
|
cabin_data = get_accommodations_with_cache(
|
||||||
departure_port, arrival_port, departure_date, ticket_tier
|
departure_port, arrival_port, departure_date, ticket_tier
|
||||||
)
|
)
|
||||||
|
@ -299,6 +449,11 @@ def cabins(
|
||||||
ticket_tier=ticket_tier,
|
ticket_tier=ticket_tier,
|
||||||
accommodations=accommodations,
|
accommodations=accommodations,
|
||||||
pet_accommodations=cabin_data["petAccommodations"],
|
pet_accommodations=cabin_data["petAccommodations"],
|
||||||
|
crossing=crossing,
|
||||||
|
get_duration=get_duration,
|
||||||
|
time_delta=time_delta,
|
||||||
|
format_pet_options=format_pet_options,
|
||||||
|
section=section,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -30,12 +30,18 @@ a:link {
|
||||||
<body>
|
<body>
|
||||||
<div class="m-3">
|
<div class="m-3">
|
||||||
|
|
||||||
<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>{{ days_until_start }} days / {{ (days_until_start / 7) | int }} weeks / {{ "{:.1f}".format(days_until_start / 30.5) }} months until start of Dodainville week: Friday 5 May 2022</p>
|
||||||
|
|
||||||
{#
|
{#
|
||||||
<p><a href="{{ url_for(other + "_page") }}">{{ other }}</a></p>
|
<p><a href="{{ url_for(other + "_page") }}">{{ other }}</a></p>
|
||||||
#}
|
#}
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li><a href="?adults=2&small_dogs=1">2 adults and a dog</a></li>
|
||||||
|
<li><a href="?adults=2&small_dogs=0">2 adults</a></li>
|
||||||
|
<li><a href="?adults=1&small_dogs=0">1 adult</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
{% if extra_routes %}
|
{% if extra_routes %}
|
||||||
<ul>
|
<ul>
|
||||||
{% for dep, arr in extra_routes %}
|
{% for dep, arr in extra_routes %}
|
||||||
|
|
|
@ -34,13 +34,15 @@ a:link {
|
||||||
|
|
||||||
<p>{{ departure_date.strftime("%A, %d %B %Y %H:%M UTC") }} {{ ticket_tier }}</p>
|
<p>{{ departure_date.strftime("%A, %d %B %Y %H:%M UTC") }} {{ ticket_tier }}</p>
|
||||||
|
|
||||||
|
<p><a href="{{ url_for(section + "_page") }}">back to sailings</a></p>
|
||||||
|
|
||||||
<table class="table w-auto">
|
<table class="table w-auto">
|
||||||
<tr>
|
<tr>
|
||||||
<th>code</th>
|
<th>code</th>
|
||||||
<th>description</th>
|
<th>description</th>
|
||||||
<th>births</th>
|
<th>births</th>
|
||||||
<th>quantity<br/>available</th>
|
<th>quantity<br/>available</th>
|
||||||
<th>price</th>
|
<th class="text-end">price</th>
|
||||||
</tr>
|
</tr>
|
||||||
{% for a in accommodations if a.quantityAvailable > 0 %}
|
{% for a in accommodations if a.quantityAvailable > 0 %}
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -54,7 +56,7 @@ a:link {
|
||||||
{{ a.quantityAvailable }}
|
{{ a.quantityAvailable }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>£{{ a.unitCost.amount }}</td>
|
<td class="text-end">£{{ a.unitCost.amount }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
|
@ -69,6 +71,19 @@ a:link {
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
<h4>Sailing</h4>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>sailing ID: {{ crossing.sailingId }}</li>
|
||||||
|
<li>depart: {{ crossing.departureDateTime.time }}</li>
|
||||||
|
<li>arrive: {{ crossing.arrivalDateTime.time }}</li>
|
||||||
|
<li>duration: {{ get_duration(crossing.departureDateTime.time, crossing.arrivalDateTime.time, time_delta) }}</li>
|
||||||
|
<li>ship: {{ crossing.shipName }}</li>
|
||||||
|
<li>economy price: £{{ crossing.economyPrice.amount }}</li>
|
||||||
|
<li>standard price: £{{ crossing.standardPrice.amount }}</li>
|
||||||
|
<li>flexi price: £{{ crossing.flexiPrice.amount }}</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -11,9 +11,10 @@
|
||||||
<div class="m-3">
|
<div class="m-3">
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="{{ url_for("outbound1_page") }}">Outbound: 10 March</a>
|
<li><a href="{{ url_for("outbound1_page") }}">Outbound: 5 May</a>
|
||||||
<li><a href="{{ url_for("outbound2_page") }}">Outbound: 17 March</a>
|
<li><a href="{{ url_for("return1_page") }}">Return: 12 May</a>
|
||||||
<li><a href="{{ url_for("return_page") }}">Return: 24 March</a>
|
<li><a href="{{ url_for("outbound3_page") }}">Outbound: 29 September</a>
|
||||||
|
<li><a href="{{ url_for("return2_page") }}">Return: 6 October</a>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -39,9 +39,13 @@
|
||||||
{{ crossing.shipName }}
|
{{ crossing.shipName }}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-nowrap">
|
<td class="text-nowrap">
|
||||||
|
{% if crossing.economyPrice %}
|
||||||
<a href="{{ cabins_url(dep, arr, crossing, "ECONOMY") }}">
|
<a href="{{ cabins_url(dep, arr, crossing, "ECONOMY") }}">
|
||||||
£{{ crossing.economyPrice.amount }}
|
£{{ crossing.economyPrice.amount }}
|
||||||
</a>
|
</a>
|
||||||
|
{% else %}
|
||||||
|
n/a
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-nowrap">
|
<td class="text-nowrap">
|
||||||
<a href="{{ cabins_url(dep, arr, crossing, "STANDARD") }}">
|
<a href="{{ cabins_url(dep, arr, crossing, "STANDARD") }}">
|
||||||
|
@ -53,9 +57,11 @@
|
||||||
£{{ crossing.flexiPrice.amount }}
|
£{{ crossing.flexiPrice.amount }}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
{% if crossing.petAvailabilities %}
|
||||||
<td>
|
<td>
|
||||||
{{ format_pet_options(crossing.petAvailabilities) | join(", ") }}
|
{{ format_pet_options(crossing.petAvailabilities) | join(", ") }}
|
||||||
</td>
|
</td>
|
||||||
|
{% endif %}
|
||||||
<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 %}
|
||||||
|
|
|
@ -7,9 +7,9 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="p-2">
|
<div class="p-2">
|
||||||
|
|
||||||
<h1>Software error: {{ tb.exception_type }}</h1>
|
<h1>Software error: {{ exception_type }}</h1>
|
||||||
<div>
|
<div>
|
||||||
<pre>{{ tb.exception }}</pre>
|
<pre>{{ exception }}</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% set body %}
|
{% set body %}
|
||||||
|
@ -19,10 +19,12 @@ URL: {{ request.url }}
|
||||||
{% endset %}
|
{% endset %}
|
||||||
|
|
||||||
<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 }}
|
{{ summary | safe }}
|
||||||
|
|
||||||
|
{#
|
||||||
<p>Error in function "{{ last_frame.function_name }}": {{ last_frame_args | pprint }}</p>
|
<p>Error in function "{{ last_frame.function_name }}": {{ last_frame_args | pprint }}</p>
|
||||||
<pre>{{ last_frame.locals | pprint }}</pre>
|
<pre>{{ last_frame.locals | pprint }}</pre>
|
||||||
|
#}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
Loading…
Reference in a new issue