Compare commits

...

63 commits
slug ... master

Author SHA1 Message Date
redfast00 73671bd8f1
Merge pull request #213 from ZeusWPI/feat/docker
Dockerize the application
2023-06-28 22:09:06 +02:00
Maxim De Clercq 45b4913657
Add menus provided by Zeus WPI by default 2023-06-25 17:18:50 +02:00
Maxim De Clercq 7b78e7d8ff
Add .dockerignore 2023-06-25 17:18:39 +02:00
Maxim De Clercq a29d3a33be
Use correct app when running with waitress 2023-06-25 17:18:31 +02:00
Maxim De Clercq 7fad75fc08
Dockerize the application 2023-06-25 17:18:20 +02:00
redfast00 5a82354b78
Merge pull request #214 from ZeusWPI/microsoft-auth-wip
Microsoft auth
2023-06-25 16:49:13 +02:00
Maxim De Clercq fbb69c843a
Merge branch 'master' into microsoft-auth-wip 2023-06-25 16:18:32 +02:00
redfast00 30626e457a
Merge pull request #217 from ZeusWPI/no-double-space
Remove double space requirement
2023-06-13 23:31:56 +02:00
redfast00 0aea3f6d34
Remove double space requirement 2023-06-13 21:39:19 +02:00
redfast00 cdca5646ef
Merge pull request #204 from Happilands/patch-1
Add robots.txt file
2023-04-22 17:52:17 +02:00
Maxim De Clercq 8b1b3f482a
Add migration for Microsoft Auth 2023-04-20 02:10:59 +02:00
Maxim De Clercq 2d6aea10fb
Change Microsoft account type to ugentbe.onmicrosoft.com 2023-04-19 23:17:51 +02:00
Maxim De Clercq 6e79fc50ed
Merge pull request #186 from ZeusWPI/feature/microsoft-auth
Add working microsoft login flow
2023-04-19 22:55:32 +02:00
Maxim De Clercq ba29ecbc73
Check for user with matching Microsoft UUID first 2023-04-19 22:38:30 +02:00
Maxim De Clercq 1ffcdc3ec1
Do not store UGent username since it is not exposed through Graph API 2023-04-19 22:03:40 +02:00
Maxim De Clercq 6bb11e49a3
Add support for when cwd is outside the project
E.g. Pycharm runs `/.../haldis/venv/bin/python /.../haldis/app/app.py` when no working directory is set.
2023-04-19 21:18:53 +02:00
Maxim De Clercq aab522eef9
Merge branch 'master' into feature/microsoft-auth
# Conflicts:
#	app/app.py
#	app/create_database.py
#	app/models/user.py
#	requirements.in
#	requirements.txt
2023-04-19 20:55:35 +02:00
Tibo e86fce0a7e
Merge branch 'master' into patch-1 2023-04-19 19:14:54 +02:00
Charlotte Van Petegem 02afba70a9
Merge pull request #212 from ZeusWPI/fix/admin-order-current-top
Order current user to the top for admins when choosing courier
2023-01-24 19:11:21 +01:00
Charlotte Van Petegem 1bc6a5931e
Order current user to the top for admins 2023-01-24 19:06:10 +01:00
Jasper c991cd7882
Merge pull request #207 from JasperJanin/master
Add user group reference to orders
2022-10-27 22:15:37 +02:00
Jasper Janin a29ade4773 Remove redundant class attributes 2022-10-27 22:12:03 +02:00
Jasper 6f7aff15cc
Merge branch 'ZeusWPI:master' into master 2022-10-27 21:35:28 +02:00
Jasper Janin 7b12c266b3 Add user group reference to orders 2022-10-27 21:32:22 +02:00
Tibo 7d122cf6e9
Merge pull request #203 from ZeusWPI/addToolversions
.tool-versions toegevoegd
2022-10-27 21:32:06 +02:00
Charlotte Van Petegem 202d5d3e7a
Revert "Merge branch 'master' of github.com:ZeusWPI/Haldis"
This reverts commit 28fa1b7592, reversing
changes made to b14671413c.
2022-10-27 21:23:13 +02:00
Jasper Janin 28fa1b7592 Merge branch 'master' of github.com:ZeusWPI/Haldis 2022-10-27 20:28:11 +02:00
Jasper Janin bf8eb94117 Add user group reference to orders 2022-10-27 20:24:35 +02:00
Maxime b14671413c
Merge pull request #206 from ZeusWPI/sentry-local-dev
Make sure local dev still works with sentry
2022-10-27 19:49:57 +02:00
Charlotte Van Petegem 29afc8db7a
Make sure local dev still works with sentry 2022-10-27 19:47:12 +02:00
Charlotte Van Petegem 1dcd723bd4
Merge pull request #205 from ZeusWPI/add-sentry
Add glitchtip
2022-10-27 19:41:26 +02:00
Charlotte Van Petegem c0f44ab037
Add glitchtip 2022-10-27 19:38:46 +02:00
Happilands 4e8799eca5
Add robots.txt file
From
https://www.pythonanywhere.com/forums/topic/2899/
2022-10-27 19:29:50 +02:00
AlexVDP8 e302da0335 .tool-versions toegevoegd
Co-authored-by: Francis <francisklinck@gmail.com>
2022-10-27 19:15:29 +02:00
redfast00 c839fce270
Merge pull request #199 from ZeusWPI/fix/delete-bottom-list
Fix not being able to delete an item from the list at the bottom of an order
2022-06-10 18:41:14 +02:00
Charlotte Van Petegem 687d389fa2
Fix not being able to delete an item from the list at the bottom of an order 2022-06-10 18:28:31 +02:00
redfast00 9c4361ab1b
Merge pull request #197 from ZeusWPI/remove-dot-title-from-username
Don't title username
2022-06-03 19:36:54 +02:00
redfast00 754eae4a50
Don't title username 2022-06-03 19:27:11 +02:00
Charlotte Van Petegem f3911b377d
Merge pull request #196 from ZeusWPI/eight-character-slug
Make slug eight characters
2022-06-03 19:16:11 +02:00
Charlotte Van Petegem 3bc2ad83ea
Remove some more letters from the alphabet 2022-06-03 19:12:00 +02:00
Charlotte Van Petegem 0661016236
Make slug eight characters 2022-06-03 19:05:33 +02:00
Maxime 10327941d2
Merge pull request #195 from ZeusWPI/base58-slugs
Use base58 for slugs
2022-06-01 21:26:49 +02:00
mcbloch 5d204a4012 use a string, not bytes 2022-06-01 17:36:52 +02:00
Maxime 2bdd07c9af
Update app/models/order.py
Co-authored-by: Charlotte Van Petegem <charlotte.vanpetegem@ugent.be>
2022-06-01 17:24:27 +02:00
mcbloch 978b432d7e use base58 for slugs to remove doubt 2022-06-01 17:18:47 +02:00
Charlotte Van Petegem 426357f00d
Move filtering by association to get_orders
Fixes duplication and restores old `/orders` behaviour
2022-05-30 20:24:07 +02:00
Midgard 5306561ddd
Correct word on home page 2022-05-30 20:20:51 +02:00
Charlotte Van Petegem 01b5c72e7b
Generate slug in app 2022-05-30 19:48:23 +02:00
Midgard 4a353ec17e
Create a slug for old orders in the migration 2022-05-30 19:47:48 +02:00
Charlotte Van Petegem 8f3750060b
VARCHAR requires a length in mysql 2022-05-30 18:50:46 +02:00
Charlotte Van Petegem bb49fb2795
Add merge migration 2022-05-30 18:44:18 +02:00
Charlotte Van Petegem 28a6dc5422
Merge pull request #193 from ZeusWPI/feature/association-management
Integrate associations
2022-05-30 18:29:05 +02:00
Charlotte Van Petegem c04d9bbd44
Fix typing of associations in user model 2022-05-25 10:07:30 +02:00
Charlotte Van Petegem 4d9d43b0f0
Don't limit length (and fix migration) 2022-05-24 21:26:56 +02:00
Charlotte Van Petegem da1a708e28
List order association in admin view 2022-05-20 23:29:05 +02:00
Charlotte Van Petegem d6d9d61f27
Remove some unused code 2022-05-20 23:21:11 +02:00
Charlotte Van Petegem a077a8038a
Only list orders to users of its association 2022-05-20 23:15:45 +02:00
Charlotte Van Petegem 1c0d78f2ee
Make sure only users with at least one association can create an order 2022-05-20 22:46:56 +02:00
Charlotte Van Petegem c43efa4b10
Migration 2022-05-20 21:34:18 +02:00
mcbloch 8a2b9247e1
initial work, model works, layout doenst 2022-05-20 21:02:24 +02:00
mcbloch 841c3d5fb8 add flag to disable microsoft login 2022-04-20 01:34:19 +02:00
mcbloch da88d807d1 disable printing of user info 2022-04-20 01:28:48 +02:00
mcbloch cc0c271a22 Add working microsoft login flow 2022-04-20 01:27:52 +02:00
35 changed files with 513 additions and 150 deletions

10
.dockerignore Normal file
View file

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

1
.tool-versions Normal file
View file

@ -0,0 +1 @@
python 3.9.2

26
Dockerfile Normal file
View file

@ -0,0 +1,26 @@
# 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

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

View file

@ -28,11 +28,12 @@ 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"]
column_list = ["starttime", "stoptime", "location_name", "location_id", "courier", "association"]
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,24 +3,29 @@
"""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 flask import Flask, render_template
from config import Configuration
from flask import Flask, render_template, Response
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:
@ -97,18 +102,22 @@ def add_routes(application: Flask) -> None:
# import views # TODO convert to blueprint
# import views.stats # TODO convert to blueprint
from login import auth_bp
from auth.login import auth_bp
from auth.microsoft import auth_microsoft_bp
from auth.zeus import auth_zeus_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="/")
application.register_blueprint(oauth_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")
if application.debug:
application.register_blueprint(debug_bp, url_prefix="/debug")
@ -158,6 +167,12 @@ 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")
@ -166,10 +181,20 @@ 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,31 +1,25 @@
"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()
@ -33,6 +27,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)

77
app/auth/microsoft.py Normal file
View file

@ -0,0 +1,77 @@
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

@ -4,24 +4,30 @@ 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
from flask_oauthlib.client import OAuth, OAuthException, OAuthRemoteApp
from models import User, db
from werkzeug.wrappers import Response
oauth_bp = Blueprint("oauth_bp", __name__)
auth_zeus_bp = Blueprint("auth_zeus_bp", __name__)
def zeus_login():
"Log in using ZeusWPI"
"""Log in using ZeusWPI"""
return current_app.zeus.authorize(
callback=url_for("oauth_bp.authorized", _external=True))
callback=url_for("auth_zeus_bp.authorized", _external=True))
@oauth_bp.route("/login/zeus/authorized")
@auth_zeus_bp.route("/login")
def login():
"""Function to handle a user trying to log in"""
return zeus_login()
@auth_zeus_bp.route("/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
@ -45,8 +51,8 @@ def authorized() -> typing.Any:
return redirect(url_for("general_bp.home"))
def init_oauth(app):
"Initialize the OAuth for ZeusWPI"
def init_oauth(app) -> OAuthRemoteApp:
"""Initialize the OAuth for ZeusWPI"""
oauth = OAuth(app)
zeus = oauth.remote_app(
@ -69,15 +75,15 @@ def init_oauth(app):
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)
user.configure(username, False, 1, associations=["zeus"])
db.session.add(user)
db.session.commit()
return user

View file

@ -1,16 +1,26 @@
"An example for a Haldis config"
# config
"""An example for a Haldis config"""
# import os
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 = []
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

@ -24,13 +24,17 @@ 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)] + [
(u.id, u.username) for u in User.query.order_by("username")
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
]
else:
self.courier_id.choices = [
@ -38,6 +42,7 @@ 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,7 +1,6 @@
# Import this class to load the standard HLDS definitions
import subprocess
from os import path
from pathlib import Path
from typing import List
from .models import Location
@ -12,10 +11,14 @@ __all__ = ["location_definitions", "location_definition_version"]
# pylint: disable=invalid-name
# TODO Use proper way to get resources, see https://stackoverflow.com/a/10935674
DATA_DIR = path.join(path.dirname(__file__), "..", "..", "menus")
ROOT_DIR = Path(__file__).parent.parent.parent
DATA_DIR = ROOT_DIR / "menus"
location_definitions: List[Location] = parse_all_directory(DATA_DIR)
location_definitions: List[Location] = parse_all_directory(str(DATA_DIR))
location_definitions.sort(key=lambda l: l.name)
proc = subprocess.run(["git", "rev-parse", "HEAD"], stdout=subprocess.PIPE, check=True)
location_definition_version = proc.stdout.decode().strip()
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 = ""

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

@ -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"]),
sa.ForeignKeyConstraint(["location_id"], ["location.id"], name="order_ibfk_1"),
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"]),
sa.ForeignKeyConstraint(["product_id"], ["product.id"], name="order_item_ibfk_3"),
sa.ForeignKeyConstraint(["user_id"], ["user.id"]),
sa.PrimaryKeyConstraint("id"),
)

View file

@ -12,12 +12,19 @@ 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=7), nullable=True))
op.create_unique_constraint(None, 'order', ['slug'])
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(None, 'order', type_='unique')
op.drop_constraint('order_slug_unique', 'order', type_='unique')
op.drop_column('order', 'slug')

View file

@ -0,0 +1,26 @@
"""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,14 +112,12 @@ def upgrade():
)
)
# Historical product data migrated, drop obsolete column and table
op.execute(text("ALTER TABLE order_item DROP FOREIGN KEY order_item_ibfk_3"))
op.drop_constraint("order_item_ibfk_3", "order_item", type_="foreignkey")
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",
@ -157,6 +155,7 @@ 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

@ -0,0 +1,22 @@
"""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

@ -0,0 +1,28 @@
"""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,10 +1,14 @@
"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

@ -11,11 +11,13 @@ from utils import first
from .database import db
from .user import User
BASE31_ALPHABET = '23456789abcdefghjkmnpqrstuvwxyz'
def generate_slug():
alphabet = string.ascii_letters + string.digits
return ''.join(secrets.choice(alphabet) for i in range(7))
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"""
@ -26,7 +28,8 @@ 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)
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")

View file

@ -1,13 +1,21 @@
"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",
@ -16,11 +24,18 @@ class User(db.Model):
)
orderItems = db.relationship("OrderItem", backref="user", lazy="dynamic")
def configure(self, username: str, admin: bool, bias: int) -> None:
"Configure the User"
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 = []
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

@ -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.title(),
order.courier.username,
)
# pylint: disable=C0209

View file

@ -63,7 +63,8 @@
<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>
@ -81,7 +82,10 @@
</ul>
<ul class="nav navbar-nav navbar-right">
{% if current_user.is_anonymous() %}
<li><a href="{{ url_for('auth_bp.login') }}">Login</a></li>
{% 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>
{% 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>
@ -96,8 +100,8 @@
{{ utils.flashed_messages(container=True) }}
<div class="container main">
{% block container -%}
{%- endblock %}
{% block container -%}
{%- endblock %}
</div>
<footer>

View file

@ -155,60 +155,66 @@
<div class="box" id="order_info">
<h3>Order information</h3>
<dl>
<div>
<dt>Order opens</dt>
<dd>{{ order.starttime.strftime("%Y-%m-%d, %H:%M") }}</dd>
<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>
<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>
<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%">
</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">
@ -314,9 +320,7 @@
<li class="{{ 'paid' if item.paid }}">
<div class="actions">
{% 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">
<button class="btn btn-link btn-sm" type="submit" style="padding: 0 0.5em;"><span class="glyphicon glyphicon-remove"></span></button>
</form>
<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>
{% else %}
<span class="glyphicon glyphicon-remove" style="color: var(--gray3); padding: 0 0.5em; cursor: not-allowed"></span>
{%- endif %}

View file

@ -38,6 +38,11 @@
{{ 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,14 +1,17 @@
{% macro render_order(order) -%}
<div class="row order_row">
<div class="col-md-8 col-lg-9 order_data">
<div class="col-md-6 order_data">
<h5>{{ order.location_name }}</h5>
<b class="amount_of_orders">{{ order.items.count() }} orders</b></p>
<b class="amount_of_orders">{{ order.items.count() }} items ordered for {{ order.association }}</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-4 col-lg-3 expand_button_wrapper">
<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>
</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 login_required
from flask_login import current_user, login_required
from hlds.definitions import location_definitions
from hlds.models import Location
from models import Order
@ -34,7 +34,9 @@ def home() -> str:
(Order.stoptime > prev_day) & (Order.stoptime < datetime.now())
)
return render_template(
"home.html", orders=get_orders(), recently_closed=recently_closed
"home.html", orders=get_orders(
((datetime.now() > Order.starttime) & (Order.stoptime > datetime.now()) | (Order.stoptime == None))
), 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 not current_user.is_anonymous():
if form is None and current_user.association_list():
form = OrderForm()
location_id = request.args.get("location_id")
form.location_id.default = location_id
@ -34,6 +34,9 @@ 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():
@ -393,16 +396,17 @@ 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)
expression = ((datetime.now() > Order.starttime) & (
Order.stoptime
> datetime.now()
# pylint: disable=C0121
) | (Order.stoptime == None)
) & (Order.association.in_(current_user.association_list()))
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)
expression & (Order.public == True) & (Order.association.in_(current_user.association_list()))
).all()
return order_list

16
app/waitress_wsgi.py Normal file
View file

@ -0,0 +1,16 @@
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

@ -0,0 +1,17 @@
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

31
docker-compose.yml Normal file
View file

@ -0,0 +1,31 @@
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

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

@ -12,3 +12,5 @@ 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
# This file is autogenerated by pip-compile with python 3.9
# To update, run:
#
# pip-compile
@ -11,11 +11,15 @@ appdirs==1.4.4
black==21.6b0
# via -r requirements.in
blinker==1.4
# via flask-debugtoolbar
# via
# flask-debugtoolbar
# sentry-sdk
cachelib==0.1.1
# via flask-oauthlib
certifi==2021.5.30
# via requests
# via
# requests
# sentry-sdk
chardet==4.0.0
# via requests
click==7.1.2
@ -24,6 +28,19 @@ 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
@ -44,18 +61,6 @@ 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
@ -74,6 +79,8 @@ 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
@ -92,10 +99,14 @@ 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
requests==2.25.1
# via requests-oauthlib
sentry-sdk[flask]==1.10.1
# via -r requirements.in
six==1.16.0
# via python-dateutil
sqlalchemy==1.4.18
@ -106,8 +117,10 @@ tatsu==4.4.0
# via -r requirements.in
toml==0.10.2
# via black
urllib3==1.26.5
# via requests
urllib3==1.26.12
# via
# requests
# sentry-sdk
visitor==0.1.3
# via flask-bootstrap
werkzeug==1.0.1