diff --git a/app/models/order.py b/app/models/order.py index 255704a..6e5391c 100644 --- a/app/models/order.py +++ b/app/models/order.py @@ -1,7 +1,9 @@ -"Script for everything Order related in the database" +"""Script for everything Order related in the database""" import typing from collections import defaultdict from datetime import datetime +import secrets +import string from hlds.definitions import location_definitions from utils import first @@ -10,8 +12,13 @@ from .database import db from .user import User +def generate_slug(): + alphabet = string.ascii_letters + string.digits + return ''.join(secrets.choice(alphabet) for i in range(7)) + + class Order(db.Model): - "Class used for configuring the Order model in the database" + """Class used for configuring the Order model in the database""" id = db.Column(db.Integer, primary_key=True) courier_id = db.Column(db.Integer, nullable=True) location_id = db.Column(db.String(64)) @@ -19,6 +26,7 @@ class Order(db.Model): starttime = db.Column(db.DateTime) stoptime = db.Column(db.DateTime) public = db.Column(db.Boolean, default=True) + slug = db.Column(db.String(7), default=generate_slug, unique=True) items = db.relationship("OrderItem", backref="order", lazy="dynamic") @@ -47,7 +55,7 @@ class Order(db.Model): self.location_name = self.location.name def for_user(self, anon=None, user=None) -> typing.List: - "Get the items for a certain user" + """Get the items for a certain user""" return list( filter( (lambda i: i.user == user) @@ -58,7 +66,7 @@ class Order(db.Model): ) def group_by_user(self) -> typing.List[typing.Tuple[str, typing.List]]: - "Group items of an Order by user" + """Group items of an Order by user""" group: typing.Dict[str, typing.List] = {} # pylint: disable=E1133 @@ -78,7 +86,7 @@ class Order(db.Model): ) -> typing.List[ typing.Tuple[str, int, typing.List[typing.Tuple[str, typing.List]]] ]: - "Group items of an Order by dish" + """Group items of an Order by dish""" group: typing.Dict[str, typing.Dict[str, typing.List]] = defaultdict( lambda: defaultdict(list) ) @@ -101,11 +109,11 @@ class Order(db.Model): ) def is_closed(self) -> bool: - "Return whether or not the order is closed" + """Return whether the order is closed""" return self.stoptime and datetime.now() > self.stoptime def can_close(self, user_id: int) -> bool: - "Check if a user can close the Order" + """Check if a user can close the Order""" if self.stoptime and self.stoptime < datetime.now(): return False user = None diff --git a/app/models/orderitem.py b/app/models/orderitem.py index 623d751..fa74bba 100644 --- a/app/models/orderitem.py +++ b/app/models/orderitem.py @@ -10,7 +10,7 @@ from .user import User class OrderItem(db.Model): - "Class used for configuring the OrderItem model in the database" + """Class used for configuring the OrderItem model in the database""" id = db.Column(db.Integer, primary_key=True) order_id = db.Column(db.Integer, db.ForeignKey("order.id"), nullable=False) user_id = db.Column(db.Integer, db.ForeignKey("user.id")) @@ -61,7 +61,7 @@ class OrderItem(db.Model): # pylint: disable=W0613 def can_delete(self, order_id: int, user_id: int, name: str) -> bool: - "Check if a user can delete an item" + """Check if a user can delete an item""" if int(self.order_id) != int(order_id): return False if self.order.is_closed(): diff --git a/app/notification.py b/app/notification.py index fa97045..14ee065 100644 --- a/app/notification.py +++ b/app/notification.py @@ -11,14 +11,14 @@ from models.order import Order def webhook_text(order: Order) -> typing.Optional[str]: - "Function that makes the text for the notification" + """Function that makes the text for the notification""" if order.location_id == "test": return None if order.courier is not None: # pylint: disable=C0301, C0209 return " {3} is going to {1}, order <{0}|here>! Deadline in {2} minutes!".format( - url_for("order_bp.order_from_id", order_id=order.id, _external=True), + url_for("order_bp.order_from_slug", order_slug=order.slug, _external=True), order.location_name, remaining_minutes(order.stoptime), order.courier.username.title(), @@ -28,12 +28,12 @@ def webhook_text(order: Order) -> typing.Optional[str]: return " New order for {}. Deadline in {} minutes. <{}|Open here.>".format( order.location_name, remaining_minutes(order.stoptime), - url_for("order_bp.order_from_id", order_id=order.id, _external=True), + url_for("order_bp.order_from_slug", order_slug=order.slug, _external=True), ) 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) if message: webhookthread = WebhookSenderThread(message, app.config["SLACK_WEBHOOK"]) @@ -41,7 +41,7 @@ def post_order_to_webhook(order: Order) -> None: class WebhookSenderThread(Thread): - "Extension of the Thread class, which sends a webhook for the notification" + """Extension of the Thread class, which sends a webhook for the notification""" def __init__(self, message: str, url: str) -> None: super().__init__() @@ -52,7 +52,7 @@ class WebhookSenderThread(Thread): self.slack_webhook() def slack_webhook(self) -> None: - "The webhook for the specified chat platform" + """The webhook for the specified chat platform""" if self.url: requests.post(self.url, json={"text": self.message}) else: @@ -60,7 +60,7 @@ class WebhookSenderThread(Thread): def remaining_minutes(value) -> str: - "Return the remaining minutes until the deadline of and order" + """Return the remaining minutes until the deadline of and order""" delta = value - datetime.now() if delta.total_seconds() < 0: return "0" diff --git a/app/templates/order.html b/app/templates/order.html index ffb267b..cd218ff 100644 --- a/app/templates/order.html +++ b/app/templates/order.html @@ -36,7 +36,7 @@ {% for item in my_items %}
  • {% if item.can_delete(order.id, current_user.id, session.get('anon_name', '')) -%} -
    +
    {%- endif %} @@ -65,7 +65,7 @@

    Add item to order

    {% for dish in order.location.dishes %} -
    + {{ form.csrf_token }} @@ -167,7 +167,7 @@
    {% if order.courier == None %} {% if not current_user.is_anonymous() %} - +
    {% else %}No-one yet{% endif %} @@ -180,12 +180,12 @@
    {% if order.can_close(current_user.id) -%} -
    +
    {% endif %} {% if courier_or_admin %} - Edit + Edit {%- endif %}
    @@ -258,7 +258,7 @@ @@ -293,7 +293,9 @@
  • {% if item.can_delete(order.id, current_user.id, session.get('anon_name', '')) -%} - +
    + +
    {% else %} {%- endif %} diff --git a/app/templates/order_edit.html b/app/templates/order_edit.html index 2667a9c..db6d709 100644 --- a/app/templates/order_edit.html +++ b/app/templates/order_edit.html @@ -11,7 +11,7 @@

    Edit order

    -
    + {{ form.csrf_token }}
    {{ form.courier_id.label(class='control-label') }}
    diff --git a/app/templates/utils.html b/app/templates/utils.html index 15d85eb..0af7b82 100644 --- a/app/templates/utils.html +++ b/app/templates/utils.html @@ -9,7 +9,7 @@ {% else %}open{% endif %}
    {%- endmacro %} diff --git a/app/views/order.py b/app/views/order.py index 2e91fb8..8023ecf 100644 --- a/app/views/order.py +++ b/app/views/order.py @@ -1,4 +1,4 @@ -"Script to generate the order related views of Haldis" +"""Script to generate the order related views of Haldis""" import random import re import typing @@ -20,7 +20,7 @@ order_bp = Blueprint("order_bp", "order") @order_bp.route("/") def orders(form: OrderForm = None) -> str: - "Generate general order view" + """Generate general order view""" if form is None and not current_user.is_anonymous(): form = OrderForm() location_id = request.args.get("location_id") @@ -33,7 +33,7 @@ def orders(form: OrderForm = None) -> str: @order_bp.route("/create", methods=["POST"]) @login_required def order_create() -> typing.Union[str, Response]: - "Generate order create view" + """Generate order create view""" orderForm = OrderForm() orderForm.populate() if orderForm.validate_on_submit(): @@ -43,14 +43,14 @@ def order_create() -> typing.Union[str, Response]: db.session.add(order) db.session.commit() post_order_to_webhook(order) - return redirect(url_for("order_bp.order_from_id", order_id=order.id)) + return redirect(url_for("order_bp.order_from_slug", order_slug=order.slug)) return orders(form=orderForm) -@order_bp.route("/") -def order_from_id(order_id: int, form: OrderForm = None, dish_id=None) -> str: - "Generate order view from id" - order = Order.query.filter(Order.id == order_id).first() +@order_bp.route("/") +def order_from_slug(order_slug: str, form: OrderForm = None, dish_id=None) -> str: + """Generate order view from id""" + order = Order.query.filter(Order.slug == order_slug).first() if order is None: abort(404) if current_user.is_anonymous() and not order.public: @@ -77,10 +77,10 @@ def order_from_id(order_id: int, form: OrderForm = None, dish_id=None) -> str: ) -@order_bp.route("//items") -def items_shop_view(order_id: int) -> str: - "Generate order items view from id" - order = Order.query.filter(Order.id == order_id).first() +@order_bp.route("//items") +def items_shop_view(order_slug: int) -> str: + """Generate order items view from id""" + order = Order.query.filter(Order.slug == order_slug).first() if order is None: abort(404) if current_user.is_anonymous() and not order.public: @@ -90,31 +90,31 @@ def items_shop_view(order_id: int) -> str: return render_template("order_items.html", order=order, total_price=total_price) -@order_bp.route("//edit", methods=["GET", "POST"]) +@order_bp.route("//edit", methods=["GET", "POST"]) @login_required -def order_edit(order_id: int) -> typing.Union[str, Response]: - "Generate order edit view from id" - order = Order.query.filter(Order.id == order_id).first() +def order_edit(order_slug: str) -> typing.Union[str, Response]: + """Generate order edit view from id""" + order = Order.query.filter(Order.slug == order_slug).first() if current_user.id is not order.courier_id and not current_user.is_admin(): abort(401) if order is None: abort(404) - orderForm = OrderForm(obj=order) - orderForm.populate() - if orderForm.validate_on_submit(): - orderForm.populate_obj(order) + order_form = OrderForm(obj=order) + order_form.populate() + if order_form.validate_on_submit(): + order_form.populate_obj(order) order.update_from_hlds() db.session.commit() - return redirect(url_for("order_bp.order_from_id", order_id=order.id)) - return render_template("order_edit.html", form=orderForm, order_id=order_id) + return redirect(url_for("order_bp.order_from_slug", order_slug=order.slug)) + return render_template("order_edit.html", form=order_form, order_slug=order.slug) -@order_bp.route("//create", methods=["GET", "POST"]) -def order_item_create(order_id: int) -> typing.Any: +@order_bp.route("//create", methods=["GET", "POST"]) +def order_item_create(order_slug: str) -> typing.Any: # type is 'typing.Union[str, Response]', but this errors due to # https://github.com/python/mypy/issues/7187 - "Add item to order from id" - current_order = Order.query.filter(Order.id == order_id).first() + """Add item to order from slug""" + current_order = Order.query.filter(Order.slug == order_slug).first() if current_order is None: abort(404) if current_order.is_closed(): @@ -123,7 +123,7 @@ def order_item_create(order_id: int) -> typing.Any: flash("Please login to see this order.", "info") abort(401) location = current_order.location - # If location doesn't exist any more, adding items is nonsensical + # If location doesn't exist anymore, adding items is nonsensical if not location: abort(404) form = AnonOrderItemForm() if current_user.is_anonymous() else OrderItemForm() @@ -171,7 +171,7 @@ def order_item_create(order_id: int) -> typing.Any: return redirect( url_for( "order_bp.order_item_create", - order_id=order_id, + order_slug=current_order.slug, dish=form.dish_id.data, user_name=user_name, comment=comment, @@ -180,14 +180,13 @@ def order_item_create(order_id: int) -> typing.Any: # If the form was not submitted (GET request) or the form had errors: show form again if not form.validate_on_submit(): - return order_from_id(order_id, form=form, dish_id=dish_id) + return order_from_slug(current_order.slug, form=form, dish_id=dish_id) # Form was submitted and is valid - item = OrderItem() form.populate_obj(item) item.hlds_data_version = location_definition_version - item.order_id = order_id + item.order_id = current_order.id if not current_user.is_anonymous(): item.user_id = current_user.id else: @@ -222,16 +221,16 @@ def order_item_create(order_id: int) -> typing.Any: db.session.add(item) db.session.commit() - flash("Ordered %s" % (item.dish_name), "success") - return redirect(url_for("order_bp.order_from_id", order_id=order_id)) + flash("Ordered %s" % item.dish_name, "success") + return redirect(url_for("order_bp.order_from_slug", order_slug=order_slug)) -@order_bp.route("//modify_items", methods=["POST"]) +@order_bp.route("//modify_items", methods=["POST"]) @login_required # pylint: disable=R1710 -def modify_items(order_id: int) -> typing.Optional[Response]: +def modify_items(order_slug: str) -> typing.Optional[Response]: if "delete_item" in request.form: - return delete_item(order_id, int(request.form["delete_item"])) + return delete_item(order_slug, int(request.form["delete_item"])) user_names = request.form.getlist("user_names") if request.form.get("action") == "mark_paid": return set_items_paid(order_id, user_names, True) @@ -241,7 +240,8 @@ def modify_items(order_id: int) -> typing.Optional[Response]: abort(404) return None -def set_items_paid(order_id: int, user_names: typing.Iterable[str], paid: bool): +def set_items_paid(order_slug: str, user_names: typing.Iterable[str], paid: bool): + order = Order.query.filter(Order.slug == order_slug).first() total_paid_items = 0 total_failed_items = 0 for user_name in user_names: @@ -249,15 +249,15 @@ def set_items_paid(order_id: int, user_names: typing.Iterable[str], paid: bool): items: typing.List[OrderItem] = [] if user: items = OrderItem.query.filter( - (OrderItem.user_id == user.id) & (OrderItem.order_id == order_id) + (OrderItem.user_id == user.id) & (OrderItem.order_id == order.id) ).all() else: items = OrderItem.query.filter( - (OrderItem.user_name == user_name) & (OrderItem.order_id == order_id) + (OrderItem.user_name == user_name) & (OrderItem.order_id == order.id) ).all() for item in items: - if item.can_modify_payment(order_id, current_user.id): + if item.can_modify_payment(order.id, current_user.id): if item.paid != paid: item.paid = paid total_paid_items += 1 @@ -269,33 +269,34 @@ def set_items_paid(order_id: int, user_names: typing.Iterable[str], paid: bool): flash("Marked %d items as paid" % (total_paid_items,), "success") else: flash("Failed to mark %d items as paid (succeeded in marking %d items as paid)" % (total_failed_items, total_paid_items), "error") - return redirect(url_for("order_bp.order_from_id", order_id=order_id)) + return redirect(url_for("order_bp.order_from_slug", order_slug=order_slug)) -@order_bp.route("///delete", methods=["POST"]) +@order_bp.route("///delete", methods=["POST"]) # pylint: disable=R1710 -def delete_item(order_id: int, item_id: int) -> typing.Any: +def delete_item(order_slug: str, item_id: int) -> typing.Any: # type is 'typing.Optional[Response]', but this errors due to # https://github.com/python/mypy/issues/7187 - "Delete an item from an order" - item = OrderItem.query.filter(OrderItem.id == item_id).first() + """Delete an item from an order""" + item: OrderItem = OrderItem.query.filter(OrderItem.id == item_id).first() + order: Order = Order.query.filter(Order.slug == order_slug).first() user_id = None if not current_user.is_anonymous(): user_id = current_user.id - if item.can_delete(order_id, user_id, session.get("anon_name", "")): + if item.can_delete(order.id, user_id, session.get("anon_name", "")): dish_name = item.dish_name db.session.delete(item) db.session.commit() - flash("Deleted %s" % (dish_name), "success") - return redirect(url_for("order_bp.order_from_id", order_id=order_id)) + flash("Deleted %s" % dish_name, "success") + return redirect(url_for("order_bp.order_from_slug", order_slug=order_slug)) abort(404) -@order_bp.route("//volunteer", methods=["POST"]) +@order_bp.route("//volunteer", methods=["POST"]) @login_required -def volunteer(order_id: int) -> Response: - "Add a volunteer to an order" - order = Order.query.filter(Order.id == order_id).first() +def volunteer(order_slug: str) -> Response: + """Add a volunteer to an order""" + order = Order.query.filter(Order.slug == order_slug).first() if order is None: abort(404) if order.courier_id is None or order.courier_id == 0: @@ -304,14 +305,14 @@ def volunteer(order_id: int) -> Response: flash("Thank you for volunteering!") else: flash("Volunteering not possible!") - return redirect(url_for("order_bp.order_from_id", order_id=order_id)) + return redirect(url_for("order_bp.order_from_slug", order_slug=order.slug)) -@order_bp.route("//close", methods=["POST"]) +@order_bp.route("//close", methods=["POST"]) @login_required -def close_order(order_id: int) -> typing.Optional[Response]: - "Close an order" - order = Order.query.filter(Order.id == order_id).first() +def close_order(order_slug: str) -> typing.Optional[Response]: + """Close an order""" + order = Order.query.filter(Order.slug == order_slug).first() if order is None: abort(404) if ( @@ -323,7 +324,7 @@ def close_order(order_id: int) -> typing.Optional[Response]: if courier is not None: order.courier_id = courier.id db.session.commit() - return redirect(url_for("order_bp.order_from_id", order_id=order_id)) + return redirect(url_for("order_bp.order_from_slug", order_slug=order_slug)) return None @@ -370,7 +371,7 @@ def prices(order_id: int) -> typing.Optional[Response]: def select_user(items) -> typing.Optional[User]: - "Select a random user from those who are signed up for the order" + """Select a random user from those who are signed up for the order""" user = None # remove non users items = [i for i in items if i.user_id] @@ -389,7 +390,7 @@ def select_user(items) -> typing.Optional[User]: def get_orders(expression=None) -> typing.List[Order]: - "Give the list of all currently open and public Orders" + """Give the list of all currently open and public Orders""" order_list: typing.List[OrderForm] = [] if expression is None: expression = (datetime.now() > Order.starttime) & (