Reformat with black
This commit is contained in:
parent
588ffdadfb
commit
4f7ffc0e3d
18 changed files with 204 additions and 131 deletions
15
app/app.py
15
app/app.py
|
@ -92,8 +92,7 @@ def register_plugins(app: Flask) -> Manager:
|
||||||
|
|
||||||
# Make cookies more secure
|
# Make cookies more secure
|
||||||
app.config.update(
|
app.config.update(
|
||||||
SESSION_COOKIE_HTTPONLY=True,
|
SESSION_COOKIE_HTTPONLY=True, SESSION_COOKIE_SAMESITE="Lax",
|
||||||
SESSION_COOKIE_SAMESITE='Lax',
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if not app.debug:
|
if not app.debug:
|
||||||
|
@ -140,8 +139,9 @@ def add_template_filters(app: Flask) -> None:
|
||||||
"Add functions which can be used in the templates"
|
"Add functions which can be used in the templates"
|
||||||
# pylint: disable=W0612
|
# pylint: disable=W0612
|
||||||
@app.template_filter("countdown")
|
@app.template_filter("countdown")
|
||||||
def countdown(value, only_positive: bool = True,
|
def countdown(
|
||||||
show_text: bool = True, reload: bool = True) -> str:
|
value, only_positive: bool = True, show_text: bool = True, reload: bool = True
|
||||||
|
) -> str:
|
||||||
delta = int(value.timestamp() - datetime.now().timestamp())
|
delta = int(value.timestamp() - datetime.now().timestamp())
|
||||||
if delta < 0 and only_positive:
|
if delta < 0 and only_positive:
|
||||||
text = "closed"
|
text = "closed"
|
||||||
|
@ -157,8 +157,11 @@ def add_template_filters(app: Flask) -> None:
|
||||||
|
|
||||||
reload_str = "yes" if reload else "no"
|
reload_str = "yes" if reload else "no"
|
||||||
|
|
||||||
return Markup(f"<span class='time' data-seconds='{delta}' data-reload='{reload_str}'>" +
|
return Markup(
|
||||||
text + "</span>")
|
f"<span class='time' data-seconds='{delta}' data-reload='{reload_str}'>"
|
||||||
|
+ text
|
||||||
|
+ "</span>"
|
||||||
|
)
|
||||||
|
|
||||||
@app.template_filter("year")
|
@app.template_filter("year")
|
||||||
def current_year(_value: typing.Any) -> str:
|
def current_year(_value: typing.Any) -> str:
|
||||||
|
|
|
@ -36,9 +36,9 @@ class FatOrder(Order, FatModel):
|
||||||
@classmethod
|
@classmethod
|
||||||
def items_per_order(cls):
|
def items_per_order(cls):
|
||||||
return (
|
return (
|
||||||
Order.query.join(OrderItem).group_by(Order.id)
|
Order.query.join(OrderItem)
|
||||||
.with_entities(Order.id,
|
.group_by(Order.id)
|
||||||
func.count(OrderItem.user_id).label("total"))
|
.with_entities(Order.id, func.count(OrderItem.user_id).label("total"))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
16
app/forms.py
16
app/forms.py
|
@ -6,8 +6,15 @@ from typing import Optional
|
||||||
from flask import session, request
|
from flask import session, request
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from flask_wtf import FlaskForm as Form
|
from flask_wtf import FlaskForm as Form
|
||||||
from wtforms import (DateTimeField, SelectField, SelectMultipleField, StringField, SubmitField,
|
from wtforms import (
|
||||||
FieldList, validators)
|
DateTimeField,
|
||||||
|
SelectField,
|
||||||
|
SelectMultipleField,
|
||||||
|
StringField,
|
||||||
|
SubmitField,
|
||||||
|
FieldList,
|
||||||
|
validators,
|
||||||
|
)
|
||||||
|
|
||||||
from utils import euro_string, price_range_string
|
from utils import euro_string, price_range_string
|
||||||
from hlds.definitions import location_definitions
|
from hlds.definitions import location_definitions
|
||||||
|
@ -39,9 +46,7 @@ class OrderForm(Form):
|
||||||
(0, None),
|
(0, None),
|
||||||
(current_user.id, current_user.username),
|
(current_user.id, current_user.username),
|
||||||
]
|
]
|
||||||
self.location_id.choices = [
|
self.location_id.choices = [(l.id, l.name) for l in location_definitions]
|
||||||
(l.id, l.name) for l in location_definitions
|
|
||||||
]
|
|
||||||
if self.stoptime.data is None:
|
if self.stoptime.data is None:
|
||||||
self.stoptime.data = datetime.now() + timedelta(hours=1)
|
self.stoptime.data = datetime.now() + timedelta(hours=1)
|
||||||
|
|
||||||
|
@ -67,6 +72,7 @@ class AnonOrderItemForm(OrderItemForm):
|
||||||
Class which defines the form for a new Item in an Order
|
Class which defines the form for a new Item in an Order
|
||||||
For Users who aren't logged in
|
For Users who aren't logged in
|
||||||
"""
|
"""
|
||||||
|
|
||||||
user_name = StringField("Name", validators=[validators.required()])
|
user_name = StringField("Name", validators=[validators.required()])
|
||||||
|
|
||||||
def populate(self, location: Location) -> None:
|
def populate(self, location: Location) -> None:
|
||||||
|
|
|
@ -6,11 +6,9 @@ from utils import euro_string, first
|
||||||
|
|
||||||
|
|
||||||
def _format_tags(tags: Iterable[str]) -> str:
|
def _format_tags(tags: Iterable[str]) -> str:
|
||||||
return (
|
return " :: {}".format(" ".join(["{" + tag + "}" for tag in tags])) \
|
||||||
" :: {}".format(" ".join(["{" + tag + "}" for tag in tags]))
|
if tags \
|
||||||
if tags else
|
else ""
|
||||||
""
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _format_price(price: int) -> str:
|
def _format_price(price: int) -> str:
|
||||||
|
@ -35,7 +33,7 @@ class Option:
|
||||||
self,
|
self,
|
||||||
" -- {}".format(self.description) if self.description else "",
|
" -- {}".format(self.description) if self.description else "",
|
||||||
_format_tags(self.tags),
|
_format_tags(self.tags),
|
||||||
_format_price(self.price)
|
_format_price(self.price),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -51,7 +49,7 @@ class Choice:
|
||||||
return "{0.id}: {0.name}{1}\n\t\t{2}".format(
|
return "{0.id}: {0.name}{1}\n\t\t{2}".format(
|
||||||
self,
|
self,
|
||||||
" -- {}".format(self.description) if self.description else "",
|
" -- {}".format(self.description) if self.description else "",
|
||||||
"\n\t\t".join(map(str, self.options))
|
"\n\t\t".join(map(str, self.options)),
|
||||||
)
|
)
|
||||||
|
|
||||||
def option_by_id(self, option_id: str) -> Optional[Option]:
|
def option_by_id(self, option_id: str) -> Optional[Option]:
|
||||||
|
@ -75,12 +73,14 @@ class Dish:
|
||||||
" -- {}".format(self.description) if self.description else "",
|
" -- {}".format(self.description) if self.description else "",
|
||||||
_format_tags(self.tags),
|
_format_tags(self.tags),
|
||||||
_format_price(self.price),
|
_format_price(self.price),
|
||||||
"\n\t".join(map(_format_type_and_choice, self.choices))
|
"\n\t".join(map(_format_type_and_choice, self.choices)),
|
||||||
)
|
)
|
||||||
|
|
||||||
def price_range(self) -> Tuple[int, int]:
|
def price_range(self) -> Tuple[int, int]:
|
||||||
return (self.price + self._sum_f_option_prices(min),
|
return (
|
||||||
self.price + self._sum_f_option_prices(max))
|
self.price + self._sum_f_option_prices(min),
|
||||||
|
self.price + self._sum_f_option_prices(max),
|
||||||
|
)
|
||||||
|
|
||||||
def _sum_f_option_prices(self, f):
|
def _sum_f_option_prices(self, f):
|
||||||
return sum(
|
return sum(
|
||||||
|
@ -91,7 +91,9 @@ class Dish:
|
||||||
|
|
||||||
|
|
||||||
class Location:
|
class Location:
|
||||||
def __init__(self, id_, *, name, dishes, osm=None, address=None, telephone=None, website=None):
|
def __init__(
|
||||||
|
self, id_, *, name, dishes, osm=None, address=None, telephone=None, website=None
|
||||||
|
):
|
||||||
self.id: str = id_
|
self.id: str = id_
|
||||||
self.name: str = name
|
self.name: str = name
|
||||||
self.osm: Optional[str] = osm
|
self.osm: Optional[str] = osm
|
||||||
|
@ -114,11 +116,15 @@ class Location:
|
||||||
"{2}"
|
"{2}"
|
||||||
).format(
|
).format(
|
||||||
self,
|
self,
|
||||||
"".join("\n\t{} {}".format(k, v) for k, v in (
|
"".join(
|
||||||
|
"\n\t{} {}".format(k, v)
|
||||||
|
for k, v in (
|
||||||
("osm", self.osm),
|
("osm", self.osm),
|
||||||
("address", self.address),
|
("address", self.address),
|
||||||
("telephone", self.telephone),
|
("telephone", self.telephone),
|
||||||
("website", self.website),
|
("website", self.website),
|
||||||
) if v is not None),
|
)
|
||||||
"\n".join(map(str, self.dishes))
|
if v is not None
|
||||||
|
),
|
||||||
|
"\n".join(map(str, self.dishes)),
|
||||||
)
|
)
|
||||||
|
|
|
@ -24,7 +24,9 @@ def filter_instance(cls, iterable):
|
||||||
# pylint: disable=no-self-use
|
# pylint: disable=no-self-use
|
||||||
class HldsSemanticActions:
|
class HldsSemanticActions:
|
||||||
def location(self, ast) -> Location:
|
def location(self, ast) -> Location:
|
||||||
choices = {choice.id: choice for choice in filter_instance(Choice, ast["items_"])}
|
choices = {
|
||||||
|
choice.id: choice for choice in filter_instance(Choice, ast["items_"])
|
||||||
|
}
|
||||||
dishes: Iterable[Dish] = filter_instance(Dish, ast["items_"])
|
dishes: Iterable[Dish] = filter_instance(Dish, ast["items_"])
|
||||||
for dish in dishes:
|
for dish in dishes:
|
||||||
for i, choice in enumerate(dish.choices):
|
for i, choice in enumerate(dish.choices):
|
||||||
|
@ -32,7 +34,9 @@ class HldsSemanticActions:
|
||||||
dish.choices[i] = (dish.choices[i][0], deepcopy(choices[choice[1]]))
|
dish.choices[i] = (dish.choices[i][0], deepcopy(choices[choice[1]]))
|
||||||
|
|
||||||
# Move the base price to the first single_choice if the dish has a fixed price
|
# Move the base price to the first single_choice if the dish has a fixed price
|
||||||
first_single_choice = first(c[1] for c in dish.choices if c[0] == "single_choice")
|
first_single_choice = first(
|
||||||
|
c[1] for c in dish.choices if c[0] == "single_choice"
|
||||||
|
)
|
||||||
price_range = dish.price_range()
|
price_range = dish.price_range()
|
||||||
if dish.price and price_range[0] != price_range[1] and first_single_choice:
|
if dish.price and price_range[0] != price_range[1] and first_single_choice:
|
||||||
for option in first_single_choice.options:
|
for option in first_single_choice.options:
|
||||||
|
@ -41,7 +45,6 @@ class HldsSemanticActions:
|
||||||
|
|
||||||
attributes = {att["key"]: att["value"] for att in ast["attributes"]}
|
attributes = {att["key"]: att["value"] for att in ast["attributes"]}
|
||||||
|
|
||||||
|
|
||||||
return Location(
|
return Location(
|
||||||
ast["id"],
|
ast["id"],
|
||||||
name=ast["name"],
|
name=ast["name"],
|
||||||
|
@ -64,7 +67,9 @@ class HldsSemanticActions:
|
||||||
|
|
||||||
def choice_block(self, ast) -> Choice:
|
def choice_block(self, ast) -> Choice:
|
||||||
if ast["price"] or ast["tags"]:
|
if ast["price"] or ast["tags"]:
|
||||||
raise SemanticError("Choice blocks cannot have price or tags, put them on each of its options instead")
|
raise SemanticError(
|
||||||
|
"Choice blocks cannot have price or tags, put them on each of its options instead"
|
||||||
|
)
|
||||||
|
|
||||||
return Choice(
|
return Choice(
|
||||||
ast["id"],
|
ast["id"],
|
||||||
|
@ -76,8 +81,8 @@ class HldsSemanticActions:
|
||||||
def indent_choice_block(self, ast) -> Tuple[str, Union[Choice, AST]]:
|
def indent_choice_block(self, ast) -> Tuple[str, Union[Choice, AST]]:
|
||||||
return (
|
return (
|
||||||
(ast["type"], self.choice_block(ast))
|
(ast["type"], self.choice_block(ast))
|
||||||
if ast["kind"] == "declaration" else
|
if ast["kind"] == "declaration"
|
||||||
(ast["type"], ast["id"])
|
else (ast["type"], ast["id"])
|
||||||
)
|
)
|
||||||
|
|
||||||
def indent_choice_entry(self, ast) -> Option:
|
def indent_choice_entry(self, ast) -> Option:
|
||||||
|
@ -92,18 +97,18 @@ class HldsSemanticActions:
|
||||||
noindent_choice_entry = indent_choice_entry
|
noindent_choice_entry = indent_choice_entry
|
||||||
|
|
||||||
def price(self, ast) -> int:
|
def price(self, ast) -> int:
|
||||||
return (
|
return 100 * int(ast["value_unit"]) + (
|
||||||
100 * int(ast["value_unit"]) +
|
0
|
||||||
(
|
if not ast["value_cents"]
|
||||||
0 if not ast["value_cents"] else
|
else 10 * int(ast["value_cents"])
|
||||||
10 * int(ast["value_cents"]) if len(ast["value_cents"]) == 1 else
|
if len(ast["value_cents"]) == 1
|
||||||
int(ast["value_cents"])
|
else int(ast["value_cents"])
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def _default(self, ast):
|
def _default(self, ast):
|
||||||
return ast
|
return ast
|
||||||
|
|
||||||
|
|
||||||
SEMANTICS = HldsSemanticActions()
|
SEMANTICS = HldsSemanticActions()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ from __future__ import with_statement
|
||||||
from logging.config import fileConfig
|
from logging.config import fileConfig
|
||||||
|
|
||||||
from alembic import context
|
from alembic import context
|
||||||
|
|
||||||
# add your model's MetaData object here
|
# add your model's MetaData object here
|
||||||
# for 'autogenerate' support
|
# for 'autogenerate' support
|
||||||
# from myapp import mymodel
|
# from myapp import mymodel
|
||||||
|
|
|
@ -22,7 +22,9 @@ class Order(db.Model):
|
||||||
|
|
||||||
def __getattr__(self, name):
|
def __getattr__(self, name):
|
||||||
if name == "location":
|
if name == "location":
|
||||||
return first(filter(lambda l: l.id == self.location_id, location_definitions))
|
return first(
|
||||||
|
filter(lambda l: l.id == self.location_id, location_definitions)
|
||||||
|
)
|
||||||
raise AttributeError()
|
raise AttributeError()
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
|
@ -37,7 +39,9 @@ class Order(db.Model):
|
||||||
Update the location name from the HLDS definition.
|
Update the location name from the HLDS definition.
|
||||||
User should commit after running this to make the change persistent.
|
User should commit after running this to make the change persistent.
|
||||||
"""
|
"""
|
||||||
assert self.location_id, "location_id must be configured before updating from HLDS"
|
assert (
|
||||||
|
self.location_id
|
||||||
|
), "location_id must be configured before updating from HLDS"
|
||||||
self.location_name = self.location.name
|
self.location_name = self.location.name
|
||||||
|
|
||||||
def group_by_user(self) -> typing.Dict[str, typing.Any]:
|
def group_by_user(self) -> typing.Dict[str, typing.Any]:
|
||||||
|
@ -46,17 +50,16 @@ class Order(db.Model):
|
||||||
for item in self.items:
|
for item in self.items:
|
||||||
user = group.get(item.get_name(), dict())
|
user = group.get(item.get_name(), dict())
|
||||||
user["total"] = user.get("total", 0) + item.price
|
user["total"] = user.get("total", 0) + item.price
|
||||||
user["to_pay"] = (
|
user["to_pay"] = user.get("to_pay", 0) + item.price if not item.paid else 0
|
||||||
user.get("to_pay", 0) +
|
|
||||||
item.price if not item.paid else 0
|
|
||||||
)
|
|
||||||
user["paid"] = user.get("paid", True) and item.paid
|
user["paid"] = user.get("paid", True) and item.paid
|
||||||
user["dishes"] = user.get("dishes", []) + [item.dish_name]
|
user["dishes"] = user.get("dishes", []) + [item.dish_name]
|
||||||
group[str(item.get_name())] = user
|
group[str(item.get_name())] = user
|
||||||
|
|
||||||
return group
|
return group
|
||||||
|
|
||||||
def group_by_dish(self, sort_comments=False) -> typing.Dict[str, typing.Dict[str, typing.Any]]:
|
def group_by_dish(
|
||||||
|
self, sort_comments=False
|
||||||
|
) -> typing.Dict[str, typing.Dict[str, typing.Any]]:
|
||||||
"Group items of an Order by dish"
|
"Group items of an Order by dish"
|
||||||
group: typing.Dict[str, typing.Dict[str, typing.Any]] = dict()
|
group: typing.Dict[str, typing.Dict[str, typing.Any]] = dict()
|
||||||
for item in self.items:
|
for item in self.items:
|
||||||
|
|
|
@ -17,9 +17,7 @@ class OrderItem(db.Model):
|
||||||
dish_id = db.Column(db.String(64), nullable=True)
|
dish_id = db.Column(db.String(64), nullable=True)
|
||||||
dish_name = db.Column(db.String(120), nullable=True)
|
dish_name = db.Column(db.String(120), nullable=True)
|
||||||
price = db.Column(db.Integer, nullable=True)
|
price = db.Column(db.Integer, nullable=True)
|
||||||
paid = db.Column(
|
paid = db.Column(db.Boolean, default=False, nullable=True)
|
||||||
db.Boolean, default=False, nullable=True
|
|
||||||
)
|
|
||||||
comment = db.Column(db.Text(), nullable=True)
|
comment = db.Column(db.Text(), nullable=True)
|
||||||
hlds_data_version = db.Column(db.String(40), nullable=True)
|
hlds_data_version = db.Column(db.String(40), nullable=True)
|
||||||
|
|
||||||
|
@ -27,8 +25,12 @@ class OrderItem(db.Model):
|
||||||
|
|
||||||
def __getattr__(self, name):
|
def __getattr__(self, name):
|
||||||
if name == "dish":
|
if name == "dish":
|
||||||
location_id = Order.query.filter(Order.id == self.order_id).first().location_id
|
location_id = (
|
||||||
location = first(filter(lambda l: l.id == location_id, location_definitions))
|
Order.query.filter(Order.id == self.order_id).first().location_id
|
||||||
|
)
|
||||||
|
location = first(
|
||||||
|
filter(lambda l: l.id == location_id, location_definitions)
|
||||||
|
)
|
||||||
if location:
|
if location:
|
||||||
return first(filter(lambda d: d.id == self.dish_id, location.dishes))
|
return first(filter(lambda d: d.id == self.dish_id, location.dishes))
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -7,7 +7,9 @@ from .orderitem import OrderItem
|
||||||
class OrderItemChoice(db.Model):
|
class OrderItemChoice(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
choice_id = db.Column(db.String(64), nullable=True)
|
choice_id = db.Column(db.String(64), nullable=True)
|
||||||
order_item_id = db.Column(db.Integer, db.ForeignKey("order_item.id"), nullable=False)
|
order_item_id = db.Column(
|
||||||
|
db.Integer, db.ForeignKey("order_item.id"), nullable=False
|
||||||
|
)
|
||||||
kind = db.Column(db.String(1), nullable=False)
|
kind = db.Column(db.String(1), nullable=False)
|
||||||
name = db.Column(db.String(120), nullable=True)
|
name = db.Column(db.String(120), nullable=True)
|
||||||
value = db.Column(db.String(120), nullable=True)
|
value = db.Column(db.String(120), nullable=True)
|
||||||
|
|
|
@ -35,8 +35,7 @@ def post_order_to_webhook(order: Order) -> None:
|
||||||
"Function that sends the notification for the order"
|
"Function that sends the notification for the order"
|
||||||
message = webhook_text(order)
|
message = webhook_text(order)
|
||||||
if message:
|
if message:
|
||||||
webhookthread = WebhookSenderThread(
|
webhookthread = WebhookSenderThread(message, app.config["SLACK_WEBHOOK"])
|
||||||
message, app.config["SLACK_WEBHOOK"])
|
|
||||||
webhookthread.start()
|
webhookthread.start()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@ def main(filenames):
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
args = sys.argv[1:]
|
args = sys.argv[1:]
|
||||||
if "-h" in args or "--help" in args:
|
if "-h" in args or "--help" in args:
|
||||||
print(USAGE.format(sys.argv[0]), file=sys.stderr)
|
print(USAGE.format(sys.argv[0]), file=sys.stderr)
|
||||||
|
|
|
@ -10,6 +10,7 @@ if sys.executable != INTERP:
|
||||||
|
|
||||||
sys.path.append(os.getcwd())
|
sys.path.append(os.getcwd())
|
||||||
from app import create_app
|
from app import create_app
|
||||||
|
|
||||||
application = create_app().app
|
application = create_app().app
|
||||||
|
|
||||||
# For running on the server with passenger etc
|
# For running on the server with passenger etc
|
||||||
|
|
|
@ -9,10 +9,13 @@ def euro_string(value: int) -> str:
|
||||||
"""
|
"""
|
||||||
return "€ {}.{:02}".format(*divmod(value, 100))
|
return "€ {}.{:02}".format(*divmod(value, 100))
|
||||||
|
|
||||||
|
|
||||||
def price_range_string(price_range, include_upper=False):
|
def price_range_string(price_range, include_upper=False):
|
||||||
if price_range[0] == price_range[1]:
|
if price_range[0] == price_range[1]:
|
||||||
return euro_string(price_range[0])
|
return euro_string(price_range[0])
|
||||||
return ("{}—{}" if include_upper else "from {}").format(*map(euro_string, price_range))
|
return ("{}—{}" if include_upper else "from {}").format(
|
||||||
|
*map(euro_string, price_range)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def first(iterable: Iterable, default=None):
|
def first(iterable: Iterable, default=None):
|
||||||
|
|
|
@ -16,6 +16,7 @@ from utils import first
|
||||||
from hlds.definitions import location_definitions
|
from hlds.definitions import location_definitions
|
||||||
from hlds.models import Location
|
from hlds.models import Location
|
||||||
from models import Order
|
from models import Order
|
||||||
|
|
||||||
# import views
|
# import views
|
||||||
from views.order import get_orders
|
from views.order import get_orders
|
||||||
|
|
||||||
|
@ -43,7 +44,7 @@ def get_css_dict(css_path):
|
||||||
|
|
||||||
# Open the YAML file with all the themes.
|
# Open the YAML file with all the themes.
|
||||||
path = os.path.join(app.root_path, "views/themes.yml")
|
path = os.path.join(app.root_path, "views/themes.yml")
|
||||||
with open(path, 'r') as stream:
|
with open(path, "r") as stream:
|
||||||
data = yaml.safe_load(stream)
|
data = yaml.safe_load(stream)
|
||||||
# Build a dictionary from the YAML file with all the themes and their attributes.
|
# Build a dictionary from the YAML file with all the themes and their attributes.
|
||||||
themes = {}
|
themes = {}
|
||||||
|
@ -57,46 +58,46 @@ def get_css_dict(css_path):
|
||||||
|
|
||||||
# Check each theme in the dictionary and return the first one that is "correct"
|
# Check each theme in the dictionary and return the first one that is "correct"
|
||||||
for key, theme in themes.items():
|
for key, theme in themes.items():
|
||||||
if theme['type'] == 'static-date':
|
if theme["type"] == "static-date":
|
||||||
start_day, start_month = theme['start'].split('/')
|
start_day, start_month = theme["start"].split("/")
|
||||||
start_date = datetime(year=current_year, day=int(
|
start_date = datetime(year=current_year, day=int(start_day), month=int(start_month))
|
||||||
start_day), month=int(start_month))
|
|
||||||
|
|
||||||
end_day, end_month = theme['end'].split('/')
|
end_day, end_month = theme["end"].split("/")
|
||||||
if int(start_month) > int(end_month):
|
if int(start_month) > int(end_month):
|
||||||
current_year += 1
|
current_year += 1
|
||||||
end_date = datetime(
|
end_date = datetime(year=current_year, day=int(end_day), month=int(end_month))
|
||||||
year=current_year, day=int(end_day), month=int(end_month))
|
|
||||||
|
|
||||||
if start_date <= current_date <= end_date:
|
if start_date <= current_date <= end_date:
|
||||||
path = os.path.join(app.root_path, css_path, theme['file'])
|
path = os.path.join(app.root_path, css_path, theme["file"])
|
||||||
themes_dict[key] = path
|
themes_dict[key] = path
|
||||||
themes_dict['darkmode'] = os.path.join(
|
themes_dict["darkmode"] = os.path.join(
|
||||||
app.root_path, "static/css/themes/lowPerformance/darkmode.css")
|
app.root_path, "static/css/themes/lowPerformance/darkmode.css"
|
||||||
themes_dict['lightmode'] = os.path.join(
|
)
|
||||||
app.root_path, "static/css/themes/lowPerformance/lightmode.css")
|
themes_dict["lightmode"] = os.path.join(
|
||||||
|
app.root_path, "static/css/themes/lowPerformance/lightmode.css"
|
||||||
|
)
|
||||||
|
|
||||||
return themes_dict
|
return themes_dict
|
||||||
|
|
||||||
|
|
||||||
def css_list():
|
def css_list():
|
||||||
"Generate the list of names of all the currently available themes"
|
"Generate the list of names of all the currently available themes"
|
||||||
if request.cookies.get('performance', '') == 'highPerformance':
|
if request.cookies.get("performance", "") == "highPerformance":
|
||||||
css_path = 'static/css/themes/highPerformance/'
|
css_path = "static/css/themes/highPerformance/"
|
||||||
else:
|
else:
|
||||||
css_path = 'static/css/themes/lowPerformance/'
|
css_path = "static/css/themes/lowPerformance/"
|
||||||
return list(get_css_dict(css_path).keys())
|
return list(get_css_dict(css_path).keys())
|
||||||
|
|
||||||
|
|
||||||
@general_bp.route("/css")
|
@general_bp.route("/css")
|
||||||
def css():
|
def css():
|
||||||
"Generate the css"
|
"Generate the css"
|
||||||
if request.cookies.get('performance', '') == 'highPerformance':
|
if request.cookies.get("performance", "") == "highPerformance":
|
||||||
css_path = 'static/css/themes/highPerformance/'
|
css_path = "static/css/themes/highPerformance/"
|
||||||
else:
|
else:
|
||||||
css_path = 'static/css/themes/lowPerformance/'
|
css_path = "static/css/themes/lowPerformance/"
|
||||||
|
|
||||||
cookie_theme = request.cookies.get('theme', '')
|
cookie_theme = request.cookies.get("theme", "")
|
||||||
|
|
||||||
themes_dict = get_css_dict(css_path)
|
themes_dict = get_css_dict(css_path)
|
||||||
|
|
||||||
|
@ -108,7 +109,7 @@ def css():
|
||||||
|
|
||||||
f = open(path)
|
f = open(path)
|
||||||
response = make_response(f.read())
|
response = make_response(f.read())
|
||||||
response.headers['Content-Type'] = 'text/css'
|
response.headers["Content-Type"] = "text/css"
|
||||||
f.close()
|
f.close()
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
@ -136,7 +137,9 @@ def location(location_id) -> str:
|
||||||
|
|
||||||
@general_bp.route("/location/<location_id>/<dish_id>")
|
@general_bp.route("/location/<location_id>/<dish_id>")
|
||||||
def location_dish(location_id, dish_id) -> str:
|
def location_dish(location_id, dish_id) -> str:
|
||||||
loc: Optional[Location] = first(filter(lambda l: l.id == location_id, location_definitions))
|
loc: Optional[Location] = first(
|
||||||
|
filter(lambda l: l.id == location_id, location_definitions)
|
||||||
|
)
|
||||||
if loc is None:
|
if loc is None:
|
||||||
abort(404)
|
abort(404)
|
||||||
dish = loc.dish_by_id(dish_id)
|
dish = loc.dish_by_id(dish_id)
|
||||||
|
@ -154,10 +157,10 @@ def location_dish(location_id, dish_id) -> str:
|
||||||
"name": o.name,
|
"name": o.name,
|
||||||
"description": o.description,
|
"description": o.description,
|
||||||
"price": o.price,
|
"price": o.price,
|
||||||
"tags": o.tags
|
"tags": o.tags,
|
||||||
}
|
}
|
||||||
for o in c[1].options
|
for o in c[1].options
|
||||||
]
|
],
|
||||||
}
|
}
|
||||||
for c in dish.choices
|
for c in dish.choices
|
||||||
])
|
])
|
||||||
|
|
|
@ -4,9 +4,19 @@ import typing
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from werkzeug.wrappers import Response
|
from werkzeug.wrappers import Response
|
||||||
|
|
||||||
# from flask import current_app as app
|
# from flask import current_app as app
|
||||||
from flask import (Blueprint, abort, flash, redirect, render_template, request,
|
from flask import (
|
||||||
session, url_for, wrappers)
|
Blueprint,
|
||||||
|
abort,
|
||||||
|
flash,
|
||||||
|
redirect,
|
||||||
|
render_template,
|
||||||
|
request,
|
||||||
|
session,
|
||||||
|
url_for,
|
||||||
|
wrappers,
|
||||||
|
)
|
||||||
from flask_login import current_user, login_required
|
from flask_login import current_user, login_required
|
||||||
|
|
||||||
from forms import AnonOrderItemForm, OrderForm, OrderItemForm
|
from forms import AnonOrderItemForm, OrderForm, OrderItemForm
|
||||||
|
@ -57,8 +67,7 @@ def order_from_id(order_id: int, form: OrderForm = None, dish_id=None) -> str:
|
||||||
flash("Please login to see this order.", "info")
|
flash("Please login to see this order.", "info")
|
||||||
abort(401)
|
abort(401)
|
||||||
if form is None:
|
if form is None:
|
||||||
form = AnonOrderItemForm() if current_user.is_anonymous() \
|
form = AnonOrderItemForm() if current_user.is_anonymous() else OrderItemForm()
|
||||||
else OrderItemForm()
|
|
||||||
if order.location:
|
if order.location:
|
||||||
form.populate(order.location)
|
form.populate(order.location)
|
||||||
if order.is_closed():
|
if order.is_closed():
|
||||||
|
@ -68,8 +77,14 @@ def order_from_id(order_id: int, form: OrderForm = None, dish_id=None) -> str:
|
||||||
|
|
||||||
dish = order.location.dish_by_id(dish_id) if order.location else None
|
dish = order.location.dish_by_id(dish_id) if order.location else None
|
||||||
|
|
||||||
return render_template("order.html", order=order, form=form,
|
return render_template(
|
||||||
total_price=total_price, debts=debts, dish=dish)
|
"order.html",
|
||||||
|
order=order,
|
||||||
|
form=form,
|
||||||
|
total_price=total_price,
|
||||||
|
debts=debts,
|
||||||
|
dish=dish,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@order_bp.route("/<order_id>/items")
|
@order_bp.route("/<order_id>/items")
|
||||||
|
@ -90,8 +105,7 @@ def items_showcase(order_id: int) -> str:
|
||||||
def order_edit(order_id: int) -> typing.Union[str, Response]:
|
def order_edit(order_id: int) -> typing.Union[str, Response]:
|
||||||
"Generate order edit view from id"
|
"Generate order edit view from id"
|
||||||
order = Order.query.filter(Order.id == order_id).first()
|
order = Order.query.filter(Order.id == order_id).first()
|
||||||
if current_user.id is not order.courier_id and \
|
if current_user.id is not order.courier_id and not current_user.is_admin():
|
||||||
not current_user.is_admin():
|
|
||||||
abort(401)
|
abort(401)
|
||||||
if order is None:
|
if order is None:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
@ -102,8 +116,7 @@ def order_edit(order_id: int) -> typing.Union[str, Response]:
|
||||||
order.update_from_hlds()
|
order.update_from_hlds()
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return redirect(url_for("order_bp.order_from_id", order_id=order.id))
|
return redirect(url_for("order_bp.order_from_id", order_id=order.id))
|
||||||
return render_template("order_edit.html", form=orderForm,
|
return render_template("order_edit.html", form=orderForm, order_id=order_id)
|
||||||
order_id=order_id)
|
|
||||||
|
|
||||||
|
|
||||||
@order_bp.route("/<order_id>/create", methods=["GET", "POST"])
|
@order_bp.route("/<order_id>/create", methods=["GET", "POST"])
|
||||||
|
@ -123,8 +136,7 @@ def order_item_create(order_id: int) -> typing.Any:
|
||||||
# If location doesn't exist any more, adding items is nonsensical
|
# If location doesn't exist any more, adding items is nonsensical
|
||||||
if not location:
|
if not location:
|
||||||
abort(404)
|
abort(404)
|
||||||
form = AnonOrderItemForm() if current_user.is_anonymous() \
|
form = AnonOrderItemForm() if current_user.is_anonymous() else OrderItemForm()
|
||||||
else OrderItemForm()
|
|
||||||
|
|
||||||
dish_id = form.dish_id.data if form.is_submitted() else request.args.get("dish")
|
dish_id = form.dish_id.data if form.is_submitted() else request.args.get("dish")
|
||||||
if dish_id and not location.dish_by_id(dish_id):
|
if dish_id and not location.dish_by_id(dish_id):
|
||||||
|
@ -142,8 +154,14 @@ def order_item_create(order_id: int) -> typing.Any:
|
||||||
chosen = [
|
chosen = [
|
||||||
(
|
(
|
||||||
choice.option_by_id(request.form.get("choice_" + choice.id))
|
choice.option_by_id(request.form.get("choice_" + choice.id))
|
||||||
if choice_type == "single_choice" else
|
if choice_type == "single_choice"
|
||||||
list(ignore_none(request.form.getlist("choice_" + choice.id, type=choice.option_by_id)))
|
else list(
|
||||||
|
ignore_none(
|
||||||
|
request.form.getlist(
|
||||||
|
"choice_" + choice.id, type=choice.option_by_id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
for (choice_type, choice) in choices
|
for (choice_type, choice) in choices
|
||||||
]
|
]
|
||||||
|
@ -151,14 +169,22 @@ def order_item_create(order_id: int) -> typing.Any:
|
||||||
|
|
||||||
if dish_was_changed or not all_choices_present:
|
if dish_was_changed or not all_choices_present:
|
||||||
try:
|
try:
|
||||||
user_name = form.user_name.data if form.user_name.validate(form) else None
|
user_name = (
|
||||||
|
form.user_name.data if form.user_name.validate(form) else None
|
||||||
|
)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
user_name = None
|
user_name = None
|
||||||
comment = form.comment.data if form.comment.validate(form) else None
|
comment = form.comment.data if form.comment.validate(form) else None
|
||||||
|
|
||||||
return redirect(url_for("order_bp.order_item_create",
|
return redirect(
|
||||||
order_id=order_id, dish=form.dish_id.data,
|
url_for(
|
||||||
user_name=user_name, comment=comment))
|
"order_bp.order_item_create",
|
||||||
|
order_id=order_id,
|
||||||
|
dish=form.dish_id.data,
|
||||||
|
user_name=user_name,
|
||||||
|
comment=comment,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# If the form was not submitted (GET request) or the form had errors: show form again
|
# If the form was not submitted (GET request) or the form had errors: show form again
|
||||||
if not form.validate_on_submit():
|
if not form.validate_on_submit():
|
||||||
|
@ -184,6 +210,7 @@ def order_item_create(order_id: int) -> typing.Any:
|
||||||
return option.name
|
return option.name
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
return ", ".join(o.name for o in option if no_text_tag not in o.tags)
|
return ", ".join(o.name for o in option if no_text_tag not in o.tags)
|
||||||
|
|
||||||
comments = list(ignore_none(_name(option) for option in chosen))
|
comments = list(ignore_none(_name(option) for option in chosen))
|
||||||
if item.comment:
|
if item.comment:
|
||||||
comments.append("Comment: " + item.comment)
|
comments.append("Comment: " + item.comment)
|
||||||
|
@ -198,6 +225,7 @@ def order_item_create(order_id: int) -> typing.Any:
|
||||||
return option.price or 0
|
return option.price or 0
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
return sum(o.price or 0 for o in option)
|
return sum(o.price or 0 for o in option)
|
||||||
|
|
||||||
item.price += sum(_price(option) for option in chosen)
|
item.price += sum(_price(option) for option in chosen)
|
||||||
|
|
||||||
db.session.add(item)
|
db.session.add(item)
|
||||||
|
@ -216,8 +244,7 @@ def item_paid(order_id: int, item_id: int) -> typing.Optional[Response]:
|
||||||
if item.order.courier_id == user_id or current_user.admin:
|
if item.order.courier_id == user_id or current_user.admin:
|
||||||
item.paid = True
|
item.paid = True
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash("Paid %s by %s" % (item.dish_name, item.get_name()),
|
flash("Paid %s by %s" % (item.dish_name, item.get_name()), "success")
|
||||||
"success")
|
|
||||||
return redirect(url_for("order_bp.order_from_id", order_id=order_id))
|
return redirect(url_for("order_bp.order_from_id", order_id=order_id))
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
|
@ -244,8 +271,7 @@ def items_user_paid(order_id: int, user_name: str) -> typing.Optional[Response]:
|
||||||
for item in items:
|
for item in items:
|
||||||
item.paid = True
|
item.paid = True
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash("Paid %d items for %s" %
|
flash("Paid %d items for %s" % (len(items), item.get_name()), "success")
|
||||||
(len(items), item.get_name()), "success")
|
|
||||||
return redirect(url_for("order_bp.order_from_id", order_id=order_id))
|
return redirect(url_for("order_bp.order_from_id", order_id=order_id))
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
|
@ -293,7 +319,9 @@ def close_order(order_id: int) -> typing.Optional[Response]:
|
||||||
order = Order.query.filter(Order.id == order_id).first()
|
order = Order.query.filter(Order.id == order_id).first()
|
||||||
if order is None:
|
if order is None:
|
||||||
abort(404)
|
abort(404)
|
||||||
if (current_user.id == order.courier_id or current_user.is_admin()) and not order.is_closed():
|
if (
|
||||||
|
current_user.id == order.courier_id or current_user.is_admin()
|
||||||
|
) and not order.is_closed():
|
||||||
order.stoptime = datetime.now()
|
order.stoptime = datetime.now()
|
||||||
if order.courier_id == 0 or order.courier_id is None:
|
if order.courier_id == 0 or order.courier_id is None:
|
||||||
courier = select_user(order.items)
|
courier = select_user(order.items)
|
||||||
|
@ -332,7 +360,8 @@ def get_orders(expression=None) -> typing.List[Order]:
|
||||||
order_list: typing.List[OrderForm] = []
|
order_list: typing.List[OrderForm] = []
|
||||||
if expression is None:
|
if expression is None:
|
||||||
expression = (datetime.now() > Order.starttime) & (
|
expression = (datetime.now() > Order.starttime) & (
|
||||||
Order.stoptime > datetime.now()
|
Order.stoptime
|
||||||
|
> datetime.now()
|
||||||
# pylint: disable=C0121
|
# pylint: disable=C0121
|
||||||
) | (Order.stoptime == None)
|
) | (Order.stoptime == None)
|
||||||
if not current_user.is_anonymous():
|
if not current_user.is_anonymous():
|
||||||
|
@ -340,5 +369,6 @@ def get_orders(expression=None) -> typing.List[Order]:
|
||||||
else:
|
else:
|
||||||
order_list = Order.query.filter(
|
order_list = Order.query.filter(
|
||||||
# pylint: disable=C0121
|
# pylint: disable=C0121
|
||||||
(expression & (Order.public == True))).all()
|
(expression & (Order.public == True))
|
||||||
|
).all()
|
||||||
return order_list
|
return order_list
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
"Script containing everything specific to ZeusWPI"
|
"Script containing everything specific to ZeusWPI"
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
from flask import (Blueprint, current_app, flash, redirect, request, session,
|
from flask import Blueprint, current_app, flash, redirect, request, session, url_for
|
||||||
url_for)
|
|
||||||
from flask_login import login_user
|
from flask_login import login_user
|
||||||
from flask_oauthlib.client import OAuth, OAuthException
|
from flask_oauthlib.client import OAuth, OAuthException
|
||||||
from werkzeug.wrappers import Response
|
from werkzeug.wrappers import Response
|
||||||
|
|
|
@ -1,15 +1,19 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
print("""============================
|
print(
|
||||||
|
"""============================
|
||||||
s5: S5
|
s5: S5
|
||||||
osm https://www.openstreetmap.org/node/3752879366
|
osm https://www.openstreetmap.org/node/3752879366
|
||||||
address Krijgslaan 281, 9000 Gent
|
address Krijgslaan 281, 9000 Gent
|
||||||
website https://www.ugent.be/student/nl/meer-dan-studeren/resto/restos/restocampussterre.htm
|
website https://www.ugent.be/student/nl/meer-dan-studeren/resto/restos/restocampussterre.htm
|
||||||
============================""")
|
============================"""
|
||||||
|
)
|
||||||
|
|
||||||
# Paste menu from https://www.ugent.be/student/nl/meer-dan-studeren/resto/broodjes/overzicht.htm
|
# Paste menu from https://www.ugent.be/student/nl/meer-dan-studeren/resto/broodjes/overzicht.htm
|
||||||
# here
|
# here
|
||||||
MENU = [l.split("\t") for l in """
|
MENU = [
|
||||||
|
l.split("\t")
|
||||||
|
for l in """
|
||||||
Spring break Erwten-munt spread, komkommer, radijs, sla, croutons, cocktailsaus € 1,50 € 2,40
|
Spring break Erwten-munt spread, komkommer, radijs, sla, croutons, cocktailsaus € 1,50 € 2,40
|
||||||
Groentespread Weekelijks wisselende groentespread € 1,60 € 2,60
|
Groentespread Weekelijks wisselende groentespread € 1,60 € 2,60
|
||||||
Brie Brie, honing, pijnboompitten, sla € 1,50 € 2,50
|
Brie Brie, honing, pijnboompitten, sla € 1,50 € 2,50
|
||||||
|
@ -30,40 +34,44 @@ Maison Ham, kaas, augurk, ei, sla, tomaat, cocktailsaus en mayonaise € 1,60
|
||||||
Tropical Ham, kaas, ananas, ei, sla, cocktailsaus € 1,60 € 2,40
|
Tropical Ham, kaas, ananas, ei, sla, cocktailsaus € 1,60 € 2,40
|
||||||
Toscane Mozzarella, prosciutto ham, sla en tomatensalsa € 1,60 € 2,70
|
Toscane Mozzarella, prosciutto ham, sla en tomatensalsa € 1,60 € 2,70
|
||||||
Argenteuil Ham, asperge, ei, komkommer, sla, tomaat en mayonaise € 1,50 € 2,40
|
Argenteuil Ham, asperge, ei, komkommer, sla, tomaat en mayonaise € 1,50 € 2,40
|
||||||
""".strip().split("\n")]
|
""".strip().split(
|
||||||
|
"\n"
|
||||||
|
)
|
||||||
|
]
|
||||||
# Sort by price. This fails if price is not always exactly "€ x,xx" but whatever
|
# Sort by price. This fails if price is not always exactly "€ x,xx" but whatever
|
||||||
MENU.sort(key=lambda dish: dish[2] + dish[3])
|
MENU.sort(key=lambda dish: dish[2] + dish[3])
|
||||||
|
|
||||||
SANDWICHES = [
|
SANDWICHES = [
|
||||||
[ # First price
|
[("small_white", "Klein wit "), ("small_brown", "Klein bruin"),], # First price
|
||||||
("small_white", "Klein wit "),
|
|
||||||
("small_brown", "Klein bruin"),
|
|
||||||
],
|
|
||||||
[ # Second price
|
[ # Second price
|
||||||
("large_white", "Groot wit "),
|
("large_white", "Groot wit "),
|
||||||
("large_brown", "Groot bruin"),
|
("large_brown", "Groot bruin"),
|
||||||
("quattro", " Quattro "),
|
("quattro", " Quattro "),
|
||||||
]
|
],
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def name_to_id(name):
|
def name_to_id(name):
|
||||||
return "".join(filter(
|
return "".join(
|
||||||
lambda c: ord("a") <= ord(c) <= ord("z"),
|
filter(lambda c: ord("a") <= ord(c) <= ord("z"), name.lower().replace("é", "e"))
|
||||||
name.lower().replace("é", "e")
|
)
|
||||||
))
|
|
||||||
|
|
||||||
for dish in MENU:
|
for dish in MENU:
|
||||||
print()
|
print()
|
||||||
name, description = dish[0], dish[1]
|
name, description = dish[0], dish[1]
|
||||||
prices = [p.replace(",", ".") for p in dish[2:]]
|
prices = [p.replace(",", ".") for p in dish[2:]]
|
||||||
|
|
||||||
print("dish sandwich_{}: Broodje {} -- {}".format(name_to_id(name), name, description))
|
print(
|
||||||
|
"dish sandwich_{}: Broodje {} -- {}".format(name_to_id(name), name, description)
|
||||||
|
)
|
||||||
print("\tsingle_choice sandwich: Broodje")
|
print("\tsingle_choice sandwich: Broodje")
|
||||||
for sandwiches, price in zip(SANDWICHES, prices):
|
for sandwiches, price in zip(SANDWICHES, prices):
|
||||||
for sw_id, sw_name in sandwiches:
|
for sw_id, sw_name in sandwiches:
|
||||||
print("\t\t{}: {} {}".format(sw_id, sw_name, price))
|
print("\t\t{}: {} {}".format(sw_id, sw_name, price))
|
||||||
|
|
||||||
print("""
|
print(
|
||||||
|
"""
|
||||||
dish yoghurt: Natuuryoghurt € 0.4
|
dish yoghurt: Natuuryoghurt € 0.4
|
||||||
dish yofu: Plantaardige yofu € 1
|
dish yofu: Plantaardige yofu € 1
|
||||||
dish yoghurt_muesli: Yoghurt met muesli € 1
|
dish yoghurt_muesli: Yoghurt met muesli € 1
|
||||||
|
@ -86,4 +94,5 @@ dish bionade: Bionade € 1.5
|
||||||
dish finley: Finley € 1
|
dish finley: Finley € 1
|
||||||
dish iced_coffee: IJskoffie € 2
|
dish iced_coffee: IJskoffie € 2
|
||||||
dish iced_tea: IJsthee € 2
|
dish iced_tea: IJsthee € 2
|
||||||
dish smoothie: Smoothie € 2""")
|
dish smoothie: Smoothie € 2"""
|
||||||
|
)
|
||||||
|
|
Loading…
Reference in a new issue