Make creating order and adding items work

This commit is contained in:
Midgard 2020-01-27 02:31:02 +01:00
parent 0e7675cec6
commit 6f24b52855
Signed by: midgard
GPG key ID: 511C112F1331BBB4
10 changed files with 110 additions and 76 deletions

View file

@ -7,10 +7,10 @@ from flask_wtf import FlaskForm as Form
from wtforms import (DateTimeField, SelectField, StringField, SubmitField, from wtforms import (DateTimeField, SelectField, StringField, SubmitField,
validators) validators)
from utils import euro_string
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 User from models import User
from utils import euro_string
class OrderForm(Form): class OrderForm(Form):
@ -18,7 +18,7 @@ class OrderForm(Form):
# pylint: disable=R0903 # pylint: disable=R0903
courier_id = SelectField("Courier", coerce=int) courier_id = SelectField("Courier", coerce=int)
location_id = SelectField( location_id = SelectField(
"Location", coerce=int, validators=[validators.required()] "Location", coerce=str, validators=[validators.required()]
) )
starttime = DateTimeField( starttime = DateTimeField(
"Starttime", default=datetime.now, format="%d-%m-%Y %H:%M" "Starttime", default=datetime.now, format="%d-%m-%Y %H:%M"
@ -47,15 +47,15 @@ class OrderForm(Form):
class OrderItemForm(Form): class OrderItemForm(Form):
"Class which defines the form for a new Item in an Order" "Class which defines the form for a new Item in an Order"
# pylint: disable=R0903 # pylint: disable=R0903
product_id = SelectField("Item", coerce=int) dish_id = SelectField("Dish")
extra = StringField("Extra") comment = StringField("Comment")
submit_button = SubmitField("Submit") submit_button = SubmitField("Submit")
def populate(self, location: Location) -> None: def populate(self, location: Location) -> None:
"Fill in all the product options from the location" "Fill in all the dish options from the location"
self.product_id.choices = [ self.dish_id.choices = [
(i.id, (i.name + ": " + euro_string(i.price))) (i.id, (i.name + ": " + euro_string(i.price)))
for i in location.products for i in location.dishes
] ]
@ -68,7 +68,7 @@ class AnonOrderItemForm(OrderItemForm):
def populate(self, location: Location) -> None: def populate(self, location: Location) -> None:
""" """
Fill in all the product options from the location and Fill in all the dish options from the location and
the name of the anon user the name of the anon user
""" """
OrderItemForm.populate(self, location) OrderItemForm.populate(self, location)

View file

@ -2,6 +2,7 @@
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
from typing import Iterable, List, Mapping, Any, Optional from typing import Iterable, List, Mapping, Any, Optional
from utils import euro_string
def _format_tags(tags: Iterable[str]) -> str: def _format_tags(tags: Iterable[str]) -> str:
@ -13,7 +14,7 @@ def _format_tags(tags: Iterable[str]) -> str:
def _format_price(price: int) -> str: def _format_price(price: int) -> str:
return " {}.{:02}".format(*divmod(price, 100)) if price else "" return " {}".format(euro_string(price)) if price else ""
def _format_type_and_choice(type_and_choice): def _format_type_and_choice(type_and_choice):

View file

@ -2,6 +2,8 @@
import typing import typing
from datetime import datetime from datetime import datetime
from utils import first
from hlds.definitions import location_definitions
from .database import db from .database import db
from .user import User from .user import User
@ -18,13 +20,12 @@ class Order(db.Model):
items = db.relationship("OrderItem", backref="order", lazy="dynamic") items = db.relationship("OrderItem", backref="order", lazy="dynamic")
def configure(self, courier: User, def __getattr__(self, name):
starttime: db.DateTime, stoptime: db.DateTime,) -> None: if name == "location":
"Configure the Order" location = first(filter(lambda l: l.id == self.location_id, location_definitions))
# pylint: disable=W0201 if location:
self.courier = courier return location
self.starttime = starttime raise AttributeError()
self.stoptime = stoptime
def __repr__(self) -> str: def __repr__(self) -> str:
# pylint: disable=R1705 # pylint: disable=R1705
@ -33,6 +34,14 @@ class Order(db.Model):
else: else:
return "Order %d" % (self.id) return "Order %d" % (self.id)
def update_from_hlds(self) -> None:
"""
Update the location name from the HLDS definition.
User should commit after running this to make the change persistent.
"""
assert self.location_id, "location_id must be configured before updating from HLDS"
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]:
"Group items of an Order by user" "Group items of an Order by user"
group: typing.Dict[str, typing.Any] = dict() group: typing.Dict[str, typing.Any] = dict()
@ -44,20 +53,20 @@ class Order(db.Model):
item.price if not item.paid else 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["products"] = user.get("products", []) + [item.dish_name] user["dishes"] = user.get("dishes", []) + [item.dish_name]
group[item.get_name()] = user group[item.get_name()] = user
return group return group
def group_by_product(self) -> typing.Dict[str, typing.Any]: def group_by_dish(self) -> typing.Dict[str, typing.Any]:
"Group items of an Order by product" "Group items of an Order by dish"
group: typing.Dict[str, typing.Any] = dict() group: typing.Dict[str, typing.Any] = dict()
for item in self.items: for item in self.items:
product = group.get(item.dish_name, dict()) dish = group.get(item.dish_name, dict())
product["count"] = product.get("count", 0) + 1 dish["count"] = dish.get("count", 0) + 1
if item.extra: if item.comment:
product["extras"] = product.get("extras", []) + [item.comment] dish["comments"] = dish.get("comments", []) + [item.comment]
group[item.dish_name] = product group[item.dish_name] = dish
return group return group

View file

@ -1,6 +1,8 @@
"Script for everything OrderItem related in the database" "Script for everything OrderItem related in the database"
from datetime import datetime from datetime import datetime
from utils import first
from hlds.definitions import location_definitions
from .database import db from .database import db
from .order import Order from .order import Order
from .user import User from .user import User
@ -23,17 +25,21 @@ class OrderItem(db.Model):
choices = db.relationship("OrderItemChoice", backref="order_item", lazy="dynamic") choices = db.relationship("OrderItemChoice", backref="order_item", lazy="dynamic")
def configure(self, user: User, order: Order) -> None: def __getattr__(self, name):
"Configure the OrderItem" if name == "dish":
# pylint: disable=W0201 location_id = Order.query.filter(Order.id == self.order_id).first().location_id
self.user = user location = first(filter(lambda l: l.id == location_id, location_definitions))
self.order = order if location:
return first(filter(lambda d: d.id == self.dish_id, location.dishes))
else:
raise ValueError("No Location found with id: " + location_id)
raise AttributeError()
def get_name(self) -> str: def get_name(self) -> str:
"Get the name of the user which 'owns' the item" "Get the name of the user which 'owns' the item"
if self.user_id is not None and self.user_id > 0: if self.user_id is not None and self.user_id > 0:
return self.user.username return self.user.username
return self.name return self.user_name
def __repr__(self) -> str: def __repr__(self) -> str:
return "Order %d: %s wants %s" % ( return "Order %d: %s wants %s" % (
@ -42,6 +48,16 @@ class OrderItem(db.Model):
self.dish_name or "None", self.dish_name or "None",
) )
def update_from_hlds(self) -> None:
"""
Update the dish name and price from the HLDS definition.
User should commit after running this to make the change persistent.
"""
assert self.order_id, "order_id must be configured before updating from HLDS"
assert self.dish_id, "dish_id must be configured before updating from HLDS"
self.dish_name = self.dish.name
self.price = self.dish.price
# pylint: disable=W0613 # pylint: disable=W0613
def can_delete(self, order_id: int, user_id: int, name: str) -> bool: 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"

View file

@ -10,30 +10,30 @@ from flask import url_for
from models.order import Order from models.order import Order
def webhook_text(order_item: Order) -> typing.Optional[str]: 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 "Testlocation" in order_item.location.name: if order.location_id == "test":
return None return None
if order_item.courier is not None: if order.courier is not None:
# pylint: disable=C0301 # pylint: disable=C0301
return "<!channel|@channel> {3} is going to {1}, order <{0}|here>! Deadline in {2} minutes!".format( return "<!channel|@channel> {3} is going to {1}, order <{0}|here>! Deadline in {2} minutes!".format(
url_for("order_bp.order_from_id", order_id=order_item.id, _external=True), url_for("order_bp.order_from_id", order_id=order.id, _external=True),
order_item.location.name, order.location_name,
remaining_minutes(order_item.stoptime), remaining_minutes(order.stoptime),
order_item.courier.username.title(), order.courier.username.title(),
) )
return "<!channel|@channel> New order for {}. Deadline in {} minutes. <{}|Open here.>".format( return "<!channel|@channel> New order for {}. Deadline in {} minutes. <{}|Open here.>".format(
order_item.location.name, order.location_name,
remaining_minutes(order_item.stoptime), remaining_minutes(order.stoptime),
url_for("order_bp.order_from_id", order_id=order_item.id, _external=True), url_for("order_bp.order_from_id", order_id=order.id, _external=True),
) )
def post_order_to_webhook(order_item: Order) -> None: 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_item) message = webhook_text(order)
if message: if message:
webhookthread = WebhookSenderThread( webhookthread = WebhookSenderThread(
message, app.config["SLACK_WEBHOOK"]) message, app.config["SLACK_WEBHOOK"])

View file

@ -43,15 +43,15 @@
<a class="btn btn-primary" onclick="chooseRandom()">Choose for me</a> <a class="btn btn-primary" onclick="chooseRandom()">Choose for me</a>
</span> </span>
{{ form.csrf_token }} {{ form.csrf_token }}
<div class="form-group select2-container select2 {{ 'has-errors' if form.product_id.errors}}"> <div class="form-group select2-container select2 {{ 'has-errors' if form.dish_id.errors}}">
{{ form.product_id.label(class='control-label') }}<br> {{ form.dish_id.label(class='control-label') }}<br>
{{ form.product_id(class='form-control select') }} {{ form.dish_id(class='form-control select') }}
{{ util.render_form_field_errors(form.product_id) }} {{ util.render_form_field_errors(form.dish_id) }}
</div> </div>
<div class="form-group {{ 'has-errors' if form.product_id.errors }}"> <div class="form-group {{ 'has-errors' if form.dish_id.errors }}">
{{ form.extra.label(class='control-label') }}<br> {{ form.comment.label(class='control-label') }}<br>
{{ form.extra(class='form-control', placeholder='Fill in extras, when applicable') }} {{ form.comment(class='form-control', placeholder='Fill in comment, when applicable') }}
{{ util.render_form_field_errors(form.extra) }} {{ util.render_form_field_errors(form.comment) }}
</div> </div>
{% if current_user.is_anonymous() %} {% if current_user.is_anonymous() %}
<div class="form-group{{ ' has-error' if form.name.errors }}{{ ' required' if form.name.flags.required }}"> <div class="form-group{{ ' has-error' if form.name.errors }}{{ ' required' if form.name.flags.required }}">
@ -78,8 +78,8 @@
{% for item in order.items -%} {% for item in order.items -%}
<tr> <tr>
<td>{{ item.get_name() }}</td> <td>{{ item.get_name() }}</td>
<td><span title="{{ item.extra if item.extra }}">{{ item.product.name }}{{ "*" if item.extra }}</span></td> <td><span title="{{ item.comment if item.comment }}">{{ item.dish_name }}{{ "*" if item.comment }}</span></td>
<td>{{ item.product.price|euro }}</td> <td>{{ item.price|euro }}</td>
{% if courier_or_admin %}<td>{% if not item.paid %} {% if courier_or_admin %}<td>{% if not item.paid %}
<form action="{{ url_for('order_bp.item_paid', order_id=order.id, item_id=item.id) }}" method="post" style="display:inline"> <form action="{{ url_for('order_bp.item_paid', order_id=order.id, item_id=item.id) }}" method="post" style="display:inline">
<input type="submit" class="btn btn-xs btn-primary" value="Pay"></input> <input type="submit" class="btn btn-xs btn-primary" value="Pay"></input>
@ -98,15 +98,15 @@
</table> </table>
</div> </div>
<div class="col-md-push-2 col-md-4 darker box order_ordered" id="items-ordered"> <div class="col-md-push-2 col-md-4 darker box order_ordered" id="items-ordered">
<h3>Ordered products: {{ order.items.count() }}</h3> <h3>Ordered dishes: {{ order.items.count() }}</h3>
<a class="divLink" href="{{ url_for('order_bp.items_showcase', order_id=order.id) }}"></a> <a class="divLink" href="{{ url_for('order_bp.items_showcase', order_id=order.id) }}"></a>
{% for key, value in order.group_by_product().items() -%} {% for key, value in order.group_by_dish().items() -%}
<div class="product"> <div class="product">
{{ key }}: {{ value["count"] }} {{ key }}: {{ value["count"] }}
{% if value["extras"] -%} {% if value["comments"] -%}
<div class="extras"> <div class="comments">
{% for extra in value["extras"] -%} {% for comment in value["comments"] -%}
<div>- {{ extra }}</div> <div>- {{ comment }}</div>
{% endfor %} {% endfor %}
</div> </div>
{%- endif %} {%- endif %}

View file

@ -22,8 +22,8 @@ Haldis - Order {{ order.id }}
<div class="row"> <div class="row">
<div class="col-sm-6 col-sm-offset-3 darker showcase" id="items-ordered"> <div class="col-sm-6 col-sm-offset-3 darker showcase" id="items-ordered">
<h3 class="text-center">Ordered products: {{ order.items.count() }}</h3> <h3 class="text-center">Ordered dishes: {{ order.items.count() }}</h3>
{% for key, value in order.group_by_product().items() -%} {% for key, value in order.group_by_dish().items() -%}
<div class="product text-center"> <div class="product text-center">
<p> <p>
{{ key }}: {{ value["count"] }} {{ key }}: {{ value["count"] }}

View file

@ -1,9 +1,21 @@
"Script which contains several utils for Haldis" "Script which contains several utils for Haldis"
from typing import Iterable
def euro_string(value: int) -> str: def euro_string(value: int) -> str:
""" """
Convert cents to string formatted euro Convert cents to string formatted euro
""" """
result = "{:.2f}".format(round(value / 100, 2)) return "{}.{:02}".format(*divmod(value, 100))
return result
def first(iterable: Iterable, default=None):
"""
Return first element of iterable
"""
try:
return next(iter(iterable))
except StopIteration:
return default

View file

@ -12,6 +12,7 @@ from flask import current_app as app
from flask import send_from_directory, url_for from flask import send_from_directory, url_for
from flask_login import login_required from flask_login import login_required
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
@ -21,13 +22,6 @@ from views.order import get_orders
general_bp = Blueprint("general_bp", __name__) general_bp = Blueprint("general_bp", __name__)
def _first(iterable: typing.Iterable, default=None):
try:
return next(iter(iterable))
except StopIteration:
return default
@general_bp.route("/") @general_bp.route("/")
def home() -> str: def home() -> str:
"Generate the home view" "Generate the home view"
@ -131,7 +125,7 @@ def locations() -> str:
@general_bp.route("/location/<location_id>") @general_bp.route("/location/<location_id>")
def location(location_id) -> str: def location(location_id) -> str:
"Generate the location view given an id" "Generate the location view given an id"
loc = _first(filter(lambda l: l.id == location_id, location_definitions)) loc = first(filter(lambda l: l.id == location_id, location_definitions))
if loc is None: if loc is None:
abort(404) abort(404)
return render_template("location.html", location=loc, title=loc.name) return render_template("location.html", location=loc, title=loc.name)

View file

@ -37,6 +37,7 @@ def order_create() -> typing.Union[str, Response]:
if orderForm.validate_on_submit(): if orderForm.validate_on_submit():
order = Order() order = Order()
orderForm.populate_obj(order) orderForm.populate_obj(order)
order.update_from_hlds()
db.session.add(order) db.session.add(order)
db.session.commit() db.session.commit()
post_order_to_webhook(order) post_order_to_webhook(order)
@ -59,8 +60,8 @@ def order_from_id(order_id: int, form: OrderForm = None) -> str:
form.populate(order.location) form.populate(order.location)
if order.stoptime and order.stoptime < datetime.now(): if order.stoptime and order.stoptime < datetime.now():
form = None form = None
total_price = sum([o.product.price for o in order.items]) total_price = sum([o.price for o in order.items])
debts = sum([o.product.price for o in order.items if not o.paid]) debts = sum([o.price for o in order.items if not o.paid])
return render_template("order.html", order=order, form=form, return render_template("order.html", order=order, form=form,
total_price=total_price, debts=debts) total_price=total_price, debts=debts)
@ -121,9 +122,10 @@ def order_item_create(order_id: int) -> typing.Any:
item.user_id = current_user.id item.user_id = current_user.id
else: else:
session["anon_name"] = item.name session["anon_name"] = item.name
item.update_from_hlds()
db.session.add(item) db.session.add(item)
db.session.commit() db.session.commit()
flash("Ordered %s" % (item.product.name), "success") flash("Ordered %s" % (item.dish_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))
return order_from_id(order_id, form=form) return order_from_id(order_id, form=form)
@ -138,7 +140,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.product.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)
@ -184,10 +186,10 @@ def delete_item(order_id: int, item_id: int) -> typing.Any:
print("%s tries to delete orders" % (current_user.username)) print("%s tries to delete orders" % (current_user.username))
user_id = current_user.id 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", "")):
product_name = item.product.name dish_name = item.dish_name
db.session.delete(item) db.session.delete(item)
db.session.commit() db.session.commit()
flash("Deleted %s" % (product_name), "success") flash("Deleted %s" % (dish_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)