From f900c85931f4bd81a4ebf27c6b82c59630fe368a Mon Sep 17 00:00:00 2001 From: Midgard Date: Fri, 21 Feb 2020 18:38:30 +0100 Subject: [PATCH] Add form for choices, submitting fails --- app/forms.py | 38 +++++++++++++++++++++----- app/hlds/models.py | 6 ++++- app/templates/order.html | 15 +++++++++++ app/views/order.py | 58 ++++++++++++++++++++++++++++------------ 4 files changed, 93 insertions(+), 24 deletions(-) diff --git a/app/forms.py b/app/forms.py index e5c6981..86bb9c1 100644 --- a/app/forms.py +++ b/app/forms.py @@ -1,15 +1,17 @@ "Script for everything form related in Haldis" from datetime import datetime, timedelta +from typing import Optional + from flask import session from flask_login import current_user from flask_wtf import FlaskForm as Form -from wtforms import (DateTimeField, SelectField, StringField, SubmitField, - validators) +from wtforms import (DateTimeField, SelectField, SelectMultipleField, StringField, SubmitField, + FieldList, validators) from utils import euro_string from hlds.definitions import location_definitions -from hlds.models import Location +from hlds.models import Location, Dish, Choice from models import User @@ -45,18 +47,42 @@ class OrderForm(Form): class OrderItemForm(Form): - "Class which defines the form for a new Item in an Order" + "New Item in an Order" # pylint: disable=R0903 dish_id = SelectField("Dish") + single_choices = FieldList(SelectField()) + multi_choices = FieldList(SelectMultipleField()) comment = StringField("Comment") submit_button = SubmitField("Submit") - def populate(self, location: Location) -> None: - "Fill in all the dish options from the location" + def populate(self, location: Location, dish_id: Optional[str]) -> None: self.dish_id.choices = [ (i.id, (i.name + ": " + euro_string(i.price))) for i in location.dishes ] + dish = location.dish_by_id(dish_id) if dish_id else None + if dish: + self.add_choices_for(dish) + + def add_choices_for(self, dish: Dish): + for (choice_type, choice) in dish.choices: + if choice_type == "single_choice": + field = self.single_choices.append_entry(choice.name) + elif choice_type == "multi_choice": + field = self.multi_choices.append_entry(choice.name) + else: + assert False, "Unsupported choice type" + field.label.text = choice.name + field.choices = self.options_for(choice) + + @staticmethod + def options_for(choice: Choice): + return [ + (c.id, (c.name + + (" (" + c.description + ")" if c.description else "") + + (": +" + euro_string(c.price) if c.price else ""))) + for c in choice.options + ] class AnonOrderItemForm(OrderItemForm): diff --git a/app/hlds/models.py b/app/hlds/models.py index 68160f1..2a1161d 100644 --- a/app/hlds/models.py +++ b/app/hlds/models.py @@ -2,7 +2,7 @@ # pylint: disable=too-few-public-methods from typing import Iterable, List, Mapping, Any, Optional -from utils import euro_string +from utils import euro_string, first def _format_tags(tags: Iterable[str]) -> str: @@ -63,6 +63,7 @@ class Dish: self.price: int = price self.tags: List[str] = tags + # The str in (str, Choice) is the type of choice: single_choice or multi_choice self.choices: List[(str, Choice)] = choices def __str__(self): @@ -86,6 +87,9 @@ class Location: self.dishes: List[Dish] = dishes + def dish_by_id(self, dish_id: str) -> Optional[Dish]: + return first(filter(lambda d: d.id == dish_id, self.dishes)) + def __str__(self): return ( "============================\n" diff --git a/app/templates/order.html b/app/templates/order.html index 51a617b..37a6f88 100644 --- a/app/templates/order.html +++ b/app/templates/order.html @@ -57,6 +57,21 @@ {{ form.comment(class='form-control', placeholder='Fill in comment, when applicable') }} {{ util.render_form_field_errors(form.comment) }} + + {% for choice_field in form.single_choices %} +
+ {{ choice_field.label(class='control-label') }}
+ {{ choice_field(class='form-control') }} + {{ util.render_form_field_errors(choice_field) }} +
+ {% endfor %} + {% for choice_field in form.multi_choices %} +
+ {{ choice_field.label(class='control-label') }}
+ {{ choice_field(class='form-control') }} + {{ util.render_form_field_errors(choice_field) }} +
+ {% endfor %} {% if current_user.is_anonymous() %}
{{ form.name.label(class='control-label') }} diff --git a/app/views/order.py b/app/views/order.py index 2bff4c5..a36b5c7 100644 --- a/app/views/order.py +++ b/app/views/order.py @@ -11,6 +11,7 @@ from flask_login import current_user, login_required from forms import AnonOrderItemForm, OrderForm, OrderItemForm from models import Order, OrderItem, User, db +from hlds.definitions import location_definitions from notification import post_order_to_webhook order_bp = Blueprint("order_bp", "order") @@ -58,7 +59,7 @@ def order_from_id(order_id: int, form: OrderForm = None) -> str: form = AnonOrderItemForm() if current_user.is_anonymous() \ else OrderItemForm() if order.location: - form.populate(order.location) + form.populate(order.location, None) if order.is_closed(): form = None total_price = sum([o.price for o in order.items]) @@ -101,7 +102,7 @@ def order_edit(order_id: int) -> typing.Union[str, Response]: order_id=order_id) -@order_bp.route("//create", methods=["POST"]) +@order_bp.route("//create", methods=["GET", "POST"]) def order_item_create(order_id: int) -> typing.Any: # type is 'typing.Union[str, Response]', but this errors due to # https://github.com/python/mypy/issues/7187 @@ -114,23 +115,46 @@ def order_item_create(order_id: int) -> typing.Any: if current_user.is_anonymous() and not current_order.public: 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 not location: + abort(404) form = AnonOrderItemForm() if current_user.is_anonymous() \ else OrderItemForm() - form.populate(current_order.location) - if form.validate_on_submit(): - item = OrderItem() - form.populate_obj(item) - item.order_id = order_id - if not current_user.is_anonymous(): - item.user_id = current_user.id - else: - session["anon_name"] = item.name - item.update_from_hlds() - 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)) - return order_from_id(order_id, form=form) + + 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): + abort(404) + form.populate(current_order.location, dish_id) + + if not form.validate_on_submit(): + return order_from_id(order_id, form=form) + + # Form was submitted and is valid + + # The form's validation tests that dish_id is valid and gives a friendly error if it's not + form_data = form.data + choices = location.dish_by_id(form_data["dish_id"]).choices + all_choices_present = all( + ("choice_" + choice.id) in form_data + for (_choice_type, choice) in choices + ) + if not all_choices_present: + return redirect(url_for("order_bp.order_item_create", + order_id=order_id, dish=form_data["dish_id"])) + + item = OrderItem() + form.populate_obj(item) + item.order_id = order_id + if not current_user.is_anonymous(): + item.user_id = current_user.id + else: + session["anon_name"] = item.name + item.update_from_hlds() + 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)) @order_bp.route("///paid", methods=["POST"])