Compare commits

..

1 commit

Author SHA1 Message Date
mcbloch
02d52f08dd add validation check that choice options are correctly set 2022-05-15 00:04:50 +02:00
46 changed files with 253 additions and 673 deletions

View file

@ -1,10 +0,0 @@
# Ignore everything
*
# Include source, config and scripts
!app
!etc
!*.md
!*.sh
!*.txt
!LICENSE

View file

@ -1 +0,0 @@
python 3.9.2

View file

@ -1,26 +0,0 @@
# syntax=docker/dockerfile:1
FROM python:3.9.2-slim AS development
WORKDIR /src
RUN pip install pymysql
ADD https://git.zeus.gent/haldis/menus/-/archive/master/menus-master.tar /tmp
RUN mkdir menus && \
tar --directory=menus --extract --strip-components=1 --file=/tmp/menus-master.tar
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
WORKDIR /src/app
CMD python app.py db upgrade && \
python app.py runserver -h 0.0.0.0 -p 8000
FROM development AS production
RUN pip install waitress
CMD python app.py db upgrade && \
python waitress_wsgi.py

View file

@ -26,7 +26,7 @@ Afterwards upgrade the database to the latest version using
cd app
python3 app.py db upgrade
You can now still seed the database by running, note that you might want to put your name in the `HALDIS_ADMINS` in `app/config.py`
You can now still seed the database by running, note that you might want to put your name in the `HALDIS_ADMIN_USERS` in `app/config.py`
./populate-db.sh

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
@ -45,7 +44,6 @@ class OrderItemAdminModel(ModelBaseView):
column_default_sort = ("order_id", True)
column_list = [
"order_id",
"slug",
"order.location_name",
"user_name",
"user",

View file

@ -3,29 +3,24 @@
"""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
from flask_migrate import Migrate, MigrateCommand
from flask_oauthlib.client import OAuth, OAuthException
from flask_script import Manager, Server
from login import init_login
from markupsafe import Markup
from admin import init_admin
from auth.login import init_login
from auth.zeus import init_oauth
from config import Configuration
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
def register_plugins(app: Flask) -> Manager:
@ -102,22 +97,18 @@ def add_routes(application: Flask) -> None:
# import views # TODO convert to blueprint
# import views.stats # TODO convert to blueprint
from auth.login import auth_bp
from auth.microsoft import auth_microsoft_bp
from auth.zeus import auth_zeus_bp
from login import auth_bp
from views.debug import debug_bp
from views.general import general_bp
from views.order import order_bp
from views.stats import stats_blueprint
from zeus import oauth_bp
application.register_blueprint(general_bp, url_prefix="/")
application.register_blueprint(order_bp, url_prefix="/order")
application.register_blueprint(stats_blueprint, url_prefix="/stats")
application.register_blueprint(auth_bp, url_prefix="/")
if Configuration.ENABLE_MICROSOFT_AUTH:
application.register_blueprint(auth_microsoft_bp,
url_prefix="/users/auth/microsoft_graph_auth") # "/auth/microsoft")
application.register_blueprint(auth_zeus_bp, url_prefix="/auth/zeus")
application.register_blueprint(oauth_bp, url_prefix="/")
if application.debug:
application.register_blueprint(debug_bp, url_prefix="/debug")
@ -167,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")
@ -181,20 +166,10 @@ def create_app():
add_routes(app)
add_template_filters(app)
@app.context_processor
def inject_config():
return dict(configuration=Configuration)
return app, app_manager
# 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

@ -1,77 +0,0 @@
import typing
from flask import Blueprint, url_for, request, redirect, flash, Response
from flask_login import login_user
from microsoftgraph.client import Client
from config import Configuration
from models import User, db
auth_microsoft_bp = Blueprint("auth_microsoft_bp", __name__)
client = Client(Configuration.MICROSOFT_AUTH_ID,
Configuration.MICROSOFT_AUTH_SECRET,
account_type="ugentbe.onmicrosoft.com")
def microsoft_login():
"""Log in using Microsoft"""
scope = ["openid", "profile", "User.Read", "User.Read.All"]
url = client.authorization_url(url_for("auth_microsoft_bp.authorized", _external=True), scope, state=None)
return redirect(url)
@auth_microsoft_bp.route("/login")
def login():
"""Function to handle a user trying to log in"""
return microsoft_login()
@auth_microsoft_bp.route("callback") # "/authorized")
def authorized() -> typing.Any:
# type is 'typing.Union[str, Response]', but this errors due to
# https://github.com/python/mypy/issues/7187
"""Check authorized status"""
oauth_code = request.args['code']
resp = client.exchange_code(url_for("auth_microsoft_bp.authorized", _external=True), oauth_code)
client.set_token(resp.data)
resp = client.users.get_me()
microsoft_uuid = resp.data['id']
username = resp.data['userPrincipalName']
# Fail if fields are not populated
if not microsoft_uuid or not username:
flash("You're not allowed to enter, please contact a system administrator")
return redirect(url_for("general_bp.home"))
# Find existing user by Microsoft UUID (userPrincipalName can change)
user = User.query.filter_by(microsoft_uuid=microsoft_uuid).first()
if user:
return login_and_redirect_user(user)
# Find existing user by username (pre-existing account)
user = User.query.filter_by(username=username).first()
if user:
return login_and_redirect_user(user)
# No user found, create a new one
user = create_user(username, microsoft_uuid=microsoft_uuid)
return login_and_redirect_user(user)
def login_and_redirect_user(user) -> Response:
"""Log in the user and then redirect them"""
login_user(user)
return redirect(url_for("general_bp.home"))
def create_user(username, *, microsoft_uuid) -> User:
"""Create a temporary user if it is needed"""
user = User()
user.configure(username, False, 1, microsoft_uuid=microsoft_uuid)
db.session.add(user)
db.session.commit()
return user

View file

@ -1,26 +1,16 @@
"""An example for a Haldis config"""
# import os
"An example for a Haldis config"
# config
class Configuration:
"Haldis configuration object"
# pylint: disable=too-few-public-methods
SQLALCHEMY_DATABASE_URI = "sqlite:///haldis.db"
# MARIADB_HOST = os.environ.get("MARIADB_HOST")
# MARIADB_DB = os.environ.get("MARIADB_DATABASE")
# MARIADB_USER = os.environ.get("MARIADB_USER")
# MARIADB_PASS = os.environ.get("MARIADB_PASSWORD")
# SQLALCHEMY_DATABASE_URI = f"mysql+pymysql://{MARIADB_USER}:{MARIADB_PASS}@{MARIADB_HOST}/{MARIADB_DB}"
SQLALCHEMY_TRACK_MODIFICATIONS = False
DEBUG = True
HALDIS_ADMINS = []
HALDIS_ADMIN_USERS = []
SECRET_KEY = "<change>"
SLACK_WEBHOOK = None
LOGFILE = "haldis.log"
SENTRY_DSN = None
ZEUS_KEY = "tomtest"
ZEUS_SECRET = "blargh"
ENABLE_MICROSOFT_AUTH = False
MICROSOFT_AUTH_ID = ""
MICROSOFT_AUTH_SECRET = ""

View file

@ -9,7 +9,6 @@ user
order
id
slug secret used in URL
courier_id
location_id HLDS identifier
location_name this allows historical orders to keep the same location name

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

@ -1,6 +1,7 @@
# Import this class to load the standard HLDS definitions
import subprocess
from pathlib import Path
from os import path
from typing import List
from .models import Location
@ -11,14 +12,10 @@ __all__ = ["location_definitions", "location_definition_version"]
# pylint: disable=invalid-name
# TODO Use proper way to get resources, see https://stackoverflow.com/a/10935674
ROOT_DIR = Path(__file__).parent.parent.parent
DATA_DIR = ROOT_DIR / "menus"
DATA_DIR = path.join(path.dirname(__file__), "..", "..", "menus")
location_definitions: List[Location] = parse_all_directory(str(DATA_DIR))
location_definitions: List[Location] = parse_all_directory(DATA_DIR)
location_definitions.sort(key=lambda l: l.name)
try:
proc = subprocess.run(["git", "rev-parse", "HEAD"], stdout=subprocess.PIPE, cwd=str(ROOT_DIR), check=True)
location_definition_version = proc.stdout.decode().strip()
except FileNotFoundError:
location_definition_version = ""
proc = subprocess.run(["git", "rev-parse", "HEAD"], stdout=subprocess.PIPE, check=True)
location_definition_version = proc.stdout.decode().strip()

View file

@ -29,9 +29,9 @@ location = >location_header items:{ block } ;
attributes =
name:/[^\n#]*?(?= +-- | | €| *\n| *#)/
name:/[^\n#]*?(?= +-- | | *\n| *#)/
[ s '--' ~ s description:/[^\n#]*?(?= | *\n| *#)/ ]
[ / +/ ~
[ / {2,}/ ~
[ {[ s ] ('{' tags+:identifier '}')} / +|$/ ]
[ price:price ]
]

View file

@ -88,6 +88,12 @@ class Dish:
)
def _sum_f_option_prices(self, f):
for (_, choice) in self.choices:
if len(choice.options) == 0:
print((f"[PARSE ERROR] At least 1 option expected in dish choice.\n"
f"\tDish:\t'{self.name}'\n"
f"\tChoice:\t'{choice.name}'\n"))
exit(1)
return sum(
f(option.price for option in choice.options)
for (choice_type, choice) in self.choices

View file

@ -1,25 +1,31 @@
"""Script for everything related to logging in and out"""
"Script for everything related to logging in and out"
from flask import Blueprint, abort, redirect, session, url_for
from flask_login import current_user, logout_user
from models import User
from werkzeug.wrappers import Response
from zeus import zeus_login
auth_bp = Blueprint("auth_bp", __name__)
def init_login(app) -> None:
"""Initialize the login"""
"Initialize the login"
# pylint: disable=W0612
@app.login_manager.user_loader
def load_user(userid) -> User:
"""Load the user"""
"Load the user"
return User.query.filter_by(id=userid).first()
@auth_bp.route("/login")
def login():
"Function to handle a user trying to log in"
return zeus_login()
@auth_bp.route("/logout")
def logout() -> Response:
"""Function to handle a user trying to log out"""
"Function to handle a user trying to log out"
if "zeus_token" in session:
session.pop("zeus_token", None)
logout_user()
@ -27,6 +33,6 @@ def logout() -> Response:
def before_request() -> None:
"""Function for what has to be done before a request"""
"Function for what has to be done before a request"
if current_user.is_anonymous() or not current_user.is_allowed():
abort(401)

View file

@ -43,7 +43,7 @@ def upgrade():
sa.Column("starttime", sa.DateTime(), nullable=True),
sa.Column("stoptime", sa.DateTime(), nullable=True),
sa.Column("public", sa.Boolean(), nullable=True),
sa.ForeignKeyConstraint(["location_id"], ["location.id"], name="order_ibfk_1"),
sa.ForeignKeyConstraint(["location_id"], ["location.id"]),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
@ -65,7 +65,7 @@ def upgrade():
sa.Column("extra", sa.String(length=254), nullable=True),
sa.Column("name", sa.String(length=120), nullable=True),
sa.ForeignKeyConstraint(["order_id"], ["order.id"]),
sa.ForeignKeyConstraint(["product_id"], ["product.id"], name="order_item_ibfk_3"),
sa.ForeignKeyConstraint(["product_id"], ["product.id"]),
sa.ForeignKeyConstraint(["user_id"], ["user.id"]),
sa.PrimaryKeyConstraint("id"),
)

View file

@ -1,30 +0,0 @@
"""add slug
Revision ID: 29ccbe077c57
Revises: 55013fe95bea
Create Date: 2022-05-20 19:46:11.924218
"""
# revision identifiers, used by Alembic.
revision = '29ccbe077c57'
down_revision = '55013fe95bea'
from alembic import op
import sqlalchemy as sa
from sqlalchemy.sql import text
def upgrade():
op.add_column('order', sa.Column(
'slug',
sa.String(length=8),
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'])
def downgrade():
op.drop_constraint('order_slug_unique', 'order', type_='unique')
op.drop_column('order', 'slug')

View file

@ -1,26 +0,0 @@
"""empty message
Revision ID: 89b2c980b663
Revises: 9eac0f3d7b1e
Create Date: 2023-04-20 02:01:54.558602
"""
# revision identifiers, used by Alembic.
revision = '89b2c980b663'
down_revision = '9eac0f3d7b1e'
from alembic import op
import sqlalchemy as sa
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('user', sa.Column('microsoft_uuid', sa.VARCHAR(length=120), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('user', 'microsoft_uuid')
# ### end Alembic commands ###

View file

@ -112,12 +112,14 @@ def upgrade():
)
)
# Historical product data migrated, drop obsolete column and table
op.drop_constraint("order_item_ibfk_3", "order_item", type_="foreignkey")
op.execute(text("ALTER TABLE order_item DROP FOREIGN KEY order_item_ibfk_3"))
op.drop_column("order_item", "product_id")
op.drop_table("product")
# ----------------------------------------------------------------------------------------------
# Migrate historical location data to orders
op.execute(text("ALTER TABLE `order` DROP FOREIGN KEY order_ibfk_2"))
op.alter_column(
"order",
"location_id",
@ -155,7 +157,6 @@ def upgrade():
for query in chain(new_location_id, [location_name_from_location]):
op.execute(query)
# Historical location data migrated, drop obsolete column and table
op.drop_constraint("order_ibfk_1", "order", type_="foreignkey")
op.drop_column("order", "legacy_location_id")
op.drop_table("location")

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

@ -1,9 +1,7 @@
"""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
@ -11,16 +9,9 @@ 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"""
"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))
@ -28,8 +19,6 @@ 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="")
items = db.relationship("OrderItem", backref="order", lazy="dynamic")
@ -58,7 +47,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)
@ -69,7 +58,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
@ -89,7 +78,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)
)
@ -112,11 +101,11 @@ class Order(db.Model):
)
def is_closed(self) -> bool:
"""Return whether the order is closed"""
"Return whether or not 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

@ -1,21 +1,13 @@
"Script for everything User related in the database"
from typing import List, Optional
from models import db
class User(db.Model):
"""Class used for configuring the User model in the database"""
"Class used for configuring the User model in the database"
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
admin = db.Column(db.Boolean)
bias = db.Column(db.Integer)
# Microsoft OAUTH info
microsoft_uuid = db.Column(db.String(120), unique=True)
# Association logic
associations = db.Column(db.String(255), nullable=False, server_default="")
# Relations
runs = db.relation(
"Order",
backref="courier",
@ -24,18 +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, *, microsoft_uuid: str = None, 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.microsoft_uuid = microsoft_uuid
self.associations = ",".join(associations)
# pylint: disable=C0111, R0201
def is_authenticated(self) -> bool:

View file

@ -11,29 +11,29 @@ 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_slug", order_slug=order.slug, _external=True),
url_for("order_bp.order_from_id", order_id=order.id, _external=True),
order.location_name,
remaining_minutes(order.stoptime),
order.courier.username,
order.courier.username.title(),
)
# pylint: disable=C0209
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_slug", order_slug=order.slug, _external=True),
url_for("order_bp.order_from_id", order_id=order.id, _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,9 +60,9 @@ 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"
minutes = int(delta.total_seconds() // 60)
minutes = delta.total_seconds() // 60
return f"{minutes:02}"

View file

@ -243,9 +243,9 @@ details summary {
}
details summary:before {
font-style: normal;
content: "";
content: "";
padding-right: 0.4em;
}
details[open] summary:before {
content: "";
content: "";
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -63,8 +63,7 @@
<nav class="navbar navbar-default navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar"
aria-expanded="false" aria-controls="navbar">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
@ -82,10 +81,7 @@
</ul>
<ul class="nav navbar-nav navbar-right">
{% if current_user.is_anonymous() %}
{% if configuration.ENABLE_MICROSOFT_AUTH %}
<li><a href="{{ url_for('auth_microsoft_bp.login') }}">Login with Microsoft</a></li>
{% endif %}
<li><a href="{{ url_for('auth_zeus_bp.login') }}">Login with Zeus</a></li>
<li><a href="{{ url_for('auth_bp.login') }}">Login</a></li>
{% else %}
<li><a href="{{ url_for('general_bp.profile') }}">{{ current_user.username }}</a></li>
<li><a href="{{ url_for('auth_bp.logout') }}">Logout</a></li>
@ -100,8 +96,8 @@
{{ utils.flashed_messages(container=True) }}
<div class="container main">
{% block container -%}
{%- endblock %}
{% block container -%}
{%- endblock %}
</div>
<footer>

View file

@ -12,39 +12,18 @@
{% 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 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>
<header>
<h2 id="order-title">Order {{ order.id }}</h2>
<div class="location">
{% if order.location %}
<a href="{{ url_for('general_bp.location', location_id=order.location_id) }}">{{ order.location_name }}</a>
{% else %}
{{ 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 class="location">
{% if order.location %}
<a href="{{ url_for('general_bp.location', location_id=order.location_id) }}">{{ order.location_name }}</a>
{% else %}
{{ order.location_name }}
{% endif %}
</div>
</header>
@ -57,7 +36,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_slug=order.slug, item_id=item.id) }}" method="post" style="display:inline">
<form action="{{ url_for('order_bp.delete_item', order_id=order.id, 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 %}
@ -86,7 +65,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_slug=order.slug) }}" id="dish_{{ dish.id }}">
<form method="post" action="{{ url_for('order_bp.order_item_create', order_id=order.id) }}" id="dish_{{ dish.id }}">
{{ form.csrf_token }}
<input type="hidden" name="dish_id" value="{{ dish.id }}" />
@ -155,66 +134,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_id=order.id) }}" 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_id=order.id) }}" 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>
{%- 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">
@ -285,7 +258,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_slug=order.slug) }}">Shop view</a>
<a class="btn btn-sm" href="{{ url_for('order_bp.items_shop_view', order_id=order.id) }}">Shop view</a>
</div>
</div>
</div>
@ -294,7 +267,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_slug=order.slug) }}" method="post">
<form action="{{ url_for('order_bp.modify_items', order_id=order.id) }}" method="post">
<table class="table table-condensed">
<thead>
<tr><th>Total</th><th>Name</th><th>Items</th></tr>
@ -352,7 +325,7 @@
{% if order.can_modify_prices(current_user.id) %}
&nbsp; <span style="border-left: 1px solid var(--gray0); display: inline-block;">&nbsp;</span>&nbsp;
<a href="{{ url_for('order_bp.prices', order_slug=order.slug) }}" class="btn btn-sm">
<a href="{{ url_for('order_bp.prices', order_id=order.id) }}" class="btn btn-sm">
<span class="glyphicon glyphicon-pencil"></span> Edit prices
</a>
{% 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_slug=order_slug) }}">
<form method="post" action="{{ url_for('.order_edit', order_id=order_id) }}">
{{ 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

@ -11,10 +11,10 @@
{% block container %}
<header>
<h2 id="order-title">Edit prices</h2>
<div>Only applied to <a href="{{ url_for('order_bp.order_from_slug', order_slug=order.slug) }}">order {{ order.id }}</a>. To permanently change prices for {{ order.location_name }}, edit the <a href="https://git.zeus.gent/haldis/menus/-/blob/master/{{order.location_id}}.hlds">HLDS location definition</a>.</div>
<div>Only applied to <a href="{{ url_for('order_bp.order_from_id', order_id=order.id) }}">order {{ order.id }}</a>. To permanently change prices for {{ order.location_name }}, edit the <a href="https://git.zeus.gent/haldis/menus/-/blob/master/{{order.location_id}}.hlds">HLDS location definition</a>.</div>
</header>
<form action="{{ url_for('order_bp.prices', order_slug=order.slug) }}" method="post">
<form action="{{ url_for('order_bp.prices', order_id=order.id) }}" method="post">
<div class="col-md-6" id="per_dish">
<h3>Per dish</h3>
<div class="noscript">This functionality requires JavaScript.</div>
@ -86,7 +86,7 @@
</div>
<div>
<a href="{{ url_for('order_bp.order_from_slug', order_slug=order.slug) }}" class="btn btn-sm">Cancel</a>
<a href="{{ url_for('order_bp.order_from_id', order_id=order.id) }}" class="btn btn-sm">Cancel</a>
<button class="btn btn-sm btn-primary">Apply</button>
</div>
</form>

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,18 +1,15 @@
{% 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">
<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 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>
</div>
</div>
{%- endmacro %}

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

@ -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,8 +20,8 @@ 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():
"Generate general order view"
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
@ -33,10 +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"""
if not current_user.association_list():
flash("Not allowed to create an order.", "info")
abort(401)
"Generate order create view"
orderForm = OrderForm()
orderForm.populate()
if orderForm.validate_on_submit():
@ -46,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_slug", order_slug=order.slug))
return redirect(url_for("order_bp.order_from_id", order_id=order.id))
return orders(form=orderForm)
@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()
@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()
if order is None:
abort(404)
if current_user.is_anonymous() and not order.public:
@ -80,10 +77,10 @@ def order_from_slug(order_slug: str, form: OrderForm = None, dish_id=None) -> st
)
@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()
@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()
if order is None:
abort(404)
if current_user.is_anonymous() and not order.public:
@ -93,31 +90,31 @@ def items_shop_view(order_slug: int) -> str:
return render_template("order_items.html", order=order, total_price=total_price)
@order_bp.route("/<order_slug>/edit", methods=["GET", "POST"])
@order_bp.route("/<order_id>/edit", methods=["GET", "POST"])
@login_required
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()
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()
if current_user.id is not order.courier_id and not current_user.is_admin():
abort(401)
if order is None:
abort(404)
order_form = OrderForm(obj=order)
order_form.populate()
if order_form.validate_on_submit():
order_form.populate_obj(order)
orderForm = OrderForm(obj=order)
orderForm.populate()
if orderForm.validate_on_submit():
orderForm.populate_obj(order)
order.update_from_hlds()
db.session.commit()
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)
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)
@order_bp.route("/<order_slug>/create", methods=["GET", "POST"])
def order_item_create(order_slug: str) -> typing.Any:
@order_bp.route("/<order_id>/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
"""Add item to order from slug"""
current_order = Order.query.filter(Order.slug == order_slug).first()
"Add item to order from id"
current_order = Order.query.filter(Order.id == order_id).first()
if current_order is None:
abort(404)
if current_order.is_closed():
@ -126,7 +123,7 @@ def order_item_create(order_slug: str) -> typing.Any:
flash("Please login to see this order.", "info")
abort(401)
location = current_order.location
# If location doesn't exist anymore, adding items is nonsensical
# 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()
@ -174,7 +171,7 @@ def order_item_create(order_slug: str) -> typing.Any:
return redirect(
url_for(
"order_bp.order_item_create",
order_slug=current_order.slug,
order_id=order_id,
dish=form.dish_id.data,
user_name=user_name,
comment=comment,
@ -183,13 +180,14 @@ def order_item_create(order_slug: str) -> 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_slug(current_order.slug, form=form, dish_id=dish_id)
return order_from_id(order_id, 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 = current_order.id
item.order_id = order_id
if not current_user.is_anonymous():
item.user_id = current_user.id
else:
@ -224,27 +222,26 @@ def order_item_create(order_slug: str) -> typing.Any:
db.session.add(item)
db.session.commit()
flash("Ordered %s" % item.dish_name, "success")
return redirect(url_for("order_bp.order_from_slug", order_slug=order_slug))
flash("Ordered %s" % (item.dish_name), "success")
return redirect(url_for("order_bp.order_from_id", order_id=order_id))
@order_bp.route("/<order_slug>/modify_items", methods=["POST"])
@order_bp.route("/<order_id>/modify_items", methods=["POST"])
@login_required
# pylint: disable=R1710
def modify_items(order_slug: str) -> typing.Optional[Response]:
def modify_items(order_id: int) -> typing.Optional[Response]:
if "delete_item" in request.form:
return delete_item(order_slug, int(request.form["delete_item"]))
return delete_item(order_id, int(request.form["delete_item"]))
user_names = request.form.getlist("user_names")
if request.form.get("action") == "mark_paid":
return set_items_paid(order_slug, user_names, True)
return set_items_paid(order_id, user_names, True)
elif request.form.get("action") == "mark_unpaid":
return set_items_paid(order_slug, user_names, False)
return set_items_paid(order_id, user_names, False)
else:
abort(404)
return None
def set_items_paid(order_slug: str, user_names: typing.Iterable[str], paid: bool):
order = Order.query.filter(Order.slug == order_slug).first()
def set_items_paid(order_id: int, user_names: typing.Iterable[str], paid: bool):
total_paid_items = 0
total_failed_items = 0
for user_name in user_names:
@ -252,15 +249,15 @@ def set_items_paid(order_slug: str, 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
@ -272,34 +269,33 @@ def set_items_paid(order_slug: str, 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_slug", order_slug=order_slug))
return redirect(url_for("order_bp.order_from_id", order_id=order_id))
@order_bp.route("/<order_slug>/<item_id>/delete", methods=["POST"])
@order_bp.route("/<order_id>/<item_id>/delete", methods=["POST"])
# pylint: disable=R1710
def delete_item(order_slug: str, item_id: int) -> typing.Any:
def delete_item(order_id: int, 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 = OrderItem.query.filter(OrderItem.id == item_id).first()
order: Order = Order.query.filter(Order.slug == order_slug).first()
"Delete an item from an order"
item = OrderItem.query.filter(OrderItem.id == item_id).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_slug", order_slug=order_slug))
flash("Deleted %s" % (dish_name), "success")
return redirect(url_for("order_bp.order_from_id", order_id=order_id))
abort(404)
@order_bp.route("/<order_slug>/volunteer", methods=["POST"])
@order_bp.route("/<order_id>/volunteer", methods=["POST"])
@login_required
def volunteer(order_slug: str) -> Response:
"""Add a volunteer to an order"""
order = Order.query.filter(Order.slug == order_slug).first()
def volunteer(order_id: int) -> Response:
"Add a volunteer to an order"
order = Order.query.filter(Order.id == order_id).first()
if order is None:
abort(404)
if order.courier_id is None or order.courier_id == 0:
@ -308,14 +304,14 @@ def volunteer(order_slug: str) -> Response:
flash("Thank you for volunteering!")
else:
flash("Volunteering not possible!")
return redirect(url_for("order_bp.order_from_slug", order_slug=order.slug))
return redirect(url_for("order_bp.order_from_id", order_id=order_id))
@order_bp.route("/<order_slug>/close", methods=["POST"])
@order_bp.route("/<order_id>/close", methods=["POST"])
@login_required
def close_order(order_slug: str) -> typing.Optional[Response]:
"""Close an order"""
order = Order.query.filter(Order.slug == order_slug).first()
def close_order(order_id: int) -> typing.Optional[Response]:
"Close an order"
order = Order.query.filter(Order.id == order_id).first()
if order is None:
abort(404)
if (
@ -327,19 +323,19 @@ def close_order(order_slug: str) -> typing.Optional[Response]:
if courier is not None:
order.courier_id = courier.id
db.session.commit()
return redirect(url_for("order_bp.order_from_slug", order_slug=order_slug))
return redirect(url_for("order_bp.order_from_id", order_id=order_id))
return None
@order_bp.route("/<order_slug>/prices", methods=["GET", "POST"])
@order_bp.route("/<order_id>/prices", methods=["GET", "POST"])
@login_required
def prices(order_slug: str) -> typing.Optional[Response]:
order = Order.query.filter(Order.slug == order_slug).first()
def prices(order_id: int) -> typing.Optional[Response]:
order = Order.query.filter(Order.id == order_id).first()
if order is None:
abort(404)
if not order.can_modify_prices(current_user.id):
flash("You cannot modify the prices at this time.", "error")
return redirect(url_for("order_bp.order_from_slug", order_slug=order.slug))
return redirect(url_for("order_bp.order_from_id", order_id=order_id))
if request.method == "GET":
return render_template(
@ -369,12 +365,12 @@ def prices(order_slug: str) -> typing.Optional[Response]:
item.price_modified = datetime.now()
db.session.commit()
return redirect(url_for("order_bp.order_from_slug", order_slug=order.slug))
return redirect(url_for("order_bp.order_from_id", order_id=order_id))
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]
@ -393,20 +389,19 @@ 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) & (
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

@ -1,16 +0,0 @@
import sentry_sdk
from sentry_sdk.integrations.flask import FlaskIntegration
from waitress import serve
from app import create_app
from config import Configuration
if __name__ == "__main__":
if Configuration.SENTRY_DSN:
sentry_sdk.init(
dsn=Configuration.SENTRY_DSN,
integrations=[FlaskIntegration()]
)
app, app_mgr = create_app()
serve(app, host="0.0.0.0", port=8000)

View file

@ -4,30 +4,24 @@ import typing
from flask import (Blueprint, current_app, flash, redirect, request, session,
url_for)
from flask_login import login_user
from flask_oauthlib.client import OAuth, OAuthException, OAuthRemoteApp
from flask_oauthlib.client import OAuth, OAuthException
from models import User, db
from werkzeug.wrappers import Response
auth_zeus_bp = Blueprint("auth_zeus_bp", __name__)
oauth_bp = Blueprint("oauth_bp", __name__)
def zeus_login():
"""Log in using ZeusWPI"""
"Log in using ZeusWPI"
return current_app.zeus.authorize(
callback=url_for("auth_zeus_bp.authorized", _external=True))
callback=url_for("oauth_bp.authorized", _external=True))
@auth_zeus_bp.route("/login")
def login():
"""Function to handle a user trying to log in"""
return zeus_login()
@auth_zeus_bp.route("/authorized")
@oauth_bp.route("/login/zeus/authorized")
def authorized() -> typing.Any:
# type is 'typing.Union[str, Response]', but this errors due to
# https://github.com/python/mypy/issues/7187
"""Check authorized status"""
"Check authorized status"
resp = current_app.zeus.authorized_response()
if resp is None:
# pylint: disable=C0301
@ -51,8 +45,8 @@ def authorized() -> typing.Any:
return redirect(url_for("general_bp.home"))
def init_oauth(app) -> OAuthRemoteApp:
"""Initialize the OAuth for ZeusWPI"""
def init_oauth(app):
"Initialize the OAuth for ZeusWPI"
oauth = OAuth(app)
zeus = oauth.remote_app(
@ -75,15 +69,15 @@ def init_oauth(app) -> OAuthRemoteApp:
def login_and_redirect_user(user) -> Response:
"""Log in the user and then redirect them"""
"Log in the user and then redirect them"
login_user(user)
return redirect(url_for("general_bp.home"))
def create_user(username) -> User:
"""Create a temporary user if it is needed"""
"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

@ -1,17 +0,0 @@
version: "3.4"
services:
app:
build:
target: "development"
environment:
- MARIADB_DATABASE=haldis
- MARIADB_USER=haldis
- MARIADB_PASSWORD=haldis
volumes: ["$PWD:/src"]
database:
environment:
- MARIADB_DATABASE=haldis
- MARIADB_ROOT_PASSWORD=mariadb
- MARIADB_USER=haldis
- MARIADB_PASSWORD=haldis

View file

@ -1,31 +0,0 @@
version: "3.4"
services:
app:
build:
context: .
target: production
restart: on-failure
depends_on: [database]
ports: ["8000:8000"]
environment:
- MARIADB_HOST=database
- MARIADB_DATABASE
- MARIADB_USER
- MARIADB_PASSWORD
networks: [haldis]
database:
image: mariadb:10.8
hostname: database
restart: on-failure
environment:
- MARIADB_DATABASE
- MARIADB_ROOT_PASSWORD
- MARIADB_USER
- MARIADB_PASSWORD
networks: [haldis]
volumes: [haldis_data:/var/lib/mysql]
networks:
haldis:
volumes:
haldis_data:

View file

@ -25,7 +25,7 @@ syn keyword hldsChoiceType single_choice multi_choice nextgroup=hldsBlockIdAf
syn match hldsBlockId "^[a-z0-9_-]\+: "
syn match hldsBlockIdAftrKywrd "[a-z0-9_-]\+: " contained
syn match _space " \+" nextgroup=hldsTag,hldsPrice
syn match _doubleSpace " \+" nextgroup=hldsTag,hldsPrice
syn match hldsTag "{[a-z0-9_-]\+}\( \|$\)" contained nextgroup=hldsTag,hldsPrice
syn match hldsPrice "€ *[0-9]\+\(\.[0-9]\+\|\)\( \|$\)" contained

View file

@ -12,7 +12,7 @@ E="${normal}"
if [ ! -d "venv" ]; then
PYTHON_VERSION=$(cat .python-version)
echo -e "${B} No venv found, creating a new one with version ${PYTHON_VERSION} ${E}"
python3 -m virtualenv -p "$PYTHON_VERSION" venv
python3 -m virtualenv -p $PYTHON_VERSION venv
fi
source venv/bin/activate

View file

@ -4,6 +4,3 @@ set -euo pipefail
cd "$(dirname "$0")/app"
env python create_database.py setup_database
latest_revision=$(env python app.py db heads | sed "s/ (head)$//")
echo Stamping db at $latest_revision
env python app.py db stamp $latest_revision

View file

@ -12,5 +12,3 @@ black
pymysql
pyyaml
tatsu<5.6 # >=5.6 needs Python >=3.8
microsoftgraph-python
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
@ -79,8 +74,6 @@ markupsafe==2.0.1
# jinja2
# mako
# wtforms
microsoftgraph-python==1.1.3
# via -r requirements.in
mypy-extensions==0.4.3
# via black
oauthlib==2.1.0
@ -99,14 +92,10 @@ pyyaml==5.4.1
# via -r requirements.in
regex==2021.4.4
# via black
requests==2.25.1
# via
# microsoftgraph-python
# 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
@ -117,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