Compare commits

..

3 commits
master ... slug

Author SHA1 Message Date
Midgard 0ebb877070
Create a slug for old orders in the migration 2022-05-25 13:57:02 +02:00
Midgard 105cf4a044
Revert "Don't crash on orders that don't have a slug"
This reverts commit ff0ea068de.

The next commit (Create a slug for old orders in the migration) will
change our strategy of writing code to handle the legacy slugless orders:
we will generate a slug for old orders.
2022-05-25 13:56:07 +02:00
Midgard ff0ea068de
Don't crash on orders that don't have a slug
Orders created before we introduced slugs don't have a slug. This commit
introduces code to work with them. Without these changes, the legacy
orders are not reachable any more, and trying to create a link for them,
crashes the page.

I wrote this commit because in my test environment I had a long-lived
order for testing purposes, and the home page crashed because the order
would show up in the list of Open Orders.
2022-05-25 10:55:33 +02:00
21 changed files with 113 additions and 222 deletions

View file

@ -1 +0,0 @@
python 3.9.2

View file

@ -10,5 +10,5 @@ def add() -> None:
"""Add users as admin."""
for username in Configuration.HALDIS_ADMINS:
user = User()
user.configure(username, True, 0, associations=["zeus"])
user.configure(username, True, 0)
db.session.add(user)

View file

@ -28,12 +28,11 @@ class OrderAdminModel(ModelBaseView):
"Class for the model of a OrderAdmin"
# pylint: disable=too-few-public-methods
column_default_sort = ("starttime", True)
column_list = ["starttime", "stoptime", "location_name", "location_id", "courier", "association"]
column_list = ["starttime", "stoptime", "location_name", "location_id", "courier"]
column_labels = {
"starttime": "Start Time",
"stoptime": "Closing Time",
"location_id": "HLDS Location ID",
"association": "Association",
}
form_excluded_columns = ["items", "courier_id"]
can_delete = False

View file

@ -3,14 +3,12 @@
"""Main Haldis script"""
import logging
import sentry_sdk
import typing
from datetime import datetime
from logging.handlers import TimedRotatingFileHandler
from admin import init_admin
from config import Configuration
from flask import Flask, render_template, Response
from flask import Flask, render_template
from flask_bootstrap import Bootstrap, StaticCDN
from flask_debugtoolbar import DebugToolbarExtension
from flask_login import LoginManager
@ -21,7 +19,6 @@ from login import init_login
from markupsafe import Markup
from models import db
from models.anonymous_user import AnonymouseUser
from sentry_sdk.integrations.flask import FlaskIntegration
from utils import euro_string, price_range_string, ignore_none
from zeus import init_oauth
@ -161,12 +158,6 @@ def create_app():
"""Initializer for the Flask app object"""
app = Flask(__name__)
@app.route('/robots.txt')
def noindex():
r = Response(response="User-Agent: *\nDisallow: /\n", status=200, mimetype="text/plain")
r.headers["Content-Type"] = "text/plain; charset=utf-8"
return r
# Load the config file
app.config.from_object("config.Configuration")
@ -180,11 +171,5 @@ def create_app():
# For usage when you directly call the script with python
if __name__ == "__main__":
if Configuration.SENTRY_DSN:
sentry_sdk.init(
dsn=Configuration.SENTRY_DSN,
integrations=[FlaskIntegration()]
)
app, app_mgr = create_app()
app_mgr.run()

View file

@ -12,6 +12,5 @@ class Configuration:
SECRET_KEY = "<change>"
SLACK_WEBHOOK = None
LOGFILE = "haldis.log"
SENTRY_DSN = None
ZEUS_KEY = "tomtest"
ZEUS_SECRET = "blargh"

View file

@ -24,17 +24,13 @@ class OrderForm(Form):
"Starttime", default=datetime.now, format="%d-%m-%Y %H:%M"
)
stoptime = DateTimeField("Stoptime", format="%d-%m-%Y %H:%M")
association = SelectField("Association", coerce=str, validators=[validators.required()])
submit_button = SubmitField("Submit")
def populate(self) -> None:
"Fill in the options for courier for an Order"
if current_user.is_admin():
self.courier_id.choices = [
(0, None),
(current_user.id, current_user.username),
] + [
(u.id, u.username) for u in User.query.order_by("username") if u.id != current_user.id
self.courier_id.choices = [(0, None)] + [
(u.id, u.username) for u in User.query.order_by("username")
]
else:
self.courier_id.choices = [
@ -42,7 +38,6 @@ class OrderForm(Form):
(current_user.id, current_user.username),
]
self.location_id.choices = [(l.id, l.name) for l in location_definitions]
self.association.choices = current_user.association_list()
if self.stoptime.data is None:
self.stoptime.data = datetime.now() + timedelta(hours=1)

View file

@ -17,14 +17,29 @@ from sqlalchemy.sql import text
def upgrade():
op.add_column('order', sa.Column(
'slug',
sa.String(length=8),
sa.String(length=7),
nullable=False,
# Default: random alphanumerical string
server_default=text('SUBSTRING(MD5(RAND()) FROM 1 FOR 7)')
))
op.create_unique_constraint('order_slug_unique', 'order', ['slug'])
# Trigger to handle duplicates: generate new slug if slug already exists
op.execute(text(
"""
CREATE TRIGGER order_before_insert
BEFORE UPDATE ON `order`
FOR EACH ROW
BEGIN
WHILE (NEW.slug IS NULL OR (SELECT id FROM `order` WHERE slug = NEW.slug) IS NOT NULL) DO
SET NEW.slug = SUBSTRING(MD5(RAND()) FROM 1 FOR 7);
END WHILE;
END
"""
))
def downgrade():
op.execute(text("DROP TRIGGER order_before_insert"))
op.drop_constraint('order_slug_unique', 'order', type_='unique')
op.drop_column('order', 'slug')

View file

@ -1,22 +0,0 @@
"""empty message
Revision ID: 9eac0f3d7b1e
Revises: ('f6a6004bf4b9', '29ccbe077c57')
Create Date: 2022-05-30 18:35:43.918797
"""
# revision identifiers, used by Alembic.
revision = '9eac0f3d7b1e'
down_revision = ('f6a6004bf4b9', '29ccbe077c57')
from alembic import op
import sqlalchemy as sa
def upgrade():
pass
def downgrade():
pass

View file

@ -1,28 +0,0 @@
"""Add user associations
Revision ID: f6a6004bf4b9
Revises: 55013fe95bea
Create Date: 2022-05-24 21:23:27.770365
"""
# revision identifiers, used by Alembic.
revision = 'f6a6004bf4b9'
down_revision = '55013fe95bea'
from alembic import op
import sqlalchemy as sa
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('order', sa.Column('association', sa.String(length=120), server_default='', nullable=False))
op.add_column('user', sa.Column('associations', sa.String(length=255), server_default='', nullable=False))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('user', 'associations')
op.drop_column('order', 'association')
# ### end Alembic commands ###

View file

@ -1,14 +1,10 @@
"AnonymouseUser for people who are not logged in the normal way"
from typing import List
# pylint: disable=R0201,C0111
class AnonymouseUser:
id = None
def association_list(self) -> List[str]:
return []
def is_active(self) -> bool:
return False

View file

@ -2,7 +2,6 @@
import typing
from collections import defaultdict
from datetime import datetime
import secrets
import string
from hlds.definitions import location_definitions
@ -11,13 +10,6 @@ from utils import first
from .database import db
from .user import User
BASE31_ALPHABET = '23456789abcdefghjkmnpqrstuvwxyz'
def generate_slug():
secret = ''.join(secrets.choice(BASE31_ALPHABET) for i in range(8))
while Order.query.filter(Order.slug == secret).first() is not None:
secret = ''.join(secrets.choice(BASE31_ALPHABET) for i in range(8))
return secret
class Order(db.Model):
"""Class used for configuring the Order model in the database"""
@ -28,8 +20,9 @@ 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(8), default=generate_slug, unique=True)
association = db.Column(db.String(120), nullable=False, server_default="")
# The default value for `slug`, a random 7-character alphanumerical string,
# is created on the database side. See migrations/versions/29ccbe077c57_add_slug.py
slug = db.Column(db.String(7), unique=True)
items = db.relationship("OrderItem", backref="order", lazy="dynamic")

View file

@ -1,6 +1,4 @@
"Script for everything User related in the database"
from typing import List, Optional
from models import db
@ -10,10 +8,6 @@ class User(db.Model):
username = db.Column(db.String(80), unique=True, nullable=False)
admin = db.Column(db.Boolean)
bias = db.Column(db.Integer)
# Assocation logic
associations = db.Column(db.String(255), nullable=False, server_default="")
# Relations
runs = db.relation(
"Order",
backref="courier",
@ -22,17 +16,11 @@ class User(db.Model):
)
orderItems = db.relationship("OrderItem", backref="user", lazy="dynamic")
def association_list(self) -> List[str]:
return self.associations.split(",")
def configure(self, username: str, admin: bool, bias: int, associations: Optional[List[str]] = None) -> None:
"""Configure the User"""
if associations is None:
associations = []
def configure(self, username: str, admin: bool, bias: int) -> None:
"Configure the User"
self.username = username
self.admin = admin
self.bias = bias
self.associations = ",".join(associations)
# pylint: disable=C0111, R0201
def is_authenticated(self) -> bool:

View file

@ -21,7 +21,7 @@ def webhook_text(order: Order) -> typing.Optional[str]:
url_for("order_bp.order_from_slug", order_slug=order.slug, _external=True),
order.location_name,
remaining_minutes(order.stoptime),
order.courier.username,
order.courier.username.title(),
)
# pylint: disable=C0209

View file

@ -155,66 +155,60 @@
<div class="box" id="order_info">
<h3>Order information</h3>
<div class="row">
<dl class="col-md-10 col-lg-8">
<div>
<dt>Order opens</dt>
<dd>{{ order.starttime.strftime("%Y-%m-%d, %H:%M") }}</dd>
<dl>
<div>
<dt>Order opens</dt>
<dd>{{ order.starttime.strftime("%Y-%m-%d, %H:%M") }}</dd>
<dt>Order closes</dt>
<dd>
{% if order.stoptime %}
{% set stoptimefmt = (
"%H:%M" if order.stoptime.date() == order.starttime.date()
else "%Y-%m-%d, %H:%M"
) %}
{{ order.stoptime.strftime(stoptimefmt) }} ({{ order.stoptime|countdown }})
{% else %}
Never
{% endif %}
</dd>
</div>
<div>
<dt>Location</dt>
<dd>
{% if order.location %}
<a href="{{ url_for('general_bp.location', location_id=order.location_id) }}">{{ order.location_name }}</a>
{% else %}
{{ order.location_name }}
{% endif %}
</dd>
<dt>Courier</dt>
<dd>
{% if order.courier == None %}
{% if not current_user.is_anonymous() %}
<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 %}
{% else %}
{{ order.courier.username }}
{% endif %}
</dd>
</div>
</dl>
<div class="col-md-2 col-lg-4">
<img src="https://dsa.ugent.be/api/verenigingen/{{ order.association }}/logo" class="img-responsive align-top" style="max-width:200px;width:100%">
<dt>Order closes</dt>
<dd>
{% if order.stoptime %}
{% set stoptimefmt = (
"%H:%M" if order.stoptime.date() == order.starttime.date()
else "%Y-%m-%d, %H:%M"
) %}
{{ order.stoptime.strftime(stoptimefmt) }} ({{ order.stoptime|countdown }})
{% else %}
Never
{% endif %}
</dd>
</div>
<div>
<dt>Location</dt>
<dd>
{% if order.location %}
<a href="{{ url_for('general_bp.location', location_id=order.location_id) }}">{{ order.location_name }}</a>
{% else %}
{{ order.location_name }}
{% endif %}
</dd>
<dt>Courier</dt>
<dd>
{% if order.courier == None %}
{% if not current_user.is_anonymous() %}
<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 %}
{% else %}
{{ order.courier.username }}
{% endif %}
</dd>
</div>
</dl>
<div>
{% if order.can_close(current_user.id) -%}
<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_slug=order.slug) }}">Edit</a>
{%- endif %}
</div>
{% if order.can_close(current_user.id) -%}
<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_slug=order.slug) }}">Edit</a>
{%- endif %}
</div>
<div class="box" id="how_to_order">
@ -320,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

@ -38,11 +38,6 @@
{{ form.location_id(class='form-control select') }}
{{ util.render_form_field_errors(form.location_id) }}
</div>
<div class="form-group select2 {{ 'has-errors' if form.association.errors else ''}}{{ ' required' if form.association.flags.required }}">
{{ form.association.label(class='control-label') }}
{{ form.association(class='form-control select') }}
{{ util.render_form_field_errors(form.association) }}
</div>
{% if current_user.is_admin() %}
<div class="form-group{{ ' has-error' if form.starttime.errors }}{{ ' required' if form.starttime.flags.required }}{{ ' hidden' if not current_user.is_admin() }}">
{{ form.starttime.label(class='control-label') }}

View file

@ -1,17 +1,14 @@
{% macro render_order(order) -%}
<div class="row order_row">
<div class="col-md-6 order_data">
<div class="col-md-8 col-lg-9 order_data">
<h5>{{ order.location_name }}</h5>
<b class="amount_of_orders">{{ order.items.count() }} items ordered for {{ order.association }}</b></p>
<b class="amount_of_orders">{{ order.items.count() }} orders</b></p>
<p class="time_data">
{% if order.stoptime %}
<span><b>Closes </b>{{ order.stoptime.strftime("%H:%M") }}</span>{{ order.stoptime|countdown }}
{% else %}open{% endif %}<br/>
</div>
<div class="col-md-3">
<img src="https://dsa.ugent.be/api/verenigingen/{{ order.association }}/logo" class="img-responsive align-bottom" style="max-width:200px;width:100%">
</div>
<div class="col-md-3 expand_button_wrapper">
<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_slug', order_slug=order.slug) }}">Expand</a>
</div>
</div>

View file

@ -9,7 +9,7 @@ from flask import Blueprint, Flask, abort
from flask import current_app as app
from flask import (jsonify, make_response, render_template, request,
send_from_directory, url_for)
from flask_login import current_user, login_required
from flask_login import login_required
from hlds.definitions import location_definitions
from hlds.models import Location
from models import Order
@ -34,9 +34,7 @@ def home() -> str:
(Order.stoptime > prev_day) & (Order.stoptime < datetime.now())
)
return render_template(
"home.html", orders=get_orders(
((datetime.now() > Order.starttime) & (Order.stoptime > datetime.now()) | (Order.stoptime == None))
), recently_closed=recently_closed
"home.html", orders=get_orders(), recently_closed=recently_closed
)

View file

@ -21,7 +21,7 @@ order_bp = Blueprint("order_bp", "order")
@order_bp.route("/")
def orders(form: OrderForm = None) -> str:
"""Generate general order view"""
if form is None and current_user.association_list():
if form is None and not current_user.is_anonymous():
form = OrderForm()
location_id = request.args.get("location_id")
form.location_id.default = location_id
@ -34,9 +34,6 @@ def orders(form: OrderForm = None) -> str:
@login_required
def order_create() -> typing.Union[str, Response]:
"""Generate order create view"""
if not current_user.association_list():
flash("Not allowed to create an order.", "info")
abort(401)
orderForm = OrderForm()
orderForm.populate()
if orderForm.validate_on_submit():
@ -396,17 +393,16 @@ def get_orders(expression=None) -> typing.List[Order]:
"""Give the list of all currently open and public Orders"""
order_list: typing.List[OrderForm] = []
if expression is None:
expression = ((datetime.now() > Order.starttime) & (
Order.stoptime
> datetime.now()
# pylint: disable=C0121
) | (Order.stoptime == None)
) & (Order.association.in_(current_user.association_list()))
expression = (datetime.now() > Order.starttime) & (
Order.stoptime
> datetime.now()
# pylint: disable=C0121
) | (Order.stoptime == None)
if not current_user.is_anonymous():
order_list = Order.query.filter(expression).all()
else:
order_list = Order.query.filter(
# pylint: disable=C0121
expression & (Order.public == True) & (Order.association.in_(current_user.association_list()))
expression & (Order.public == True)
).all()
return order_list

View file

@ -77,7 +77,7 @@ def login_and_redirect_user(user) -> Response:
def create_user(username) -> User:
"Create a temporary user if it is needed"
user = User()
user.configure(username, False, 1, associations=["zeus"])
user.configure(username, False, 1)
db.session.add(user)
db.session.commit()
return user

View file

@ -12,4 +12,3 @@ black
pymysql
pyyaml
tatsu<5.6 # >=5.6 needs Python >=3.8
sentry-sdk[flask]

View file

@ -1,5 +1,5 @@
#
# This file is autogenerated by pip-compile with python 3.9
# This file is autogenerated by pip-compile
# To update, run:
#
# pip-compile
@ -11,15 +11,11 @@ appdirs==1.4.4
black==21.6b0
# via -r requirements.in
blinker==1.4
# via
# flask-debugtoolbar
# sentry-sdk
# via flask-debugtoolbar
cachelib==0.1.1
# via flask-oauthlib
certifi==2021.5.30
# via
# requests
# sentry-sdk
# via requests
chardet==4.0.0
# via requests
click==7.1.2
@ -28,19 +24,6 @@ click==7.1.2
# flask
dominate==2.6.0
# via flask-bootstrap
flask==1.1.4
# via
# -r requirements.in
# flask-admin
# flask-bootstrap
# flask-debugtoolbar
# flask-login
# flask-migrate
# flask-oauthlib
# flask-script
# flask-sqlalchemy
# flask-wtf
# sentry-sdk
flask-admin==1.5.8
# via -r requirements.in
flask-bootstrap==3.3.7.1
@ -61,6 +44,18 @@ flask-sqlalchemy==2.5.1
# flask-migrate
flask-wtf==0.15.1
# via -r requirements.in
flask==1.1.4
# via
# -r requirements.in
# flask-admin
# flask-bootstrap
# flask-debugtoolbar
# flask-login
# flask-migrate
# flask-oauthlib
# flask-script
# flask-sqlalchemy
# flask-wtf
greenlet==1.1.0
# via sqlalchemy
idna==2.10
@ -97,12 +92,10 @@ pyyaml==5.4.1
# via -r requirements.in
regex==2021.4.4
# via black
requests==2.25.1
# via requests-oauthlib
requests-oauthlib==1.1.0
# via flask-oauthlib
sentry-sdk[flask]==1.10.1
# via -r requirements.in
requests==2.25.1
# via requests-oauthlib
six==1.16.0
# via python-dateutil
sqlalchemy==1.4.18
@ -113,10 +106,8 @@ tatsu==4.4.0
# via -r requirements.in
toml==0.10.2
# via black
urllib3==1.26.12
# via
# requests
# sentry-sdk
urllib3==1.26.5
# via requests
visitor==0.1.3
# via flask-bootstrap
werkzeug==1.0.1