Merge pull request #187 from ZeusWPI/feature/invitelink-token

Feature/invitelink token
This commit is contained in:
Maxime 2022-05-20 19:15:20 +02:00 committed by GitHub
commit f87f3c5446
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 129 additions and 94 deletions

View file

@ -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

View file

@ -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():

View file

@ -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 "<!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.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 "<!channel|@channel> 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"

2
app/static/js/jquery.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
app/static/js/qrcode.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -12,10 +12,27 @@
{% block metas %}
{{ super() }}
<meta name="robots" content="noindex, nofollow">
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/qrcode.min.js') }}"></script>
{% endblock %}
{% block container %}
<header>
<header class="row">
<div class="col-md-2" style="padding-top: 2em">
<div id="qrcode"></div>
<script type="text/javascript">
var qrcode = new QRCode(document.getElementById("qrcode"), {
text: "{{ url_for("order_bp.order_from_slug", order_slug=order.slug, _external=True) }}",
width: 128,
height: 128,
colorDark : "#000000",
colorLight : "#ffffff",
correctLevel : QRCode.CorrectLevel.H
});
</script>
</div>
<div class="col-md-10">
<h2 id="order-title">Order {{ order.id }}</h2>
<div class="location">
@ -25,6 +42,10 @@
{{ order.location_name }}
{% endif %}
</div>
<div>
Unique order link: <code>{{ url_for("order_bp.order_from_slug", order_slug=order.slug, _external=True) }}</code>
</div>
</div>
</header>
<section>
@ -36,7 +57,7 @@
{% for item in my_items %}
<li class="spacecake">
{% if item.can_delete(order.id, current_user.id, session.get('anon_name', '')) -%}
<form action="{{ url_for('order_bp.delete_item', order_id=order.id, item_id=item.id) }}" method="post" style="display:inline">
<form action="{{ url_for('order_bp.delete_item', order_slug=order.slug, item_id=item.id) }}" method="post" style="display:inline">
<button class="btn btn-link btn-sm" type="submit" style="padding: 0 0.5em;"><span class="glyphicon glyphicon-remove"></span></button>
</form>
{%- endif %}
@ -65,7 +86,7 @@
<h3>Add item to order</h3>
{% for dish in order.location.dishes %}
<form method="post" action="{{ url_for('order_bp.order_item_create', order_id=order.id) }}" id="dish_{{ dish.id }}">
<form method="post" action="{{ url_for('order_bp.order_item_create', order_slug=order.slug) }}" id="dish_{{ dish.id }}">
{{ form.csrf_token }}
<input type="hidden" name="dish_id" value="{{ dish.id }}" />
@ -167,7 +188,7 @@
<dd>
{% if order.courier == None %}
{% if not current_user.is_anonymous() %}
<form action="{{ url_for('order_bp.volunteer', order_id=order.id) }}" method="post" style="display:inline">
<form action="{{ url_for('order_bp.volunteer', order_slug=order.slug) }}" method="post" style="display:inline">
<input type="submit" class="btn btn-primary btn-sm" value="Volunteer"></input>
</form>
{% else %}No-one yet{% endif %}
@ -180,12 +201,12 @@
<div>
{% if order.can_close(current_user.id) -%}
<form action="{{ url_for('order_bp.close_order', order_id=order.id) }}" method="post" style="display:inline">
<form action="{{ url_for('order_bp.close_order', order_slug=order.slug) }}" method="post" style="display:inline">
<input type="submit" class="btn btn-danger" value="Close"></input>
</form>
{% endif %}
{% if courier_or_admin %}
<a class="btn" href="{{ url_for('order_bp.order_edit', order_id=order.id) }}">Edit</a>
<a class="btn" href="{{ url_for('order_bp.order_edit', order_slug=order.slug) }}">Edit</a>
{%- endif %}
</div>
</div>
@ -258,7 +279,7 @@
<div class="footer">
Total {{ order.items.count() }} items — {{ total_price|euro }}
&nbsp;
<a class="btn btn-sm" href="{{ url_for('order_bp.items_shop_view', order_id=order.id) }}">Shop view</a>
<a class="btn btn-sm" href="{{ url_for('order_bp.items_shop_view', order_slug=order.slug) }}">Shop view</a>
</div>
</div>
</div>
@ -267,7 +288,7 @@
<section class="single_column">
<div class="box" id="per_person">
<h3>Items per person</h3>
<form action="{{ url_for('order_bp.modify_items', order_id=order.id) }}" method="post">
<form action="{{ url_for('order_bp.modify_items', order_slug=order.slug) }}" method="post">
<table class="table table-condensed">
<thead>
<tr><th>Total</th><th>Name</th><th>Items</th></tr>
@ -293,7 +314,9 @@
<li class="{{ 'paid' if item.paid }}">
<div class="actions">
{% if item.can_delete(order.id, current_user.id, session.get('anon_name', '')) -%}
<button class="btn btn-link btn-sm" type="submit" name="delete_item" value="{{ item.id }}" style="padding: 0 0.5em;"><span class="glyphicon glyphicon-remove"></span></button>
<form action="{{ url_for('order_bp.delete_item', order_slug=order.slug, item_id=item.id) }}" method="post" style="display:inline">
<button class="btn btn-link btn-sm" type="submit" style="padding: 0 0.5em;"><span class="glyphicon glyphicon-remove"></span></button>
</form>
{% else %}
<span class="glyphicon glyphicon-remove" style="color: var(--gray3); padding: 0 0.5em; cursor: not-allowed"></span>
{%- endif %}

View file

@ -11,7 +11,7 @@
<h3>Edit order</h3>
<div class="row darker">
<div class="col-sm-12">
<form method="post" action="{{ url_for('.order_edit', order_id=order_id) }}">
<form method="post" action="{{ url_for('.order_edit', order_slug=order_slug) }}">
{{ form.csrf_token }}
<div class="form-group select2 {{ 'has-errors' if form.courier_id.errors else ''}}">
{{ form.courier_id.label(class='control-label') }}<br>

View file

@ -9,7 +9,7 @@
{% else %}open{% endif %}<br/>
</div>
<div class="col-md-4 col-lg-3 expand_button_wrapper">
<a class="btn btn-primary btn-block align-bottom expand_button" href="{{ url_for('order_bp.order_from_id', order_id=order.id) }}">Expand</a>
<a class="btn btn-primary btn-block align-bottom expand_button" href="{{ url_for('order_bp.order_from_slug', order_slug=order.slug) }}">Expand</a>
</div>
</div>
{%- endmacro %}

View file

@ -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("/<order_id>")
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("/<order_slug>")
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("/<order_id>/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("/<order_slug>/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("/<order_id>/edit", methods=["GET", "POST"])
@order_bp.route("/<order_slug>/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("/<order_id>/create", methods=["GET", "POST"])
def order_item_create(order_id: int) -> typing.Any:
@order_bp.route("/<order_slug>/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():
@ -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("/<order_id>/modify_items", methods=["POST"])
@order_bp.route("/<order_slug>/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("/<order_id>/<item_id>/delete", methods=["POST"])
@order_bp.route("/<order_slug>/<item_id>/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("/<order_id>/volunteer", methods=["POST"])
@order_bp.route("/<order_slug>/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("/<order_id>/close", methods=["POST"])
@order_bp.route("/<order_slug>/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) & (