Compare commits
105 commits
feature/sl
...
master
Author | SHA1 | Date | |
---|---|---|---|
3cde7764c4 | |||
|
73671bd8f1 | ||
|
45b4913657 | ||
|
7b78e7d8ff | ||
|
a29d3a33be | ||
|
7fad75fc08 | ||
|
5a82354b78 | ||
|
fbb69c843a | ||
|
30626e457a | ||
|
0aea3f6d34 | ||
|
cdca5646ef | ||
|
8b1b3f482a | ||
|
2d6aea10fb | ||
|
6e79fc50ed | ||
|
ba29ecbc73 | ||
|
1ffcdc3ec1 | ||
|
6bb11e49a3 | ||
|
aab522eef9 | ||
|
e86fce0a7e | ||
|
02afba70a9 | ||
|
1bc6a5931e | ||
|
c991cd7882 | ||
|
a29ade4773 | ||
|
6f7aff15cc | ||
|
7b12c266b3 | ||
|
7d122cf6e9 | ||
|
202d5d3e7a | ||
|
28fa1b7592 | ||
|
bf8eb94117 | ||
|
b14671413c | ||
|
29afc8db7a | ||
|
1dcd723bd4 | ||
|
c0f44ab037 | ||
|
4e8799eca5 | ||
|
e302da0335 | ||
|
c839fce270 | ||
|
687d389fa2 | ||
|
9c4361ab1b | ||
|
754eae4a50 | ||
|
f3911b377d | ||
|
3bc2ad83ea | ||
|
0661016236 | ||
|
10327941d2 | ||
|
5d204a4012 | ||
|
2bdd07c9af | ||
|
978b432d7e | ||
|
426357f00d | ||
5306561ddd | |||
|
01b5c72e7b | ||
4a353ec17e | |||
|
8f3750060b | ||
|
bb49fb2795 | ||
|
28a6dc5422 | ||
453cacebd9 | |||
44feb1a4ff | |||
|
c04d9bbd44 | ||
|
4d9d43b0f0 | ||
|
0a0d13c0dc | ||
|
2c4a288d4e | ||
|
da1a708e28 | ||
|
d6d9d61f27 | ||
|
a077a8038a | ||
|
1c0d78f2ee | ||
|
bbb38aa825 | ||
|
c43efa4b10 | ||
|
8a2b9247e1 | ||
|
25e2757461 | ||
|
ab47c0a882 | ||
|
f87f3c5446 | ||
|
a33c76f84b | ||
|
98214f8b84 | ||
|
7702fdecbe | ||
|
0e0771bae1 | ||
|
749012140b | ||
b5202a9de6 | |||
7b16a3b6c5 | |||
03f1e56161 | |||
cf27a7de8a | |||
a568103a60 | |||
85d8892176 | |||
c35d107502 | |||
|
dfbf1de5a1 | ||
|
fec9d660c3 | ||
|
9c00fcc0cf | ||
|
2271b0427c | ||
5a9d9c1d31 | |||
ced04acb2e | |||
09e2d704cd | |||
fc630e9061 | |||
|
841c3d5fb8 | ||
|
da88d807d1 | ||
|
cc0c271a22 | ||
|
0ace54a8fd | ||
|
ae77adc54e | ||
|
e93460743a | ||
|
fb3e7b95f1 | ||
|
d59ad9abba | ||
|
492d1ca91c | ||
|
461664f629 | ||
|
781e4cd45b | ||
|
5e29f2a5f7 | ||
|
1cdd22c1c0 | ||
|
33d2fe3b52 | ||
|
2123d7d1a3 | ||
|
ba1b37f5fe |
62 changed files with 1298 additions and 550 deletions
10
.dockerignore
Normal file
10
.dockerignore
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
# Ignore everything
|
||||||
|
*
|
||||||
|
|
||||||
|
# Include source, config and scripts
|
||||||
|
!app
|
||||||
|
!etc
|
||||||
|
!*.md
|
||||||
|
!*.sh
|
||||||
|
!*.txt
|
||||||
|
!LICENSE
|
|
@ -5,6 +5,8 @@
|
||||||
# run arbitrary code.
|
# run arbitrary code.
|
||||||
extension-pkg-whitelist=
|
extension-pkg-whitelist=
|
||||||
|
|
||||||
|
fail-under=9.58
|
||||||
|
|
||||||
# Add files or directories to the blacklist. They should be base names, not
|
# Add files or directories to the blacklist. They should be base names, not
|
||||||
# paths.
|
# paths.
|
||||||
ignore=CVS
|
ignore=CVS
|
||||||
|
@ -28,7 +30,7 @@ limit-inference-results=100
|
||||||
|
|
||||||
# List of plugins (as comma separated values of python modules names) to load,
|
# List of plugins (as comma separated values of python modules names) to load,
|
||||||
# usually to register additional checkers.
|
# usually to register additional checkers.
|
||||||
load-plugins=
|
load-plugins=pylint_flask_sqlalchemy,pylint_flask
|
||||||
|
|
||||||
# Pickle collected data for later comparisons.
|
# Pickle collected data for later comparisons.
|
||||||
persistent=yes
|
persistent=yes
|
||||||
|
@ -60,7 +62,7 @@ confidence=
|
||||||
# --enable=similarities". If you want to run only the classes checker, but have
|
# --enable=similarities". If you want to run only the classes checker, but have
|
||||||
# no Warning level messages displayed, use "--disable=all --enable=classes
|
# no Warning level messages displayed, use "--disable=all --enable=classes
|
||||||
# --disable=W".
|
# --disable=W".
|
||||||
disable=E0401,E0611,C0103,W0511,W0611
|
disable=E0401,E0611,C0103,W0511,W0611,C0415
|
||||||
|
|
||||||
# Enable the message, report, category or checker with the given id(s). You can
|
# Enable the message, report, category or checker with the given id(s). You can
|
||||||
# either give multiple identifier separated by comma (,) or put this option
|
# either give multiple identifier separated by comma (,) or put this option
|
||||||
|
|
1
.tool-versions
Normal file
1
.tool-versions
Normal file
|
@ -0,0 +1 @@
|
||||||
|
python 3.9.2
|
26
Dockerfile
Normal file
26
Dockerfile
Normal 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
|
|
@ -26,7 +26,7 @@ Afterwards upgrade the database to the latest version using
|
||||||
cd app
|
cd app
|
||||||
python3 app.py db upgrade
|
python3 app.py db upgrade
|
||||||
|
|
||||||
You can now still seed the database by running
|
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`
|
||||||
|
|
||||||
./populate-db.sh
|
./populate-db.sh
|
||||||
|
|
||||||
|
|
0
app/__init__.py
Normal file
0
app/__init__.py
Normal file
14
app/add_admins.py
Normal file
14
app/add_admins.py
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
"""Script for adding users as admin to Haldis."""
|
||||||
|
from models import User
|
||||||
|
|
||||||
|
from app import db
|
||||||
|
from models import User
|
||||||
|
from config import Configuration
|
||||||
|
|
||||||
|
|
||||||
|
def add() -> None:
|
||||||
|
"""Add users as admin."""
|
||||||
|
for username in Configuration.HALDIS_ADMINS:
|
||||||
|
user = User()
|
||||||
|
user.configure(username, True, 0, associations=["zeus"])
|
||||||
|
db.session.add(user)
|
39
app/admin.py
39
app/admin.py
|
@ -1,19 +1,22 @@
|
||||||
|
"Module for everything related to Admin users"
|
||||||
import flask_login as login
|
import flask_login as login
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
from flask_admin import Admin
|
from flask_admin import Admin
|
||||||
from flask_admin.contrib.sqla import ModelView
|
from flask_admin.contrib.sqla import ModelView
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
|
||||||
from models import Order, OrderItem, OrderItemChoice, User
|
from models import Order, OrderItem, OrderItemChoice, User
|
||||||
|
|
||||||
|
|
||||||
class ModelBaseView(ModelView):
|
class ModelBaseView(ModelView):
|
||||||
|
"Class for the base view of the model"
|
||||||
# pylint: disable=too-few-public-methods, no-self-use
|
# pylint: disable=too-few-public-methods, no-self-use
|
||||||
def is_accessible(self) -> bool:
|
def is_accessible(self) -> bool:
|
||||||
|
"Function to check if the logged in user is an admin"
|
||||||
return login.current_user.is_admin()
|
return login.current_user.is_admin()
|
||||||
|
|
||||||
|
|
||||||
class UserAdminModel(ModelBaseView):
|
class UserAdminModel(ModelBaseView):
|
||||||
|
"Class for the model of a UserAdmin"
|
||||||
# pylint: disable=too-few-public-methods
|
# pylint: disable=too-few-public-methods
|
||||||
column_searchable_list = ("username",)
|
column_searchable_list = ("username",)
|
||||||
column_editable_list = ("username",)
|
column_editable_list = ("username",)
|
||||||
|
@ -22,27 +25,45 @@ class UserAdminModel(ModelBaseView):
|
||||||
|
|
||||||
|
|
||||||
class OrderAdminModel(ModelBaseView):
|
class OrderAdminModel(ModelBaseView):
|
||||||
|
"Class for the model of a OrderAdmin"
|
||||||
# pylint: disable=too-few-public-methods
|
# pylint: disable=too-few-public-methods
|
||||||
column_default_sort = ("starttime", True)
|
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 = {
|
column_labels = {
|
||||||
"starttime": "Start Time", "stoptime": "Closing Time",
|
"starttime": "Start Time",
|
||||||
"location_id": "HLDS Location ID"}
|
"stoptime": "Closing Time",
|
||||||
|
"location_id": "HLDS Location ID",
|
||||||
|
"association": "Association",
|
||||||
|
}
|
||||||
form_excluded_columns = ["items", "courier_id"]
|
form_excluded_columns = ["items", "courier_id"]
|
||||||
can_delete = False
|
can_delete = False
|
||||||
|
|
||||||
|
|
||||||
class OrderItemAdminModel(ModelBaseView):
|
class OrderItemAdminModel(ModelBaseView):
|
||||||
|
"Class for the model of a OrderItemAdmin"
|
||||||
# pylint: disable=too-few-public-methods
|
# pylint: disable=too-few-public-methods
|
||||||
column_default_sort = ("order_id", True)
|
column_default_sort = ("order_id", True)
|
||||||
column_list = [
|
column_list = [
|
||||||
"order_id", "order.location_name", "user_name", "user", "dish_name", "dish_id", "comment", "price", "paid",
|
"order_id",
|
||||||
"hlds_data_version"
|
"slug",
|
||||||
|
"order.location_name",
|
||||||
|
"user_name",
|
||||||
|
"user",
|
||||||
|
"dish_name",
|
||||||
|
"dish_id",
|
||||||
|
"comment",
|
||||||
|
"price",
|
||||||
|
"paid",
|
||||||
|
"hlds_data_version",
|
||||||
]
|
]
|
||||||
column_labels = {
|
column_labels = {
|
||||||
"order_id": "Order", "order.location_name": "Order's Location",
|
"order_id": "Order",
|
||||||
"user_name": "Anon. User", "user_id": "Registered User",
|
"order.location_name": "Order's Location",
|
||||||
"hlds_data_version": "HLDS Data Version", "dish_id": "HLDS Dish ID"}
|
"user_name": "Anon. User",
|
||||||
|
"user_id": "Registered User",
|
||||||
|
"hlds_data_version": "HLDS Data Version",
|
||||||
|
"dish_id": "HLDS Dish ID",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def init_admin(app: Flask, database: SQLAlchemy) -> None:
|
def init_admin(app: Flask, database: SQLAlchemy) -> None:
|
||||||
|
|
85
app/app.py
85
app/app.py
|
@ -1,29 +1,35 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
"Main Haldis script"
|
"""Main Haldis script"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from logging.handlers import TimedRotatingFileHandler
|
import sentry_sdk
|
||||||
import typing
|
import typing
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from logging.handlers import TimedRotatingFileHandler
|
||||||
|
|
||||||
from flask import Flask, render_template
|
from admin import init_admin
|
||||||
|
from config import Configuration
|
||||||
|
from flask import Flask, render_template, Response
|
||||||
from flask_bootstrap import Bootstrap, StaticCDN
|
from flask_bootstrap import Bootstrap, StaticCDN
|
||||||
from flask_debugtoolbar import DebugToolbarExtension
|
from flask_debugtoolbar import DebugToolbarExtension
|
||||||
from flask_login import LoginManager
|
from flask_login import LoginManager
|
||||||
from flask_migrate import Migrate, MigrateCommand
|
from flask_migrate import Migrate, MigrateCommand
|
||||||
from flask_oauthlib.client import OAuth, OAuthException
|
|
||||||
from flask_script import Manager, Server
|
from flask_script import Manager, Server
|
||||||
from markupsafe import Markup
|
from markupsafe import Markup
|
||||||
|
|
||||||
from admin import init_admin
|
from admin import init_admin
|
||||||
from login import init_login
|
from auth.login import init_login
|
||||||
|
from auth.zeus import init_oauth
|
||||||
|
from config import Configuration
|
||||||
from models import db
|
from models import db
|
||||||
from models.anonymous_user import AnonymouseUser
|
from models.anonymous_user import AnonymouseUser
|
||||||
from utils import euro_string, price_range_string
|
from sentry_sdk.integrations.flask import FlaskIntegration
|
||||||
from zeus import init_oauth
|
from utils import euro_string, price_range_string, ignore_none
|
||||||
|
|
||||||
|
|
||||||
def register_plugins(app: Flask) -> Manager:
|
def register_plugins(app: Flask) -> Manager:
|
||||||
|
"""Register the plugins to the app"""
|
||||||
# pylint: disable=W0612
|
# pylint: disable=W0612
|
||||||
if not app.debug:
|
if not app.debug:
|
||||||
timedFileHandler = TimedRotatingFileHandler(
|
timedFileHandler = TimedRotatingFileHandler(
|
||||||
|
@ -68,7 +74,8 @@ def register_plugins(app: Flask) -> Manager:
|
||||||
|
|
||||||
# Make cookies more secure
|
# Make cookies more secure
|
||||||
app.config.update(
|
app.config.update(
|
||||||
SESSION_COOKIE_HTTPONLY=True, SESSION_COOKIE_SAMESITE="Lax",
|
SESSION_COOKIE_HTTPONLY=True,
|
||||||
|
SESSION_COOKIE_SAMESITE="Lax",
|
||||||
)
|
)
|
||||||
|
|
||||||
if not app.debug:
|
if not app.debug:
|
||||||
|
@ -78,7 +85,8 @@ def register_plugins(app: Flask) -> Manager:
|
||||||
|
|
||||||
|
|
||||||
def add_handlers(app: Flask) -> None:
|
def add_handlers(app: Flask) -> None:
|
||||||
"Add handlers for 4xx error codes"
|
"""Add handlers for 4xx error codes"""
|
||||||
|
|
||||||
# pylint: disable=W0612,W0613
|
# pylint: disable=W0612,W0613
|
||||||
@app.errorhandler(404)
|
@app.errorhandler(404)
|
||||||
def handle404(e) -> typing.Tuple[str, int]:
|
def handle404(e) -> typing.Tuple[str, int]:
|
||||||
|
@ -90,29 +98,34 @@ def add_handlers(app: Flask) -> None:
|
||||||
|
|
||||||
|
|
||||||
def add_routes(application: Flask) -> None:
|
def add_routes(application: Flask) -> None:
|
||||||
"Add all routes to Haldis"
|
"""Add all routes to Haldis"""
|
||||||
# import views # TODO convert to blueprint
|
# import views # TODO convert to blueprint
|
||||||
# import views.stats # TODO convert to blueprint
|
# import views.stats # TODO convert to blueprint
|
||||||
|
|
||||||
from views.order import order_bp
|
from auth.login import auth_bp
|
||||||
from views.general import general_bp
|
from auth.microsoft import auth_microsoft_bp
|
||||||
from views.stats import stats_blueprint
|
from auth.zeus import auth_zeus_bp
|
||||||
from views.debug import debug_bp
|
from views.debug import debug_bp
|
||||||
from login import auth_bp
|
from views.general import general_bp
|
||||||
from zeus import oauth_bp
|
from views.order import order_bp
|
||||||
|
from views.stats import stats_blueprint
|
||||||
|
|
||||||
application.register_blueprint(general_bp, url_prefix="/")
|
application.register_blueprint(general_bp, url_prefix="/")
|
||||||
application.register_blueprint(order_bp, url_prefix="/order")
|
application.register_blueprint(order_bp, url_prefix="/order")
|
||||||
application.register_blueprint(stats_blueprint, url_prefix="/stats")
|
application.register_blueprint(stats_blueprint, url_prefix="/stats")
|
||||||
application.register_blueprint(auth_bp, url_prefix="/")
|
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:
|
if application.debug:
|
||||||
application.register_blueprint(debug_bp, url_prefix="/debug")
|
application.register_blueprint(debug_bp, url_prefix="/debug")
|
||||||
|
|
||||||
|
|
||||||
def add_template_filters(app: Flask) -> None:
|
def add_template_filters(app: Flask) -> None:
|
||||||
"Add functions which can be used in the templates"
|
"""Add functions which can be used in the templates"""
|
||||||
|
|
||||||
# pylint: disable=W0612
|
# pylint: disable=W0612
|
||||||
@app.template_filter("countdown")
|
@app.template_filter("countdown")
|
||||||
def countdown(
|
def countdown(
|
||||||
|
@ -147,19 +160,41 @@ def add_template_filters(app: Flask) -> None:
|
||||||
app.template_filter("price_range")(price_range_string)
|
app.template_filter("price_range")(price_range_string)
|
||||||
app.template_filter("any")(any)
|
app.template_filter("any")(any)
|
||||||
app.template_filter("all")(all)
|
app.template_filter("all")(all)
|
||||||
|
app.template_filter("ignore_none")(ignore_none)
|
||||||
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
def create_app():
|
||||||
|
"""Initializer for the Flask app object"""
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
# Load the config file
|
@app.route('/robots.txt')
|
||||||
app.config.from_object("config.Configuration")
|
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
|
||||||
|
|
||||||
app_manager = register_plugins(app)
|
# Load the config file
|
||||||
add_handlers(app)
|
app.config.from_object("config.Configuration")
|
||||||
add_routes(app)
|
|
||||||
add_template_filters(app)
|
app_manager = register_plugins(app)
|
||||||
|
add_handlers(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
|
# For usage when you directly call the script with python
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app_manager.run()
|
if Configuration.SENTRY_DSN:
|
||||||
|
sentry_sdk.init(
|
||||||
|
dsn=Configuration.SENTRY_DSN,
|
||||||
|
integrations=[FlaskIntegration()]
|
||||||
|
)
|
||||||
|
|
||||||
|
app, app_mgr = create_app()
|
||||||
|
app_mgr.run()
|
||||||
|
|
|
@ -1,32 +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 import Blueprint, abort, redirect, session, url_for
|
||||||
from flask_login import current_user, logout_user
|
from flask_login import current_user, logout_user
|
||||||
from werkzeug.wrappers import Response
|
|
||||||
|
|
||||||
from models import User
|
from models import User
|
||||||
from zeus import zeus_login
|
from werkzeug.wrappers import Response
|
||||||
|
|
||||||
auth_bp = Blueprint("auth_bp", __name__)
|
auth_bp = Blueprint("auth_bp", __name__)
|
||||||
|
|
||||||
|
|
||||||
def init_login(app) -> None:
|
def init_login(app) -> None:
|
||||||
"Initialize the login"
|
"""Initialize the login"""
|
||||||
|
|
||||||
# pylint: disable=W0612
|
# pylint: disable=W0612
|
||||||
@app.login_manager.user_loader
|
@app.login_manager.user_loader
|
||||||
def load_user(userid) -> User:
|
def load_user(userid) -> User:
|
||||||
"Load the user"
|
"""Load the user"""
|
||||||
return User.query.filter_by(id=userid).first()
|
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")
|
@auth_bp.route("/logout")
|
||||||
def logout() -> Response:
|
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:
|
if "zeus_token" in session:
|
||||||
session.pop("zeus_token", None)
|
session.pop("zeus_token", None)
|
||||||
logout_user()
|
logout_user()
|
||||||
|
@ -34,6 +27,6 @@ def logout() -> Response:
|
||||||
|
|
||||||
|
|
||||||
def before_request() -> None:
|
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():
|
if current_user.is_anonymous() or not current_user.is_allowed():
|
||||||
abort(401)
|
abort(401)
|
77
app/auth/microsoft.py
Normal file
77
app/auth/microsoft.py
Normal 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
|
|
@ -1,34 +1,37 @@
|
||||||
"Script containing everything specific to ZeusWPI"
|
"Script containing everything specific to ZeusWPI"
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
from flask import Blueprint, current_app, flash, redirect, request, session, url_for
|
from flask import (Blueprint, current_app, flash, redirect, request, session,
|
||||||
|
url_for)
|
||||||
from flask_login import login_user
|
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
|
from werkzeug.wrappers import Response
|
||||||
|
|
||||||
from models import User, db
|
auth_zeus_bp = Blueprint("auth_zeus_bp", __name__)
|
||||||
|
|
||||||
oauth_bp = Blueprint("oauth_bp", __name__)
|
|
||||||
|
|
||||||
|
|
||||||
def zeus_login():
|
def zeus_login():
|
||||||
"Log in using ZeusWPI"
|
"""Log in using ZeusWPI"""
|
||||||
return current_app.zeus.authorize(
|
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:
|
def authorized() -> typing.Any:
|
||||||
# type is 'typing.Union[str, Response]', but this errors due to
|
# type is 'typing.Union[str, Response]', but this errors due to
|
||||||
# https://github.com/python/mypy/issues/7187
|
# https://github.com/python/mypy/issues/7187
|
||||||
"Check authorized status"
|
"""Check authorized status"""
|
||||||
resp = current_app.zeus.authorized_response()
|
resp = current_app.zeus.authorized_response()
|
||||||
if resp is None:
|
if resp is None:
|
||||||
return "Access denied: reason=%s error=%s" % (
|
# pylint: disable=C0301
|
||||||
request.args["error"],
|
return f"Access denied: reason={request.args['error']} error={request.args['error_description']}"
|
||||||
request.args["error_description"],
|
|
||||||
)
|
|
||||||
if isinstance(resp, OAuthException):
|
if isinstance(resp, OAuthException):
|
||||||
return f"Access denied: {resp.message}<br>{resp.data}"
|
return f"Access denied: {resp.message}<br>{resp.data}"
|
||||||
|
|
||||||
|
@ -48,8 +51,8 @@ def authorized() -> typing.Any:
|
||||||
return redirect(url_for("general_bp.home"))
|
return redirect(url_for("general_bp.home"))
|
||||||
|
|
||||||
|
|
||||||
def init_oauth(app):
|
def init_oauth(app) -> OAuthRemoteApp:
|
||||||
"Initialize the OAuth for ZeusWPI"
|
"""Initialize the OAuth for ZeusWPI"""
|
||||||
oauth = OAuth(app)
|
oauth = OAuth(app)
|
||||||
|
|
||||||
zeus = oauth.remote_app(
|
zeus = oauth.remote_app(
|
||||||
|
@ -72,15 +75,15 @@ def init_oauth(app):
|
||||||
|
|
||||||
|
|
||||||
def login_and_redirect_user(user) -> Response:
|
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)
|
login_user(user)
|
||||||
return redirect(url_for("general_bp.home"))
|
return redirect(url_for("general_bp.home"))
|
||||||
|
|
||||||
|
|
||||||
def create_user(username) -> User:
|
def create_user(username) -> User:
|
||||||
"Create a temporary user if it is needed"
|
"""Create a temporary user if it is needed"""
|
||||||
user = User()
|
user = User()
|
||||||
user.configure(username, False, 1)
|
user.configure(username, False, 1, associations=["zeus"])
|
||||||
db.session.add(user)
|
db.session.add(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return user
|
return user
|
|
@ -1,15 +1,26 @@
|
||||||
"An example for a Haldis config"
|
"""An example for a Haldis config"""
|
||||||
# config
|
# import os
|
||||||
|
|
||||||
|
|
||||||
class Configuration:
|
class Configuration:
|
||||||
"Haldis configuration object"
|
"Haldis configuration object"
|
||||||
# pylint: disable=too-few-public-methods
|
# pylint: disable=too-few-public-methods
|
||||||
SQLALCHEMY_DATABASE_URI = "sqlite:///haldis.db"
|
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
|
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
|
HALDIS_ADMINS = []
|
||||||
SECRET_KEY = "<change>"
|
SECRET_KEY = "<change>"
|
||||||
SLACK_WEBHOOK = None
|
SLACK_WEBHOOK = None
|
||||||
LOGFILE = "haldis.log"
|
LOGFILE = "haldis.log"
|
||||||
|
SENTRY_DSN = None
|
||||||
ZEUS_KEY = "tomtest"
|
ZEUS_KEY = "tomtest"
|
||||||
ZEUS_SECRET = "blargh"
|
ZEUS_SECRET = "blargh"
|
||||||
|
|
||||||
|
ENABLE_MICROSOFT_AUTH = False
|
||||||
|
MICROSOFT_AUTH_ID = ""
|
||||||
|
MICROSOFT_AUTH_SECRET = ""
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
"Script for interaction and changes to the database"
|
"""Script for interaction and changes to the database"""
|
||||||
|
|
||||||
import add_admins
|
import add_admins
|
||||||
from app import db, app_manager
|
|
||||||
|
from app import create_app, db
|
||||||
|
|
||||||
|
app, app_manager = create_app()
|
||||||
|
|
||||||
entry_sets = {
|
entry_sets = {
|
||||||
"admins": add_admins.add,
|
"admins": add_admins.add,
|
||||||
|
@ -11,13 +15,13 @@ no = ["no", "n"]
|
||||||
|
|
||||||
|
|
||||||
def commit() -> None:
|
def commit() -> None:
|
||||||
"Commit all the things to the database"
|
"""Commit all the things to the database"""
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
print("Committing successful")
|
print("Committing successful")
|
||||||
|
|
||||||
|
|
||||||
def check_if_overwrite() -> bool:
|
def check_if_overwrite() -> bool:
|
||||||
"Check if the user wants to overwrite the previous database"
|
"""Check if the user wants to overwrite the previous database"""
|
||||||
answer = input("Do you want to overwrite the previous database? (y/N) ")
|
answer = input("Do you want to overwrite the previous database? (y/N) ")
|
||||||
return answer.lower() in yes
|
return answer.lower() in yes
|
||||||
|
|
||||||
|
@ -25,12 +29,12 @@ def check_if_overwrite() -> bool:
|
||||||
def add_all() -> None:
|
def add_all() -> None:
|
||||||
"Add all possible entries in the entry_sets to the database"
|
"Add all possible entries in the entry_sets to the database"
|
||||||
for entry_set, function in entry_sets.items():
|
for entry_set, function in entry_sets.items():
|
||||||
print("Adding {}.".format(entry_set))
|
print(f"Adding {entry_set}.")
|
||||||
function()
|
function()
|
||||||
|
|
||||||
|
|
||||||
def recreate_from_scratch() -> None:
|
def recreate_from_scratch() -> None:
|
||||||
"Recreate a completely new database"
|
"""Recreate a completely new database"""
|
||||||
print("Resetting the database!")
|
print("Resetting the database!")
|
||||||
db.drop_all()
|
db.drop_all()
|
||||||
db.create_all()
|
db.create_all()
|
||||||
|
@ -38,19 +42,19 @@ def recreate_from_scratch() -> None:
|
||||||
|
|
||||||
|
|
||||||
def add_to_current() -> None:
|
def add_to_current() -> None:
|
||||||
"Add things to the current database"
|
"""Add things to the current database"""
|
||||||
available = [entry_set for entry_set in entry_sets]
|
available = list(entry_sets)
|
||||||
|
|
||||||
def add_numbers() -> str:
|
def add_numbers() -> str:
|
||||||
return " ".join(
|
return " ".join(
|
||||||
["{}({}), ".format(loc, i) for i, loc in enumerate(available)]
|
[f"{loc}({i}), " for i, loc in enumerate(available)]
|
||||||
).rstrip(", ")
|
).rstrip(", ")
|
||||||
|
|
||||||
while input("Do you still want to add something? (Y/n) ").lower() not in no:
|
while input("Do you still want to add something? (Y/n) ").lower() not in no:
|
||||||
print(
|
print(
|
||||||
"What do you want to add? (Use numbers, or A for all, or C for cancel) "
|
"What do you want to add? (Use numbers, or A for all, or C for cancel) "
|
||||||
)
|
)
|
||||||
answer = input("Available: {} : ".format(add_numbers()))
|
answer = input(f"Available: {add_numbers()} : ")
|
||||||
if answer.lower() == "a":
|
if answer.lower() == "a":
|
||||||
add_all()
|
add_all()
|
||||||
available = []
|
available = []
|
||||||
|
@ -58,7 +62,7 @@ def add_to_current() -> None:
|
||||||
pass
|
pass
|
||||||
elif answer.isnumeric() and answer in [str(x) for x in range(len(available))]:
|
elif answer.isnumeric() and answer in [str(x) for x in range(len(available))]:
|
||||||
answer_index = int(answer)
|
answer_index = int(answer)
|
||||||
print("Adding {}.".format(available[answer_index]))
|
print(f"Adding {available[answer_index]}.")
|
||||||
entry_sets[str(available[answer_index])]()
|
entry_sets[str(available[answer_index])]()
|
||||||
del available[answer_index]
|
del available[answer_index]
|
||||||
else:
|
else:
|
||||||
|
@ -68,7 +72,7 @@ def add_to_current() -> None:
|
||||||
|
|
||||||
@app_manager.command
|
@app_manager.command
|
||||||
def setup_database(): # type: None
|
def setup_database(): # type: None
|
||||||
"Start the database interaction script"
|
"""Start the database interaction script"""
|
||||||
print("Database modification script!")
|
print("Database modification script!")
|
||||||
print("=============================\n\n")
|
print("=============================\n\n")
|
||||||
if (not db.engine.table_names()) or check_if_overwrite():
|
if (not db.engine.table_names()) or check_if_overwrite():
|
|
@ -1,24 +0,0 @@
|
||||||
"Script for adding users as admin to Haldis."
|
|
||||||
from app import db
|
|
||||||
from models import User
|
|
||||||
|
|
||||||
|
|
||||||
def add() -> None:
|
|
||||||
"Add users as admin."
|
|
||||||
feli = User()
|
|
||||||
feli.configure("feliciaan", True, 0)
|
|
||||||
db.session.add(feli)
|
|
||||||
|
|
||||||
destro = User()
|
|
||||||
destro.configure("destro", True, 0)
|
|
||||||
db.session.add(destro)
|
|
||||||
|
|
||||||
iepoev = User()
|
|
||||||
iepoev.configure("iepoev", True, 1)
|
|
||||||
db.session.add(iepoev)
|
|
||||||
|
|
||||||
flynn = User()
|
|
||||||
flynn.configure("flynn", True, 0)
|
|
||||||
db.session.add(flynn)
|
|
||||||
|
|
||||||
# To future developers, add yourself here
|
|
|
@ -9,6 +9,7 @@ user
|
||||||
|
|
||||||
order
|
order
|
||||||
id
|
id
|
||||||
|
slug secret used in URL
|
||||||
courier_id
|
courier_id
|
||||||
location_id HLDS identifier
|
location_id HLDS identifier
|
||||||
location_name this allows historical orders to keep the same location name
|
location_name this allows historical orders to keep the same location name
|
||||||
|
|
|
@ -1,23 +1,31 @@
|
||||||
|
"Module used for everything related to the fat versions of models"
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
from sqlalchemy.sql import desc, func
|
|
||||||
|
|
||||||
from hlds.definitions import location_definitions
|
from hlds.definitions import location_definitions
|
||||||
from hlds.models import Location, Dish
|
from hlds.models import Dish, Location
|
||||||
from models import Order, OrderItem, User
|
from models import Order, OrderItem, User
|
||||||
|
from sqlalchemy.sql import desc, func
|
||||||
|
|
||||||
|
|
||||||
class FatModel:
|
class FatModel:
|
||||||
|
"General class for the fat version of models"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def all(cls):
|
def all(cls):
|
||||||
|
"Function to query all"
|
||||||
|
# pylint: disable=E1101
|
||||||
return cls.query.all()
|
return cls.query.all()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def amount(cls):
|
def amount(cls):
|
||||||
|
"Function to query the amount"
|
||||||
|
# pylint: disable=E1101
|
||||||
return cls.query.count()
|
return cls.query.count()
|
||||||
|
|
||||||
|
|
||||||
class FatLocation(Location, FatModel):
|
class FatLocation(Location, FatModel):
|
||||||
|
"Fat version of the Location model"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def all(cls):
|
def all(cls):
|
||||||
return location_definitions
|
return location_definitions
|
||||||
|
@ -28,6 +36,7 @@ class FatLocation(Location, FatModel):
|
||||||
|
|
||||||
|
|
||||||
class FatOrder(Order, FatModel):
|
class FatOrder(Order, FatModel):
|
||||||
|
"Fat version of the Order model"
|
||||||
|
|
||||||
# It's hard to add the unique user constraint,
|
# It's hard to add the unique user constraint,
|
||||||
# as DISTINCT seems to apply after a GROUP BY and aggregate
|
# as DISTINCT seems to apply after a GROUP BY and aggregate
|
||||||
|
@ -35,16 +44,15 @@ class FatOrder(Order, FatModel):
|
||||||
# even if they get reduced by the disctinct afterwards.
|
# even if they get reduced by the disctinct afterwards.
|
||||||
@classmethod
|
@classmethod
|
||||||
def items_per_order(cls):
|
def items_per_order(cls):
|
||||||
return (
|
"Function to get the total of all items per order"
|
||||||
Order.query.join(OrderItem)
|
return (Order.query.join(OrderItem).group_by(Order.id).with_entities(
|
||||||
.group_by(Order.id)
|
Order.id,
|
||||||
.with_entities(Order.id, func.count(OrderItem.user_id).label("total"))
|
func.count(OrderItem.user_id).label("total")))
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class FatUser(User, FatModel):
|
class FatUser(User, FatModel):
|
||||||
pass
|
"Fat version of the User model"
|
||||||
|
|
||||||
|
|
||||||
class FatOrderItem(OrderItem, FatModel):
|
class FatOrderItem(OrderItem, FatModel):
|
||||||
pass
|
"Fat version of the OrderItem model"
|
||||||
|
|
31
app/forms.py
31
app/forms.py
|
@ -1,25 +1,16 @@
|
||||||
"Script for everything form related in Haldis"
|
"Script for everything form related in Haldis"
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from flask import session, request
|
from flask import request, session
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from flask_wtf import FlaskForm as Form
|
from flask_wtf import FlaskForm as Form
|
||||||
from wtforms import (
|
|
||||||
DateTimeField,
|
|
||||||
SelectField,
|
|
||||||
SelectMultipleField,
|
|
||||||
StringField,
|
|
||||||
SubmitField,
|
|
||||||
FieldList,
|
|
||||||
validators,
|
|
||||||
)
|
|
||||||
|
|
||||||
from utils import euro_string, price_range_string
|
|
||||||
from hlds.definitions import location_definitions
|
from hlds.definitions import location_definitions
|
||||||
from hlds.models import Location, Dish, Choice
|
from hlds.models import Choice, Dish, Location
|
||||||
from models import User
|
from models import User
|
||||||
|
from utils import euro_string, price_range_string
|
||||||
|
from wtforms import (DateTimeField, FieldList, SelectField,
|
||||||
|
SelectMultipleField, StringField, SubmitField, validators)
|
||||||
|
|
||||||
|
|
||||||
class OrderForm(Form):
|
class OrderForm(Form):
|
||||||
|
@ -33,13 +24,17 @@ class OrderForm(Form):
|
||||||
"Starttime", default=datetime.now, format="%d-%m-%Y %H:%M"
|
"Starttime", default=datetime.now, format="%d-%m-%Y %H:%M"
|
||||||
)
|
)
|
||||||
stoptime = DateTimeField("Stoptime", 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")
|
submit_button = SubmitField("Submit")
|
||||||
|
|
||||||
def populate(self) -> None:
|
def populate(self) -> None:
|
||||||
"Fill in the options for courier for an Order"
|
"Fill in the options for courier for an Order"
|
||||||
if current_user.is_admin():
|
if current_user.is_admin():
|
||||||
self.courier_id.choices = [(0, None)] + [
|
self.courier_id.choices = [
|
||||||
(u.id, u.username) for u in User.query.order_by("username")
|
(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:
|
else:
|
||||||
self.courier_id.choices = [
|
self.courier_id.choices = [
|
||||||
|
@ -47,6 +42,7 @@ class OrderForm(Form):
|
||||||
(current_user.id, current_user.username),
|
(current_user.id, current_user.username),
|
||||||
]
|
]
|
||||||
self.location_id.choices = [(l.id, l.name) for l in location_definitions]
|
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:
|
if self.stoptime.data is None:
|
||||||
self.stoptime.data = datetime.now() + timedelta(hours=1)
|
self.stoptime.data = datetime.now() + timedelta(hours=1)
|
||||||
|
|
||||||
|
@ -59,6 +55,7 @@ class OrderItemForm(Form):
|
||||||
submit_button = SubmitField("Submit")
|
submit_button = SubmitField("Submit")
|
||||||
|
|
||||||
def populate(self, location: Location) -> None:
|
def populate(self, location: Location) -> None:
|
||||||
|
"Populate the order item form"
|
||||||
self.dish_id.choices = [(dish.id, dish.name) for dish in location.dishes]
|
self.dish_id.choices = [(dish.id, dish.name) for dish in location.dishes]
|
||||||
if not self.is_submitted() and self.comment.data is None:
|
if not self.is_submitted() and self.comment.data is None:
|
||||||
self.comment.data = request.args.get("comment")
|
self.comment.data = request.args.get("comment")
|
||||||
|
@ -85,7 +82,7 @@ class AnonOrderItemForm(OrderItemForm):
|
||||||
self.user_name.data = session.get("anon_name", None)
|
self.user_name.data = session.get("anon_name", None)
|
||||||
|
|
||||||
def validate(self) -> bool:
|
def validate(self) -> bool:
|
||||||
"Check if the provided anon_name is not already taken"
|
"""Check if the provided anon_name is not already taken"""
|
||||||
rv = OrderForm.validate(self)
|
rv = OrderForm.validate(self)
|
||||||
if not rv:
|
if not rv:
|
||||||
return False
|
return False
|
||||||
|
|
|
@ -6,4 +6,4 @@ These are not imported in this module's init, to avoid opening the definition fi
|
||||||
parser on them when testing other code in this module, or when testing the parser on other files.
|
parser on them when testing other code in this module, or when testing the parser on other files.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from .models import Location, Choice, Option
|
from .models import Choice, Location, Option
|
||||||
|
|
|
@ -1,21 +1,24 @@
|
||||||
# Import this class to load the standard HLDS definitions
|
# Import this class to load the standard HLDS definitions
|
||||||
|
|
||||||
from os import path
|
|
||||||
from typing import List
|
|
||||||
import subprocess
|
import subprocess
|
||||||
from .parser import parse_all_directory
|
from pathlib import Path
|
||||||
from .models import Location
|
from typing import List
|
||||||
|
|
||||||
|
from .models import Location
|
||||||
|
from .parser import parse_all_directory
|
||||||
|
|
||||||
__all__ = ["location_definitions", "location_definition_version"]
|
__all__ = ["location_definitions", "location_definition_version"]
|
||||||
|
|
||||||
# pylint: disable=invalid-name
|
# pylint: disable=invalid-name
|
||||||
|
|
||||||
# TODO Use proper way to get resources, see https://stackoverflow.com/a/10935674
|
# 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)
|
location_definitions.sort(key=lambda l: l.name)
|
||||||
|
|
||||||
proc = subprocess.run(["git", "rev-parse", "HEAD"], stdout=subprocess.PIPE, check=True)
|
try:
|
||||||
location_definition_version = proc.stdout.decode().strip()
|
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 = ""
|
||||||
|
|
|
@ -29,9 +29,9 @@ location = >location_header items:{ block } ;
|
||||||
|
|
||||||
|
|
||||||
attributes =
|
attributes =
|
||||||
name:/[^\n#]*?(?= +-- | | *\n| *#)/
|
name:/[^\n#]*?(?= +-- | | €| *\n| *#)/
|
||||||
[ s '--' ~ s description:/[^\n#]*?(?= | *\n| *#)/ ]
|
[ s '--' ~ s description:/[^\n#]*?(?= | *\n| *#)/ ]
|
||||||
[ / {2,}/ ~
|
[ / +/ ~
|
||||||
[ {[ s ] ('{' tags+:identifier '}')} / +|$/ ]
|
[ {[ s ] ('{' tags+:identifier '}')} / +|$/ ]
|
||||||
[ price:price ]
|
[ price:price ]
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,26 +1,28 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# pylint: disable=too-few-public-methods
|
# pylint: disable=too-few-public-methods
|
||||||
|
|
||||||
from typing import Iterable, List, Tuple, Mapping, Any, Optional
|
from typing import Any, Iterable, List, Mapping, Optional, Tuple
|
||||||
|
|
||||||
from utils import euro_string, first
|
from utils import euro_string, first
|
||||||
|
|
||||||
|
|
||||||
def _format_tags(tags: Iterable[str]) -> str:
|
def _format_tags(tags: Iterable[str]) -> str:
|
||||||
return " :: {}".format(" ".join(["{" + tag + "}" for tag in tags])) \
|
# pylint: disable=consider-using-f-string
|
||||||
if tags \
|
return " :: {}".format(" ".join(["{" + tag + "}"
|
||||||
else ""
|
for tag in tags])) if tags else ""
|
||||||
|
|
||||||
|
|
||||||
def _format_price(price: int) -> str:
|
def _format_price(price: int) -> str:
|
||||||
return " {}".format(euro_string(price)) if price else ""
|
return f" {euro_string(price)}" if price else ""
|
||||||
|
|
||||||
|
|
||||||
def _format_type_and_choice(type_and_choice):
|
def _format_type_and_choice(type_and_choice):
|
||||||
type_, choice = type_and_choice
|
type_, choice = type_and_choice
|
||||||
return "{} {}".format(type_, choice)
|
return f"{type_} {choice}"
|
||||||
|
|
||||||
|
|
||||||
class Option:
|
class Option:
|
||||||
|
|
||||||
def __init__(self, id_, *, name, description, price, tags):
|
def __init__(self, id_, *, name, description, price, tags):
|
||||||
self.id: str = id_
|
self.id: str = id_
|
||||||
self.name: str = name
|
self.name: str = name
|
||||||
|
@ -29,15 +31,17 @@ class Option:
|
||||||
self.tags: List[str] = tags
|
self.tags: List[str] = tags
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
# pylint: disable=consider-using-f-string
|
||||||
return "{0.id}: {0.name}{1}{2}{3}".format(
|
return "{0.id}: {0.name}{1}{2}{3}".format(
|
||||||
self,
|
self,
|
||||||
" -- {}".format(self.description) if self.description else "",
|
f" -- {self.description}" if self.description else "",
|
||||||
_format_tags(self.tags),
|
_format_tags(self.tags),
|
||||||
_format_price(self.price),
|
_format_price(self.price),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Choice:
|
class Choice:
|
||||||
|
|
||||||
def __init__(self, id_, *, name, description, options):
|
def __init__(self, id_, *, name, description, options):
|
||||||
self.id: str = id_
|
self.id: str = id_
|
||||||
self.name: str = name
|
self.name: str = name
|
||||||
|
@ -48,7 +52,7 @@ class Choice:
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "{0.id}: {0.name}{1}\n\t\t{2}".format(
|
return "{0.id}: {0.name}{1}\n\t\t{2}".format(
|
||||||
self,
|
self,
|
||||||
" -- {}".format(self.description) if self.description else "",
|
f" -- {self.description}" if self.description else "",
|
||||||
"\n\t\t".join(map(str, self.options)),
|
"\n\t\t".join(map(str, self.options)),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -57,6 +61,7 @@ class Choice:
|
||||||
|
|
||||||
|
|
||||||
class Dish:
|
class Dish:
|
||||||
|
|
||||||
def __init__(self, id_, *, name, description, price, tags, choices):
|
def __init__(self, id_, *, name, description, price, tags, choices):
|
||||||
self.id: str = id_
|
self.id: str = id_
|
||||||
self.name: str = name
|
self.name: str = name
|
||||||
|
@ -70,7 +75,7 @@ class Dish:
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "dish {0.id}: {0.name}{1}{2}{3}\n\t{4}".format(
|
return "dish {0.id}: {0.name}{1}{2}{3}\n\t{4}".format(
|
||||||
self,
|
self,
|
||||||
" -- {}".format(self.description) if self.description else "",
|
f" -- {self.description}" if self.description else "",
|
||||||
_format_tags(self.tags),
|
_format_tags(self.tags),
|
||||||
_format_price(self.price),
|
_format_price(self.price),
|
||||||
"\n\t".join(map(_format_type_and_choice, self.choices)),
|
"\n\t".join(map(_format_type_and_choice, self.choices)),
|
||||||
|
@ -86,14 +91,20 @@ class Dish:
|
||||||
return sum(
|
return sum(
|
||||||
f(option.price for option in choice.options)
|
f(option.price for option in choice.options)
|
||||||
for (choice_type, choice) in self.choices
|
for (choice_type, choice) in self.choices
|
||||||
if choice_type == "single_choice"
|
if choice_type == "single_choice")
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Location:
|
class Location:
|
||||||
def __init__(
|
|
||||||
self, id_, *, name, dishes, osm=None, address=None, telephone=None, website=None
|
def __init__(self,
|
||||||
):
|
id_,
|
||||||
|
*,
|
||||||
|
name,
|
||||||
|
dishes,
|
||||||
|
osm=None,
|
||||||
|
address=None,
|
||||||
|
telephone=None,
|
||||||
|
website=None):
|
||||||
self.id: str = id_
|
self.id: str = id_
|
||||||
self.name: str = name
|
self.name: str = name
|
||||||
self.osm: Optional[str] = osm
|
self.osm: Optional[str] = osm
|
||||||
|
@ -107,24 +118,18 @@ class Location:
|
||||||
return first(filter(lambda d: d.id == dish_id, self.dishes))
|
return first(filter(lambda d: d.id == dish_id, self.dishes))
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return (
|
return ("============================\n"
|
||||||
"============================\n"
|
|
||||||
"{0.id}: {0.name}"
|
"{0.id}: {0.name}"
|
||||||
"{1}\n"
|
"{1}\n"
|
||||||
"============================\n"
|
"============================\n"
|
||||||
"\n"
|
"\n"
|
||||||
"{2}"
|
"{2}").format(
|
||||||
).format(
|
|
||||||
self,
|
self,
|
||||||
"".join(
|
"".join(f"\n\t{k} {v}" for k, v in (
|
||||||
"\n\t{} {}".format(k, v)
|
|
||||||
for k, v in (
|
|
||||||
("osm", self.osm),
|
("osm", self.osm),
|
||||||
("address", self.address),
|
("address", self.address),
|
||||||
("telephone", self.telephone),
|
("telephone", self.telephone),
|
||||||
("website", self.website),
|
("website", self.website),
|
||||||
)
|
) if v is not None),
|
||||||
if v is not None
|
|
||||||
),
|
|
||||||
"\n".join(map(str, self.dishes)),
|
"\n".join(map(str, self.dishes)),
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,16 +1,17 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
from glob import glob
|
|
||||||
from os import path
|
|
||||||
import itertools
|
import itertools
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from typing import Iterable, List, Union, Tuple
|
from glob import glob
|
||||||
|
from os import path
|
||||||
|
from typing import Iterable, List, Tuple, Union
|
||||||
|
|
||||||
from tatsu import parse as tatsu_parse
|
from tatsu import parse as tatsu_parse
|
||||||
from tatsu.ast import AST
|
from tatsu.ast import AST
|
||||||
from tatsu.exceptions import SemanticError
|
from tatsu.exceptions import SemanticError
|
||||||
from .models import Location, Choice, Option, Dish
|
|
||||||
from utils import first
|
from utils import first
|
||||||
|
|
||||||
|
from .models import Choice, Dish, Location, Option
|
||||||
|
|
||||||
# TODO Use proper way to get resources, see https://stackoverflow.com/a/10935674
|
# TODO Use proper way to get resources, see https://stackoverflow.com/a/10935674
|
||||||
with open(path.join(path.dirname(__file__), "hlds.tatsu")) as fh:
|
with open(path.join(path.dirname(__file__), "hlds.tatsu")) as fh:
|
||||||
|
@ -58,14 +59,16 @@ class HldsSemanticActions:
|
||||||
option.price += dish.price
|
option.price += dish.price
|
||||||
dish.price = 0
|
dish.price = 0
|
||||||
dishes = list(dishes)
|
dishes = list(dishes)
|
||||||
dishes.append(Dish(
|
dishes.append(
|
||||||
|
Dish(
|
||||||
"custom",
|
"custom",
|
||||||
name="Vrije keuze",
|
name="Vrije keuze",
|
||||||
description="Zet wat je wil in comment",
|
description="Zet wat je wil in comment",
|
||||||
price=0,
|
price=0,
|
||||||
tags=[],
|
tags=[],
|
||||||
choices=[],
|
choices=[],
|
||||||
))
|
)
|
||||||
|
)
|
||||||
|
|
||||||
attributes = {att["key"]: att["value"] for att in ast["attributes"]}
|
attributes = {att["key"]: att["value"] for att in ast["attributes"]}
|
||||||
|
|
||||||
|
@ -145,7 +148,7 @@ def parse(menu: str) -> List[Location]:
|
||||||
|
|
||||||
|
|
||||||
def parse_file(filename: str) -> List[Location]:
|
def parse_file(filename: str) -> List[Location]:
|
||||||
with open(filename, "r") as file_handle:
|
with open(filename) as file_handle:
|
||||||
return parse(file_handle.read())
|
return parse(file_handle.read())
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
"Script that runs migrations online or offline"
|
"Script that runs migrations online or offline"
|
||||||
from __future__ import with_statement
|
|
||||||
|
|
||||||
from logging.config import fileConfig
|
from logging.config import fileConfig
|
||||||
|
|
||||||
from alembic import context
|
from alembic import context
|
||||||
|
|
||||||
# add your model's MetaData object here
|
# add your model's MetaData object here
|
||||||
# for 'autogenerate' support
|
# for 'autogenerate' support
|
||||||
# from myapp import mymodel
|
# from myapp import mymodel
|
||||||
|
|
|
@ -43,7 +43,7 @@ def upgrade():
|
||||||
sa.Column("starttime", sa.DateTime(), nullable=True),
|
sa.Column("starttime", sa.DateTime(), nullable=True),
|
||||||
sa.Column("stoptime", sa.DateTime(), nullable=True),
|
sa.Column("stoptime", sa.DateTime(), nullable=True),
|
||||||
sa.Column("public", sa.Boolean(), 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"),
|
sa.PrimaryKeyConstraint("id"),
|
||||||
)
|
)
|
||||||
op.create_table(
|
op.create_table(
|
||||||
|
@ -65,7 +65,7 @@ def upgrade():
|
||||||
sa.Column("extra", sa.String(length=254), nullable=True),
|
sa.Column("extra", sa.String(length=254), nullable=True),
|
||||||
sa.Column("name", sa.String(length=120), nullable=True),
|
sa.Column("name", sa.String(length=120), nullable=True),
|
||||||
sa.ForeignKeyConstraint(["order_id"], ["order.id"]),
|
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.ForeignKeyConstraint(["user_id"], ["user.id"]),
|
||||||
sa.PrimaryKeyConstraint("id"),
|
sa.PrimaryKeyConstraint("id"),
|
||||||
)
|
)
|
||||||
|
|
30
app/migrations/versions/29ccbe077c57_add_slug.py
Normal file
30
app/migrations/versions/29ccbe077c57_add_slug.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
"""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')
|
|
@ -0,0 +1,21 @@
|
||||||
|
"""Create price_modified column
|
||||||
|
|
||||||
|
Revision ID: 55013fe95bea
|
||||||
|
Revises: 9159a6fed021
|
||||||
|
Create Date: 2022-04-22 01:00:03.729596
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '55013fe95bea'
|
||||||
|
down_revision = '9159a6fed021'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
op.add_column('order_item', sa.Column('price_modified', sa.DateTime(), nullable=True))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
op.drop_column('order_item', 'price_modified')
|
26
app/migrations/versions/89b2c980b663_.py
Normal file
26
app/migrations/versions/89b2c980b663_.py
Normal 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 ###
|
|
@ -12,11 +12,11 @@ revision = "9159a6fed021"
|
||||||
down_revision = "150252c1cdb1"
|
down_revision = "150252c1cdb1"
|
||||||
|
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
from sqlalchemy.sql import table, column, text
|
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
from hlds.definitions import location_definitions
|
from hlds.definitions import location_definitions
|
||||||
|
from sqlalchemy.sql import column, table, text
|
||||||
|
|
||||||
LOCATION_LEGACY_TO_HLDS = {
|
LOCATION_LEGACY_TO_HLDS = {
|
||||||
2: "blauw_kotje",
|
2: "blauw_kotje",
|
||||||
|
@ -50,62 +50,94 @@ LOCATION_LEGACY_TO_HLDS = {
|
||||||
|
|
||||||
def upgrade():
|
def upgrade():
|
||||||
# First the simple actions
|
# First the simple actions
|
||||||
op.create_table("order_item_choice",
|
op.create_table(
|
||||||
|
"order_item_choice",
|
||||||
sa.Column("id", sa.Integer, nullable=False),
|
sa.Column("id", sa.Integer, nullable=False),
|
||||||
sa.Column("choice_id", sa.String(length=64), nullable=True),
|
sa.Column("choice_id", sa.String(length=64), nullable=True),
|
||||||
sa.Column("order_item_id", sa.Integer, nullable=False),
|
sa.Column("order_item_id", sa.Integer, nullable=False),
|
||||||
sa.Column("kind", sa.String(length=1), nullable=False),
|
sa.Column("kind", sa.String(length=1), nullable=False),
|
||||||
sa.Column("name", sa.String(length=120), nullable=True),
|
sa.Column("name", sa.String(length=120), nullable=True),
|
||||||
sa.Column("value", sa.String(length=120), nullable=True),
|
sa.Column("value", sa.String(length=120), nullable=True),
|
||||||
sa.ForeignKeyConstraint(["order_item_id"], ["order_item.id"], ),
|
sa.ForeignKeyConstraint(
|
||||||
sa.PrimaryKeyConstraint("id")
|
["order_item_id"],
|
||||||
|
["order_item.id"],
|
||||||
|
),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
)
|
||||||
|
op.add_column(
|
||||||
|
"order_item",
|
||||||
|
sa.Column("hlds_data_version", sa.String(length=40), nullable=True),
|
||||||
|
)
|
||||||
|
op.alter_column(
|
||||||
|
"order", "courrier_id", new_column_name="courier_id", type_=sa.Integer
|
||||||
|
)
|
||||||
|
op.alter_column(
|
||||||
|
"order_item",
|
||||||
|
"extra",
|
||||||
|
new_column_name="comment",
|
||||||
|
existing_type=sa.String(254),
|
||||||
|
type_=sa.Text,
|
||||||
|
)
|
||||||
|
op.alter_column(
|
||||||
|
"order_item", "name", new_column_name="user_name", type_=sa.String(120)
|
||||||
)
|
)
|
||||||
op.add_column("order_item", sa.Column("hlds_data_version", sa.String(length=40), nullable=True))
|
|
||||||
op.alter_column("order", "courrier_id", new_column_name="courier_id", type_=sa.Integer)
|
|
||||||
op.alter_column("order_item", "extra", new_column_name="comment",
|
|
||||||
existing_type=sa.String(254), type_=sa.Text)
|
|
||||||
op.alter_column("order_item", "name", new_column_name="user_name", type_=sa.String(120))
|
|
||||||
|
|
||||||
#----------------------------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------------------------
|
||||||
# Migrate historical product data to order items
|
# Migrate historical product data to order items
|
||||||
|
|
||||||
# First create the new columns we will populate
|
# First create the new columns we will populate
|
||||||
op.add_column("order_item", sa.Column("dish_id", sa.String(length=64), nullable=True))
|
op.add_column(
|
||||||
op.add_column("order_item", sa.Column("dish_name", sa.String(length=120), nullable=True))
|
"order_item", sa.Column("dish_id", sa.String(length=64), nullable=True)
|
||||||
|
)
|
||||||
|
op.add_column(
|
||||||
|
"order_item", sa.Column("dish_name", sa.String(length=120), nullable=True)
|
||||||
|
)
|
||||||
op.add_column("order_item", sa.Column("price", sa.Integer(), nullable=True))
|
op.add_column("order_item", sa.Column("price", sa.Integer(), nullable=True))
|
||||||
# Brief, ad-hoc table constructs just for our UPDATE statement, see
|
# Brief, ad-hoc table constructs just for our UPDATE statement, see
|
||||||
# https://alembic.sqlalchemy.org/en/latest/ops.html#alembic.operations.Operations.execute
|
# https://alembic.sqlalchemy.org/en/latest/ops.html#alembic.operations.Operations.execute
|
||||||
order_item = table("order_item",
|
order_item = table(
|
||||||
|
"order_item",
|
||||||
column("product_id", sa.Integer),
|
column("product_id", sa.Integer),
|
||||||
column("dish_id", sa.String),
|
column("dish_id", sa.String),
|
||||||
column("dish_name", sa.String),
|
column("dish_name", sa.String),
|
||||||
column("price", sa.Integer)
|
column("price", sa.Integer),
|
||||||
)
|
)
|
||||||
# Construct and execute queries
|
# Construct and execute queries
|
||||||
op.execute(text("""
|
op.execute(
|
||||||
|
text(
|
||||||
|
"""
|
||||||
UPDATE order_item
|
UPDATE order_item
|
||||||
SET dish_name = (SELECT product.name FROM product WHERE product.id = order_item.product_id),
|
SET dish_name = (SELECT product.name FROM product WHERE product.id = order_item.product_id),
|
||||||
price = (SELECT product.price FROM product WHERE product.id = order_item.product_id)"""
|
price = (SELECT product.price FROM product WHERE product.id = order_item.product_id)"""
|
||||||
))
|
)
|
||||||
|
)
|
||||||
# Historical product data migrated, drop obsolete column and table
|
# 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_column("order_item", "product_id")
|
||||||
op.drop_table("product")
|
op.drop_table("product")
|
||||||
|
|
||||||
#----------------------------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------------------------
|
||||||
# Migrate historical location data to orders
|
# Migrate historical location data to orders
|
||||||
|
op.alter_column(
|
||||||
op.execute(text("ALTER TABLE `order` DROP FOREIGN KEY order_ibfk_2"))
|
"order",
|
||||||
op.alter_column("order", "location_id", new_column_name="legacy_location_id",
|
"location_id",
|
||||||
type_=sa.Integer, nullable=True)
|
new_column_name="legacy_location_id",
|
||||||
op.add_column("order", sa.Column("location_id", sa.String(length=64), nullable=True))
|
type_=sa.Integer,
|
||||||
op.add_column("order", sa.Column("location_name", sa.String(length=128), nullable=True))
|
nullable=True,
|
||||||
|
)
|
||||||
|
op.add_column(
|
||||||
|
"order", sa.Column("location_id", sa.String(length=64), nullable=True)
|
||||||
|
)
|
||||||
|
op.add_column(
|
||||||
|
"order", sa.Column("location_name", sa.String(length=128), nullable=True)
|
||||||
|
)
|
||||||
# Brief, ad-hoc table constructs just for our UPDATE statement, see
|
# Brief, ad-hoc table constructs just for our UPDATE statement, see
|
||||||
# https://alembic.sqlalchemy.org/en/latest/ops.html#alembic.operations.Operations.execute
|
# https://alembic.sqlalchemy.org/en/latest/ops.html#alembic.operations.Operations.execute
|
||||||
order = table("order",
|
order = table(
|
||||||
|
"order",
|
||||||
column("legacy_location_id", sa.Integer),
|
column("legacy_location_id", sa.Integer),
|
||||||
column("location_id", sa.String),
|
column("location_id", sa.String),
|
||||||
column("location_name", sa.String)
|
column("location_name", sa.String),
|
||||||
)
|
)
|
||||||
# Construct and execute queries
|
# Construct and execute queries
|
||||||
new_location_id = [
|
new_location_id = [
|
||||||
|
@ -114,7 +146,8 @@ def upgrade():
|
||||||
.values(location_id=new_id)
|
.values(location_id=new_id)
|
||||||
for old_id, new_id in LOCATION_LEGACY_TO_HLDS.items()
|
for old_id, new_id in LOCATION_LEGACY_TO_HLDS.items()
|
||||||
]
|
]
|
||||||
location_name_from_location = text("""
|
location_name_from_location = text(
|
||||||
|
"""
|
||||||
UPDATE `order`
|
UPDATE `order`
|
||||||
SET location_name = (SELECT location.name FROM location
|
SET location_name = (SELECT location.name FROM location
|
||||||
WHERE location.id = `order`.legacy_location_id)"""
|
WHERE location.id = `order`.legacy_location_id)"""
|
||||||
|
@ -122,6 +155,7 @@ def upgrade():
|
||||||
for query in chain(new_location_id, [location_name_from_location]):
|
for query in chain(new_location_id, [location_name_from_location]):
|
||||||
op.execute(query)
|
op.execute(query)
|
||||||
# Historical location data migrated, drop obsolete column and table
|
# 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_column("order", "legacy_location_id")
|
||||||
op.drop_table("location")
|
op.drop_table("location")
|
||||||
|
|
||||||
|
|
22
app/migrations/versions/9eac0f3d7b1e_.py
Normal file
22
app/migrations/versions/9eac0f3d7b1e_.py
Normal 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
|
|
@ -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 ###
|
|
@ -1,10 +1,14 @@
|
||||||
"AnonymouseUser for people who are not logged in the normal way"
|
"AnonymouseUser for people who are not logged in the normal way"
|
||||||
|
from typing import List
|
||||||
# pylint: disable=R0201,C0111
|
# pylint: disable=R0201,C0111
|
||||||
|
|
||||||
|
|
||||||
class AnonymouseUser:
|
class AnonymouseUser:
|
||||||
id = None
|
id = None
|
||||||
|
|
||||||
|
def association_list(self) -> List[str]:
|
||||||
|
return []
|
||||||
|
|
||||||
def is_active(self) -> bool:
|
def is_active(self) -> bool:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,26 @@
|
||||||
"Script for everything Order related in the database"
|
"""Script for everything Order related in the database"""
|
||||||
import typing
|
import typing
|
||||||
from datetime import datetime
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
from datetime import datetime
|
||||||
import secrets
|
import secrets
|
||||||
import string
|
import string
|
||||||
|
|
||||||
from utils import first
|
|
||||||
from hlds.definitions import location_definitions
|
from hlds.definitions import location_definitions
|
||||||
|
from utils import first
|
||||||
|
|
||||||
from .database import db
|
from .database import db
|
||||||
from .user import User
|
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 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)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
courier_id = db.Column(db.Integer, nullable=True)
|
courier_id = db.Column(db.Integer, nullable=True)
|
||||||
location_id = db.Column(db.String(64))
|
location_id = db.Column(db.String(64))
|
||||||
|
@ -20,15 +28,11 @@ class Order(db.Model):
|
||||||
starttime = db.Column(db.DateTime)
|
starttime = db.Column(db.DateTime)
|
||||||
stoptime = db.Column(db.DateTime)
|
stoptime = db.Column(db.DateTime)
|
||||||
public = db.Column(db.Boolean, default=True)
|
public = db.Column(db.Boolean, default=True)
|
||||||
slug = db.Column(db.String(7))
|
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")
|
items = db.relationship("OrderItem", backref="order", lazy="dynamic")
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
alphabet = string.ascii_letters + string.digits
|
|
||||||
self.slug = ''.join(secrets.choice(alphabet) for i in range(7))
|
|
||||||
|
|
||||||
def __getattr__(self, name):
|
def __getattr__(self, name):
|
||||||
if name == "location":
|
if name == "location":
|
||||||
return first(
|
return first(
|
||||||
|
@ -39,9 +43,9 @@ class Order(db.Model):
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
# pylint: disable=R1705
|
# pylint: disable=R1705
|
||||||
if self.location:
|
if self.location:
|
||||||
return "Order %d @ %s" % (self.id, self.location.name or "None")
|
return f"Order {self.id} @ {self.location.name or 'None'}"
|
||||||
else:
|
else:
|
||||||
return "Order %d" % (self.id)
|
return f"Order {self.id}"
|
||||||
|
|
||||||
def update_from_hlds(self) -> None:
|
def update_from_hlds(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -54,19 +58,21 @@ class Order(db.Model):
|
||||||
self.location_name = self.location.name
|
self.location_name = self.location.name
|
||||||
|
|
||||||
def for_user(self, anon=None, user=None) -> typing.List:
|
def for_user(self, anon=None, user=None) -> typing.List:
|
||||||
|
"""Get the items for a certain user"""
|
||||||
return list(
|
return list(
|
||||||
filter(
|
filter(
|
||||||
(lambda i: i.user == user)
|
(lambda i: i.user == user)
|
||||||
if user is not None
|
if user is not None
|
||||||
else (lambda i: i.user_name == anon),
|
else (lambda i: i.user_name == anon),
|
||||||
self.items
|
self.items,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def group_by_user(self) -> typing.List[typing.Tuple[str, typing.List]]:
|
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] = dict()
|
group: typing.Dict[str, typing.List] = {}
|
||||||
|
|
||||||
|
# pylint: disable=E1133
|
||||||
for item in self.items:
|
for item in self.items:
|
||||||
if item.for_name not in group:
|
if item.for_name not in group:
|
||||||
group[item.for_name] = []
|
group[item.for_name] = []
|
||||||
|
@ -78,12 +84,17 @@ class Order(db.Model):
|
||||||
|
|
||||||
return list(sorted(group.items(), key=lambda t: (t[0] or "", t[1] or "")))
|
return list(sorted(group.items(), key=lambda t: (t[0] or "", t[1] or "")))
|
||||||
|
|
||||||
def group_by_dish(self) \
|
def group_by_dish(
|
||||||
-> typing.List[typing.Tuple[str, int, typing.List[typing.Tuple[str, typing.List]]]]:
|
self,
|
||||||
"Group items of an Order by dish"
|
) -> typing.List[
|
||||||
group: typing.Dict[str, typing.Dict[str, typing.List]] = \
|
typing.Tuple[str, int, typing.List[typing.Tuple[str, typing.List]]]
|
||||||
defaultdict(lambda: defaultdict(list))
|
]:
|
||||||
|
"""Group items of an Order by dish"""
|
||||||
|
group: typing.Dict[str, typing.Dict[str, typing.List]] = defaultdict(
|
||||||
|
lambda: defaultdict(list)
|
||||||
|
)
|
||||||
|
|
||||||
|
# pylint: disable=E1133
|
||||||
for item in self.items:
|
for item in self.items:
|
||||||
group[item.dish_name][item.comment].append(item)
|
group[item.dish_name][item.comment].append(item)
|
||||||
|
|
||||||
|
@ -95,16 +106,17 @@ class Order(db.Model):
|
||||||
sorted(
|
sorted(
|
||||||
(comment, sorted(items, key=lambda x: (x.for_name or "")))
|
(comment, sorted(items, key=lambda x: (x.for_name or "")))
|
||||||
for comment, items in comment_group.items()
|
for comment, items in comment_group.items()
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
for dish_name, comment_group in group.items()
|
for dish_name, comment_group in group.items()
|
||||||
)
|
)
|
||||||
|
|
||||||
def is_closed(self) -> bool:
|
def is_closed(self) -> bool:
|
||||||
|
"""Return whether the order is closed"""
|
||||||
return self.stoptime and datetime.now() > self.stoptime
|
return self.stoptime and datetime.now() > self.stoptime
|
||||||
|
|
||||||
def can_close(self, user_id: int) -> bool:
|
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():
|
if self.stoptime and self.stoptime < datetime.now():
|
||||||
return False
|
return False
|
||||||
user = None
|
user = None
|
||||||
|
@ -113,3 +125,13 @@ class Order(db.Model):
|
||||||
if self.courier_id == user_id or (user and user.is_admin()):
|
if self.courier_id == user_id or (user and user.is_admin()):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def can_modify_prices(self, user_id: int) -> bool:
|
||||||
|
if not self.is_closed():
|
||||||
|
return False
|
||||||
|
user = User.query.filter_by(id=user_id).first()
|
||||||
|
return user and (user.is_admin() or user == self.courier)
|
||||||
|
|
||||||
|
def can_modify_payment(self, user_id: int) -> bool:
|
||||||
|
user = User.query.filter_by(id=user_id).first()
|
||||||
|
return user and (user.is_admin() or user == self.courier)
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
"Script for everything OrderItem related in the database"
|
"Script for everything OrderItem related in the database"
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from utils import first
|
|
||||||
from hlds.definitions import location_definitions
|
from hlds.definitions import location_definitions
|
||||||
|
from utils import first
|
||||||
|
|
||||||
from .database import db
|
from .database import db
|
||||||
from .order import Order
|
from .order import Order
|
||||||
from .user import User
|
from .user import User
|
||||||
|
|
||||||
|
|
||||||
class OrderItem(db.Model):
|
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)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
order_id = db.Column(db.Integer, db.ForeignKey("order.id"), nullable=False)
|
order_id = db.Column(db.Integer, db.ForeignKey("order.id"), nullable=False)
|
||||||
user_id = db.Column(db.Integer, db.ForeignKey("user.id"))
|
user_id = db.Column(db.Integer, db.ForeignKey("user.id"))
|
||||||
|
@ -17,24 +18,25 @@ class OrderItem(db.Model):
|
||||||
dish_id = db.Column(db.String(64), nullable=True)
|
dish_id = db.Column(db.String(64), nullable=True)
|
||||||
dish_name = db.Column(db.String(120), nullable=True)
|
dish_name = db.Column(db.String(120), nullable=True)
|
||||||
price = db.Column(db.Integer, nullable=True)
|
price = db.Column(db.Integer, nullable=True)
|
||||||
|
price_modified = db.Column(db.DateTime, nullable=True)
|
||||||
paid = db.Column(db.Boolean, default=False, nullable=True)
|
paid = db.Column(db.Boolean, default=False, nullable=True)
|
||||||
comment = db.Column(db.Text(), nullable=True)
|
comment = db.Column(db.Text(), nullable=True)
|
||||||
hlds_data_version = db.Column(db.String(40), nullable=True)
|
hlds_data_version = db.Column(db.String(40), nullable=True)
|
||||||
|
|
||||||
choices = db.relationship("OrderItemChoice", backref="order_item", lazy="dynamic")
|
choices = db.relationship("OrderItemChoice",
|
||||||
|
backref="order_item",
|
||||||
|
lazy="dynamic")
|
||||||
|
|
||||||
def __getattr__(self, name):
|
def __getattr__(self, name):
|
||||||
if name == "dish":
|
if name == "dish":
|
||||||
location_id = (
|
location_id = (Order.query.filter(
|
||||||
Order.query.filter(Order.id == self.order_id).first().location_id
|
Order.id == self.order_id).first().location_id)
|
||||||
)
|
|
||||||
location = first(
|
location = first(
|
||||||
filter(lambda l: l.id == location_id, location_definitions)
|
filter(lambda l: l.id == location_id, location_definitions))
|
||||||
)
|
|
||||||
if location:
|
if location:
|
||||||
return first(filter(lambda d: d.id == self.dish_id, location.dishes))
|
return first(
|
||||||
else:
|
filter(lambda d: d.id == self.dish_id, location.dishes))
|
||||||
raise ValueError("No Location found with id: " + location_id)
|
raise ValueError(f"No Location found with id: {location_id}")
|
||||||
raise AttributeError()
|
raise AttributeError()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -45,11 +47,7 @@ class OrderItem(db.Model):
|
||||||
return self.user_name
|
return self.user_name
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return "Order %d: %s wants %s" % (
|
return "Order {self.order_id or 0}: {self.for_name} wants {self.dish_name or 'None'}"
|
||||||
self.order_id or 0,
|
|
||||||
self.for_name,
|
|
||||||
self.dish_name or "None",
|
|
||||||
)
|
|
||||||
|
|
||||||
def update_from_hlds(self) -> None:
|
def update_from_hlds(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -63,7 +61,7 @@ class OrderItem(db.Model):
|
||||||
|
|
||||||
# pylint: disable=W0613
|
# pylint: disable=W0613
|
||||||
def can_delete(self, order_id: int, user_id: int, name: str) -> bool:
|
def can_delete(self, order_id: int, user_id: int, name: str) -> bool:
|
||||||
"Check if a user can delete an item"
|
"""Check if a user can delete an item"""
|
||||||
if int(self.order_id) != int(order_id):
|
if int(self.order_id) != int(order_id):
|
||||||
return False
|
return False
|
||||||
if self.order.is_closed():
|
if self.order.is_closed():
|
||||||
|
@ -76,3 +74,12 @@ class OrderItem(db.Model):
|
||||||
if user and (user.is_admin() or user == self.order.courier):
|
if user and (user.is_admin() or user == self.order.courier):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# pylint: disable=W0613
|
||||||
|
def can_modify_payment(self, order_id: int, user_id: int) -> bool:
|
||||||
|
if int(self.order_id) != int(order_id):
|
||||||
|
return False
|
||||||
|
user = User.query.filter(User.id == user_id).first()
|
||||||
|
if user and (user.is_admin() or user == self.order.courier):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
"Script for everything OrderItemChoice related in the database"
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from .database import db
|
from .database import db
|
||||||
|
@ -5,6 +6,7 @@ from .orderitem import OrderItem
|
||||||
|
|
||||||
|
|
||||||
class OrderItemChoice(db.Model):
|
class OrderItemChoice(db.Model):
|
||||||
|
"Class used for configuring the OrderItemChoice model in the database"
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
choice_id = db.Column(db.String(64), nullable=True)
|
choice_id = db.Column(db.String(64), nullable=True)
|
||||||
order_item_id = db.Column(
|
order_item_id = db.Column(
|
||||||
|
@ -16,7 +18,8 @@ class OrderItemChoice(db.Model):
|
||||||
|
|
||||||
# pylint: disable=attribute-defined-outside-init
|
# pylint: disable=attribute-defined-outside-init
|
||||||
def configure(self, order: OrderItem) -> None:
|
def configure(self, order: OrderItem) -> None:
|
||||||
|
"Set the orderitem"
|
||||||
self.order = order
|
self.order = order
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return "{}: {}".format(self.name, self.value)
|
return f"{self.name}: {self.value}"
|
||||||
|
|
|
@ -1,13 +1,21 @@
|
||||||
"Script for everything User related in the database"
|
"Script for everything User related in the database"
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
from models import db
|
from models import db
|
||||||
|
|
||||||
|
|
||||||
class User(db.Model):
|
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)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
username = db.Column(db.String(80), unique=True, nullable=False)
|
username = db.Column(db.String(80), unique=True, nullable=False)
|
||||||
admin = db.Column(db.Boolean)
|
admin = db.Column(db.Boolean)
|
||||||
bias = db.Column(db.Integer)
|
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(
|
runs = db.relation(
|
||||||
"Order",
|
"Order",
|
||||||
backref="courier",
|
backref="courier",
|
||||||
|
@ -16,11 +24,18 @@ class User(db.Model):
|
||||||
)
|
)
|
||||||
orderItems = db.relationship("OrderItem", backref="user", lazy="dynamic")
|
orderItems = db.relationship("OrderItem", backref="user", lazy="dynamic")
|
||||||
|
|
||||||
def configure(self, username: str, admin: bool, bias: int) -> None:
|
def association_list(self) -> List[str]:
|
||||||
"Configure the User"
|
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.username = username
|
||||||
self.admin = admin
|
self.admin = admin
|
||||||
self.bias = bias
|
self.bias = bias
|
||||||
|
self.microsoft_uuid = microsoft_uuid
|
||||||
|
self.associations = ",".join(associations)
|
||||||
|
|
||||||
# pylint: disable=C0111, R0201
|
# pylint: disable=C0111, R0201
|
||||||
def is_authenticated(self) -> bool:
|
def is_authenticated(self) -> bool:
|
||||||
|
@ -39,4 +54,4 @@ class User(db.Model):
|
||||||
return str(self.id)
|
return str(self.id)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return "%s" % self.username
|
return f"{self.username}"
|
||||||
|
|
|
@ -11,28 +11,29 @@ from models.order import Order
|
||||||
|
|
||||||
|
|
||||||
def webhook_text(order: Order) -> typing.Optional[str]:
|
def webhook_text(order: Order) -> typing.Optional[str]:
|
||||||
"Function that makes the text for the notification"
|
"""Function that makes the text for the notification"""
|
||||||
if order.location_id == "test":
|
if order.location_id == "test":
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if order.courier is not None:
|
if order.courier is not None:
|
||||||
# pylint: disable=C0301
|
# pylint: disable=C0301, C0209
|
||||||
return "<!channel|@channel> {3} is going to {1}, order <{0}|here>! Deadline in {2} minutes!".format(
|
return "<!channel|@channel> {3} is going to {1}, order <{0}|here>! Deadline in {2} minutes!".format(
|
||||||
url_for("order_bp.order_from_id", order_id=order.id, _external=True),
|
url_for("order_bp.order_from_slug", order_slug=order.slug, _external=True),
|
||||||
order.location_name,
|
order.location_name,
|
||||||
remaining_minutes(order.stoptime),
|
remaining_minutes(order.stoptime),
|
||||||
order.courier.username.title(),
|
order.courier.username,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# pylint: disable=C0209
|
||||||
return "<!channel|@channel> New order for {}. Deadline in {} minutes. <{}|Open here.>".format(
|
return "<!channel|@channel> New order for {}. Deadline in {} minutes. <{}|Open here.>".format(
|
||||||
order.location_name,
|
order.location_name,
|
||||||
remaining_minutes(order.stoptime),
|
remaining_minutes(order.stoptime),
|
||||||
url_for("order_bp.order_from_id", order_id=order.id, _external=True),
|
url_for("order_bp.order_from_slug", order_slug=order.slug, _external=True),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def post_order_to_webhook(order: Order) -> None:
|
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)
|
message = webhook_text(order)
|
||||||
if message:
|
if message:
|
||||||
webhookthread = WebhookSenderThread(message, app.config["SLACK_WEBHOOK"])
|
webhookthread = WebhookSenderThread(message, app.config["SLACK_WEBHOOK"])
|
||||||
|
@ -40,10 +41,10 @@ def post_order_to_webhook(order: Order) -> None:
|
||||||
|
|
||||||
|
|
||||||
class WebhookSenderThread(Thread):
|
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:
|
def __init__(self, message: str, url: str) -> None:
|
||||||
super(WebhookSenderThread, self).__init__()
|
super().__init__()
|
||||||
self.message = message
|
self.message = message
|
||||||
self.url = url
|
self.url = url
|
||||||
|
|
||||||
|
@ -51,7 +52,7 @@ class WebhookSenderThread(Thread):
|
||||||
self.slack_webhook()
|
self.slack_webhook()
|
||||||
|
|
||||||
def slack_webhook(self) -> None:
|
def slack_webhook(self) -> None:
|
||||||
"The webhook for the specified chat platform"
|
"""The webhook for the specified chat platform"""
|
||||||
if self.url:
|
if self.url:
|
||||||
requests.post(self.url, json={"text": self.message})
|
requests.post(self.url, json={"text": self.message})
|
||||||
else:
|
else:
|
||||||
|
@ -59,9 +60,9 @@ class WebhookSenderThread(Thread):
|
||||||
|
|
||||||
|
|
||||||
def remaining_minutes(value) -> str:
|
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()
|
delta = value - datetime.now()
|
||||||
if delta.total_seconds() < 0:
|
if delta.total_seconds() < 0:
|
||||||
return "0"
|
return "0"
|
||||||
minutes = delta.total_seconds() // 60
|
minutes = int(delta.total_seconds() // 60)
|
||||||
return "%02d" % minutes
|
return f"{minutes:02}"
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
"Module used for parsing the HLDS files"
|
||||||
|
|
||||||
from tatsu.util import asjson
|
|
||||||
from hlds.parser import parse_files
|
from hlds.parser import parse_files
|
||||||
|
|
||||||
|
|
||||||
USAGE = """{0} [filename]...
|
USAGE = """{0} [filename]...
|
||||||
Parse HLDS files, print as JSON
|
Parse HLDS files, print as JSON
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,9 @@ sys.path.append(os.getcwd())
|
||||||
|
|
||||||
# Phusion Passenger expects this file to be called `passenger_wsgi.py`
|
# Phusion Passenger expects this file to be called `passenger_wsgi.py`
|
||||||
# and the WSGI object to be called `application`
|
# and the WSGI object to be called `application`
|
||||||
from app import app as application
|
from app import create_app
|
||||||
|
|
||||||
|
application, appmgr = create_app()
|
||||||
|
|
||||||
# For running on the server with passenger etc
|
# For running on the server with passenger etc
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
@ -243,9 +243,9 @@ details summary {
|
||||||
}
|
}
|
||||||
details summary:before {
|
details summary:before {
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
content: "⯈";
|
content: "▸";
|
||||||
padding-right: 0.4em;
|
padding-right: 0.4em;
|
||||||
}
|
}
|
||||||
details[open] summary:before {
|
details[open] summary:before {
|
||||||
content: "⯆";
|
content: "▾";
|
||||||
}
|
}
|
||||||
|
|
2
app/static/js/jquery.min.js
vendored
Normal file
2
app/static/js/jquery.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
app/static/js/qrcode.min.js
vendored
Normal file
1
app/static/js/qrcode.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -63,7 +63,8 @@
|
||||||
<nav class="navbar navbar-default navbar-fixed-top">
|
<nav class="navbar navbar-default navbar-fixed-top">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="navbar-header">
|
<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="sr-only">Toggle navigation</span>
|
||||||
<span class="icon-bar"></span>
|
<span class="icon-bar"></span>
|
||||||
<span class="icon-bar"></span>
|
<span class="icon-bar"></span>
|
||||||
|
@ -81,7 +82,10 @@
|
||||||
</ul>
|
</ul>
|
||||||
<ul class="nav navbar-nav navbar-right">
|
<ul class="nav navbar-nav navbar-right">
|
||||||
{% if current_user.is_anonymous() %}
|
{% 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 %}
|
{% else %}
|
||||||
<li><a href="{{ url_for('general_bp.profile') }}">{{ current_user.username }}</a></li>
|
<li><a href="{{ url_for('general_bp.profile') }}">{{ current_user.username }}</a></li>
|
||||||
<li><a href="{{ url_for('auth_bp.logout') }}">Logout</a></li>
|
<li><a href="{{ url_for('auth_bp.logout') }}">Logout</a></li>
|
||||||
|
|
|
@ -12,10 +12,27 @@
|
||||||
{% block metas %}
|
{% block metas %}
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
<meta name="robots" content="noindex, nofollow">
|
<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 %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block container %}
|
{% block container %}
|
||||||
<header>
|
<header class="row">
|
||||||
|
<div class="col-md-2" style="padding-top: 2em">
|
||||||
|
<div id="qrcode"></div>
|
||||||
|
<script type="text/javascript">
|
||||||
|
var qrcode = new QRCode(document.getElementById("qrcode"), {
|
||||||
|
text: "{{ url_for("order_bp.order_from_slug", order_slug=order.slug, _external=True) }}",
|
||||||
|
width: 128,
|
||||||
|
height: 128,
|
||||||
|
colorDark : "#000000",
|
||||||
|
colorLight : "#ffffff",
|
||||||
|
correctLevel : QRCode.CorrectLevel.H
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-10">
|
||||||
<h2 id="order-title">Order {{ order.id }}</h2>
|
<h2 id="order-title">Order {{ order.id }}</h2>
|
||||||
|
|
||||||
<div class="location">
|
<div class="location">
|
||||||
|
@ -25,6 +42,10 @@
|
||||||
{{ order.location_name }}
|
{{ order.location_name }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
Unique order link: <code>{{ url_for("order_bp.order_from_slug", order_slug=order.slug, _external=True) }}</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
|
@ -36,7 +57,7 @@
|
||||||
{% for item in my_items %}
|
{% for item in my_items %}
|
||||||
<li class="spacecake">
|
<li class="spacecake">
|
||||||
{% if item.can_delete(order.id, current_user.id, session.get('anon_name', '')) -%}
|
{% if item.can_delete(order.id, current_user.id, session.get('anon_name', '')) -%}
|
||||||
<form action="{{ url_for('order_bp.delete_item', order_id=order.id, item_id=item.id) }}" method="post" style="display:inline">
|
<form action="{{ url_for('order_bp.delete_item', order_slug=order.slug, item_id=item.id) }}" method="post" style="display:inline">
|
||||||
<button class="btn btn-link btn-sm" type="submit" style="padding: 0 0.5em;"><span class="glyphicon glyphicon-remove"></span></button>
|
<button class="btn btn-link btn-sm" type="submit" style="padding: 0 0.5em;"><span class="glyphicon glyphicon-remove"></span></button>
|
||||||
</form>
|
</form>
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
@ -65,7 +86,7 @@
|
||||||
<h3>Add item to order</h3>
|
<h3>Add item to order</h3>
|
||||||
|
|
||||||
{% for dish in order.location.dishes %}
|
{% for dish in order.location.dishes %}
|
||||||
<form method="post" action="{{ url_for('order_bp.order_item_create', order_id=order.id) }}" id="dish_{{ dish.id }}">
|
<form method="post" action="{{ url_for('order_bp.order_item_create', order_slug=order.slug) }}" id="dish_{{ dish.id }}">
|
||||||
{{ form.csrf_token }}
|
{{ form.csrf_token }}
|
||||||
<input type="hidden" name="dish_id" value="{{ dish.id }}" />
|
<input type="hidden" name="dish_id" value="{{ dish.id }}" />
|
||||||
|
|
||||||
|
@ -134,7 +155,8 @@
|
||||||
|
|
||||||
<div class="box" id="order_info">
|
<div class="box" id="order_info">
|
||||||
<h3>Order information</h3>
|
<h3>Order information</h3>
|
||||||
<dl>
|
<div class="row">
|
||||||
|
<dl class="col-md-10 col-lg-8">
|
||||||
<div>
|
<div>
|
||||||
<dt>Order opens</dt>
|
<dt>Order opens</dt>
|
||||||
<dd>{{ order.starttime.strftime("%Y-%m-%d, %H:%M") }}</dd>
|
<dd>{{ order.starttime.strftime("%Y-%m-%d, %H:%M") }}</dd>
|
||||||
|
@ -167,7 +189,7 @@
|
||||||
<dd>
|
<dd>
|
||||||
{% if order.courier == None %}
|
{% if order.courier == None %}
|
||||||
{% if not current_user.is_anonymous() %}
|
{% if not current_user.is_anonymous() %}
|
||||||
<form action="{{ url_for('order_bp.volunteer', order_id=order.id) }}" method="post" style="display:inline">
|
<form action="{{ url_for('order_bp.volunteer', order_slug=order.slug) }}" method="post" style="display:inline">
|
||||||
<input type="submit" class="btn btn-primary btn-sm" value="Volunteer"></input>
|
<input type="submit" class="btn btn-primary btn-sm" value="Volunteer"></input>
|
||||||
</form>
|
</form>
|
||||||
{% else %}No-one yet{% endif %}
|
{% else %}No-one yet{% endif %}
|
||||||
|
@ -176,18 +198,23 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
<div>
|
<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>
|
||||||
|
|
||||||
{% if order.can_close(current_user.id) -%}
|
{% if order.can_close(current_user.id) -%}
|
||||||
<form action="{{ url_for('order_bp.close_order', order_id=order.id) }}" method="post" style="display:inline">
|
<form action="{{ url_for('order_bp.close_order', order_slug=order.slug) }}" method="post" style="display:inline">
|
||||||
<input type="submit" class="btn btn-danger" value="Close"></input>
|
<input type="submit" class="btn btn-danger" value="Close"></input>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if courier_or_admin %}
|
{% if courier_or_admin %}
|
||||||
<a class="btn" href="{{ url_for('order_bp.order_edit', order_id=order.id) }}">Edit</a>
|
<a class="btn" href="{{ url_for('order_bp.order_edit', order_slug=order.slug) }}">Edit</a>
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="box" id="how_to_order">
|
<div class="box" id="how_to_order">
|
||||||
|
@ -258,7 +285,7 @@
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
Total {{ order.items.count() }} items — {{ total_price|euro }}
|
Total {{ order.items.count() }} items — {{ total_price|euro }}
|
||||||
|
|
||||||
<a class="btn btn-sm" href="{{ url_for('order_bp.items_shop_view', order_id=order.id) }}">Shop view</a>
|
<a class="btn btn-sm" href="{{ url_for('order_bp.items_shop_view', order_slug=order.slug) }}">Shop view</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -267,6 +294,7 @@
|
||||||
<section class="single_column">
|
<section class="single_column">
|
||||||
<div class="box" id="per_person">
|
<div class="box" id="per_person">
|
||||||
<h3>Items per person</h3>
|
<h3>Items per person</h3>
|
||||||
|
<form action="{{ url_for('order_bp.modify_items', order_slug=order.slug) }}" method="post">
|
||||||
<table class="table table-condensed">
|
<table class="table table-condensed">
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th>Total</th><th>Name</th><th>Items</th></tr>
|
<tr><th>Total</th><th>Name</th><th>Items</th></tr>
|
||||||
|
@ -276,35 +304,37 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
{% set paid = order_items | map(attribute="paid") | all %}
|
{% set paid = order_items | map(attribute="paid") | all %}
|
||||||
<input type="checkbox" name="{{ user_name }}"
|
<input type="checkbox" name="user_names" value="{{ user_name }}"
|
||||||
{{ "disabled" if paid }} style="{{ 'opacity: 0.5' if paid }}">
|
{{ "disabled" if not order.can_modify_payment(current_user.id) }}>
|
||||||
|
|
||||||
<span class="price">{{ order_items | map(attribute="price") | sum | euro }}</span>
|
<span class="price" style="{{ 'opacity: 0.5' if paid }}">
|
||||||
|
{{ order_items | map(attribute="price") | ignore_none | sum | euro }}
|
||||||
|
</span>
|
||||||
|
|
||||||
{% if paid %}paid{% endif %}
|
{% if paid %}<span class="glyphicon glyphicon-ok" style="opacity: 0.5"></span>{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ user_name }}</td>
|
<td style="{{ 'opacity: 0.5' if paid }}">{{ user_name }}</td>
|
||||||
<td class="items">
|
<td class="items">
|
||||||
<ul>
|
<ul>
|
||||||
{% for item in order_items %}
|
{% for item in order_items %}
|
||||||
<li class="{{ 'paid' if item.paid }}">
|
<li class="{{ 'paid' if item.paid }}">
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
{% if item.can_delete(order.id, current_user.id, session.get('anon_name', '')) -%}
|
{% if item.can_delete(order.id, current_user.id, session.get('anon_name', '')) -%}
|
||||||
<form action="{{ url_for('order_bp.delete_item', order_id=order.id, item_id=item.id) }}" method="post" style="display:inline">
|
<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>
|
||||||
<button class="btn btn-link btn-sm" type="submit" style="padding: 0 0.5em;"><span class="glyphicon glyphicon-remove"></span></button>
|
|
||||||
</form>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="glyphicon glyphicon-remove" style="color: var(--gray3); padding: 0 0.5em"></span>
|
<span class="glyphicon glyphicon-remove" style="color: var(--gray3); padding: 0 0.5em; cursor: not-allowed"></span>
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="price_aligned">{{ item.price|euro }}</div>
|
<div class="price_aligned">
|
||||||
|
{{ item.price|euro }}
|
||||||
|
{% if item.price_modified %}
|
||||||
|
<span class="glyphicon glyphicon-pencil" style="opacity: 0.5" title="Edited"></span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
<div class="item_description">{{ item.dish_name }}{{ "; " + item.comment if item.comment }}</div>
|
<div class="item_description">{{ item.dish_name }}{{ "; " + item.comment if item.comment }}</div>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<li>
|
|
||||||
<button class="btn btn-link btn-sm" onclick="alert('TODO')" style="color: green; padding: 0 0.5em;"><span class="glyphicon glyphicon-plus"></span></button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
@ -314,11 +344,21 @@
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
|
{% if order.can_modify_payment(current_user.id) %}
|
||||||
On selected:
|
On selected:
|
||||||
<button class="btn btn-sm"><span class="glyphicon glyphicon-ok"></span> Mark paid (TODO)</button>
|
<button name="action" value="mark_paid" class="btn btn-sm"><span class="glyphicon glyphicon-ok"></span> Mark paid</button>
|
||||||
<button class="btn btn-sm"><span class="glyphicon glyphicon-piggy-bank"></span> Tab (TODO)</button>
|
<button name="action" value="mark_unpaid" class="btn btn-sm">Mark unpaid</button>
|
||||||
<button class="btn btn-sm"><span class="glyphicon glyphicon-qrcode"></span> QR code (TODO)</button>
|
{% endif %}
|
||||||
|
|
||||||
|
{% if order.can_modify_prices(current_user.id) %}
|
||||||
|
<span style="border-left: 1px solid var(--gray0); display: inline-block;"> </span>
|
||||||
|
<a href="{{ url_for('order_bp.prices', order_slug=order.slug) }}" class="btn btn-sm">
|
||||||
|
<span class="glyphicon glyphicon-pencil"></span> Edit prices
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
<h3>Edit order</h3>
|
<h3>Edit order</h3>
|
||||||
<div class="row darker">
|
<div class="row darker">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<form method="post" action="{{ url_for('.order_edit', order_id=order_id) }}">
|
<form method="post" action="{{ url_for('.order_edit', order_slug=order_slug) }}">
|
||||||
{{ form.csrf_token }}
|
{{ form.csrf_token }}
|
||||||
<div class="form-group select2 {{ 'has-errors' if form.courier_id.errors else ''}}">
|
<div class="form-group select2 {{ 'has-errors' if form.courier_id.errors else ''}}">
|
||||||
{{ form.courier_id.label(class='control-label') }}<br>
|
{{ form.courier_id.label(class='control-label') }}<br>
|
||||||
|
|
132
app/templates/order_prices.html
Normal file
132
app/templates/order_prices.html
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
{% extends "layout.html" %}
|
||||||
|
{% set active_page = "orders" -%}
|
||||||
|
|
||||||
|
{% import "utils.html" as util %}
|
||||||
|
|
||||||
|
{% block metas %}
|
||||||
|
{{ super() }}
|
||||||
|
<meta name="robots" content="noindex, nofollow">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% 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>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<form action="{{ url_for('order_bp.prices', order_slug=order.slug) }}" method="post">
|
||||||
|
<div class="col-md-6" id="per_dish">
|
||||||
|
<h3>Per dish</h3>
|
||||||
|
<div class="noscript">This functionality requires JavaScript.</div>
|
||||||
|
<div class="script">
|
||||||
|
|
||||||
|
<table class="table table-condensed">
|
||||||
|
<thead>
|
||||||
|
<tr><th colspan="2">Dish</th><th>Price</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for dish_name, dish_quantity, dish_comment_groups in order.group_by_dish() -%}
|
||||||
|
{% set has_comments = dish_comment_groups | length > 1 or (dish_comment_groups | map("first") | any) -%}
|
||||||
|
{% for comment, items in dish_comment_groups -%}
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
{% if loop.first %}
|
||||||
|
<td rowspan="{{dish_comment_groups | length }}">
|
||||||
|
<span class="quantity">{{ dish_quantity }}</span> ×
|
||||||
|
{{ dish_name }}
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<span class="quantity">{{ items | length }}</span> ×
|
||||||
|
{% if comment %}{{ comment }}
|
||||||
|
{% else %}<i>No comment</i>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
{% set price = items[0].price | euro("") %}
|
||||||
|
{% set item_ids = items | map(attribute="id") %}
|
||||||
|
€ <input type="text" data-for-items="{{ item_ids | join(",") }}" value="{{ price }}">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
{% endfor %}
|
||||||
|
{%- endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6" id="per_person">
|
||||||
|
<h3>Per person</h3>
|
||||||
|
<table class="table table-condensed">
|
||||||
|
<thead>
|
||||||
|
<tr><th>Name</th><th>Items</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for user_name, order_items in order.group_by_user() -%}
|
||||||
|
<tr>
|
||||||
|
<td>{{ user_name }}</td>
|
||||||
|
<td class="items">
|
||||||
|
<ul>
|
||||||
|
{% for item in order_items %}
|
||||||
|
<li class="{{ 'paid' if item.paid }}">
|
||||||
|
€ <input type="text" value="{{ item.price|euro("") }}" name="item_{{ item.id }}" id="item_{{ item.id }}">
|
||||||
|
<span class="item_description">{{ item.dish_name }}{{ "; " + item.comment if item.comment }}</span>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
{%- endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<a href="{{ url_for('order_bp.order_from_slug', order_slug=order.slug) }}" class="btn btn-sm">Cancel</a>
|
||||||
|
<button class="btn btn-sm btn-primary">Apply</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block styles %}
|
||||||
|
{{ super() }}
|
||||||
|
<style>
|
||||||
|
.script {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#per_dish ul, #per_person ul {
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#per_dish input, #per_person input {
|
||||||
|
width: 3em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
{{ super() }}
|
||||||
|
<script type="text/javascript">
|
||||||
|
"use strict";
|
||||||
|
$(window).on("load", () => {
|
||||||
|
$(".noscript").css("display", "none");
|
||||||
|
$(".script").css("display", "unset");
|
||||||
|
|
||||||
|
function updatePerPersonPrices(e) {
|
||||||
|
console.log(e.target);
|
||||||
|
for (let item_id of e.target.dataset.forItems.split(",")) {
|
||||||
|
$("#item_" + item_id).val(e.target.value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
$("#per_dish input").on("change", updatePerPersonPrices);
|
||||||
|
$("#per_dish input").on("keyup", updatePerPersonPrices);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
|
@ -38,6 +38,11 @@
|
||||||
{{ form.location_id(class='form-control select') }}
|
{{ form.location_id(class='form-control select') }}
|
||||||
{{ util.render_form_field_errors(form.location_id) }}
|
{{ util.render_form_field_errors(form.location_id) }}
|
||||||
</div>
|
</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() %}
|
{% 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() }}">
|
<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') }}
|
{{ form.starttime.label(class='control-label') }}
|
||||||
|
|
|
@ -1,15 +1,18 @@
|
||||||
{% macro render_order(order) -%}
|
{% macro render_order(order) -%}
|
||||||
<div class="row order_row">
|
<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>
|
<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">
|
<p class="time_data">
|
||||||
{% if order.stoptime %}
|
{% if order.stoptime %}
|
||||||
<span><b>Closes </b>{{ order.stoptime.strftime("%H:%M") }}</span>{{ order.stoptime|countdown }}
|
<span><b>Closes </b>{{ order.stoptime.strftime("%H:%M") }}</span>{{ order.stoptime|countdown }}
|
||||||
{% else %}open{% endif %}<br/>
|
{% else %}open{% endif %}<br/>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 col-lg-3 expand_button_wrapper">
|
<div class="col-md-3">
|
||||||
<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>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
|
|
22
app/utils.py
22
app/utils.py
|
@ -1,20 +1,31 @@
|
||||||
"Script which contains several utils for Haldis"
|
"Script which contains several utils for Haldis"
|
||||||
|
|
||||||
from typing import Iterable
|
import re
|
||||||
|
from typing import Iterable, Optional
|
||||||
|
|
||||||
|
|
||||||
def euro_string(value: int) -> str:
|
def euro_string(value: Optional[int], unit="€ ") -> str:
|
||||||
"""
|
"""
|
||||||
Convert cents to string formatted euro
|
Convert cents to string formatted euro
|
||||||
"""
|
"""
|
||||||
|
if value is None:
|
||||||
|
return "✗"
|
||||||
euro, cents = divmod(value, 100)
|
euro, cents = divmod(value, 100)
|
||||||
if cents:
|
if cents:
|
||||||
return "€ {}.{:02}".format(euro, cents)
|
return f"{unit}{euro}.{cents:02}"
|
||||||
else:
|
return f"{unit}{euro}"
|
||||||
return "€ {}".format(euro)
|
|
||||||
|
|
||||||
|
def parse_euro_string(value: str) -> Optional[int]:
|
||||||
|
m = re.fullmatch("(?:€ ?)?([0-9]+)(?:[.,]([0-9]+))?", value)
|
||||||
|
if not m:
|
||||||
|
return None
|
||||||
|
cents_02 = "{:0<2.2}".format(m.group(2)) if m.group(2) else "00"
|
||||||
|
return int(m.group(1)) * 100 + int(cents_02)
|
||||||
|
|
||||||
|
|
||||||
def price_range_string(price_range, include_upper=False):
|
def price_range_string(price_range, include_upper=False):
|
||||||
|
"Convert a price range to a string formatted euro"
|
||||||
if price_range[0] == price_range[1]:
|
if price_range[0] == price_range[1]:
|
||||||
return euro_string(price_range[0])
|
return euro_string(price_range[0])
|
||||||
return ("{}—{}" if include_upper else "from {}").format(
|
return ("{}—{}" if include_upper else "from {}").format(
|
||||||
|
@ -33,4 +44,5 @@ def first(iterable: Iterable, default=None):
|
||||||
|
|
||||||
|
|
||||||
def ignore_none(iterable: Iterable):
|
def ignore_none(iterable: Iterable):
|
||||||
|
"Filter to ignore None objects"
|
||||||
return filter(lambda x: x is not None, iterable)
|
return filter(lambda x: x is not None, iterable)
|
||||||
|
|
|
@ -17,12 +17,12 @@ def list_routes() -> str:
|
||||||
for rule in app.url_map.iter_rules():
|
for rule in app.url_map.iter_rules():
|
||||||
options = {}
|
options = {}
|
||||||
for arg in rule.arguments:
|
for arg in rule.arguments:
|
||||||
options[arg] = "[{0}]".format(arg)
|
options[arg] = f"[{arg}]"
|
||||||
print(rule.endpoint)
|
print(rule.endpoint)
|
||||||
methods = ",".join(rule.methods)
|
methods = ",".join(rule.methods)
|
||||||
url = url_for(rule.endpoint, **options)
|
url = url_for(rule.endpoint, **options)
|
||||||
line = urllib.parse.unquote(
|
line = urllib.parse.unquote(
|
||||||
"{:50s} {:20s} {}".format(rule.endpoint, methods, url)
|
f"{rule.endpoint:50s} {methods:20s} {url}"
|
||||||
)
|
)
|
||||||
output.append(line)
|
output.append(line)
|
||||||
|
|
||||||
|
|
|
@ -1,32 +1,26 @@
|
||||||
"Script to generate the general views of Haldis"
|
"Script to generate the general views of Haldis"
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
import yaml
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from flask import Flask, render_template, make_response
|
import yaml
|
||||||
from flask import request, jsonify
|
from flask import Blueprint, Flask, abort
|
||||||
from flask import Blueprint, abort
|
|
||||||
from flask import current_app as app
|
from flask import current_app as app
|
||||||
from flask import send_from_directory, url_for
|
from flask import (jsonify, make_response, render_template, request,
|
||||||
from flask_login import login_required
|
send_from_directory, url_for)
|
||||||
|
from flask_login import current_user, login_required
|
||||||
from utils import first
|
|
||||||
from hlds.definitions import location_definitions
|
from hlds.definitions import location_definitions
|
||||||
from hlds.models import Location
|
from hlds.models import Location
|
||||||
from models import Order
|
from models import Order
|
||||||
|
from utils import first
|
||||||
# import views
|
# import views
|
||||||
from views.order import get_orders
|
from views.order import get_orders
|
||||||
|
|
||||||
import json
|
|
||||||
from flask import jsonify
|
|
||||||
|
|
||||||
general_bp = Blueprint("general_bp", __name__)
|
general_bp = Blueprint("general_bp", __name__)
|
||||||
|
|
||||||
|
|
||||||
with open(os.path.join(os.path.dirname(__file__), "themes.yml"), "r") as _stream:
|
with open(os.path.join(os.path.dirname(__file__), "themes.yml")) as _stream:
|
||||||
_theme_data = yaml.safe_load(_stream)
|
_theme_data = yaml.safe_load(_stream)
|
||||||
THEME_OPTIONS = _theme_data["options"]
|
THEME_OPTIONS = _theme_data["options"]
|
||||||
THEMES = _theme_data["themes"]
|
THEMES = _theme_data["themes"]
|
||||||
|
@ -37,10 +31,12 @@ def home() -> str:
|
||||||
"Generate the home view"
|
"Generate the home view"
|
||||||
prev_day = datetime.now() - timedelta(days=1)
|
prev_day = datetime.now() - timedelta(days=1)
|
||||||
recently_closed = get_orders(
|
recently_closed = get_orders(
|
||||||
((Order.stoptime > prev_day) & (Order.stoptime < datetime.now()))
|
(Order.stoptime > prev_day) & (Order.stoptime < datetime.now())
|
||||||
)
|
)
|
||||||
return render_template(
|
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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -60,7 +56,7 @@ def is_theme_active(theme, now):
|
||||||
|
|
||||||
return start_datetime <= now <= end_datetime
|
return start_datetime <= now <= end_datetime
|
||||||
|
|
||||||
raise Exception("Unknown theme type {}".format(theme_type))
|
raise Exception(f"Unknown theme type {theme_type}")
|
||||||
|
|
||||||
|
|
||||||
def get_theme_css(theme, options):
|
def get_theme_css(theme, options):
|
||||||
|
@ -71,13 +67,18 @@ def get_theme_css(theme, options):
|
||||||
|
|
||||||
for option in theme.get("options", []):
|
for option in theme.get("options", []):
|
||||||
theme_name = theme["name"]
|
theme_name = theme["name"]
|
||||||
assert option in THEME_OPTIONS, f"Theme `{theme_name}` uses undefined option `{option}`"
|
assert (
|
||||||
|
option in THEME_OPTIONS
|
||||||
|
), f"Theme `{theme_name}` uses undefined option `{option}`"
|
||||||
|
|
||||||
chosen_value = options[option]
|
chosen_value = options[option]
|
||||||
possible_values = list(THEME_OPTIONS[option].keys())
|
possible_values = list(THEME_OPTIONS[option].keys())
|
||||||
|
|
||||||
value = chosen_value if chosen_value in possible_values \
|
value = (
|
||||||
|
chosen_value
|
||||||
|
if chosen_value in possible_values
|
||||||
else THEME_OPTIONS[option]["_default"]
|
else THEME_OPTIONS[option]["_default"]
|
||||||
|
)
|
||||||
|
|
||||||
filename += "_" + value
|
filename += "_" + value
|
||||||
|
|
||||||
|
@ -119,13 +120,15 @@ def current_theme_js():
|
||||||
themes = get_active_themes()
|
themes = get_active_themes()
|
||||||
|
|
||||||
selected_theme_name = request.cookies.get("theme", None)
|
selected_theme_name = request.cookies.get("theme", None)
|
||||||
matching_theme = first((t for t in themes if t["file"] == selected_theme_name))
|
matching_theme = first(t for t in themes if t["file"] == selected_theme_name)
|
||||||
cur_theme = matching_theme or themes[-1]
|
cur_theme = matching_theme or themes[-1]
|
||||||
|
|
||||||
response = make_response(rf'''
|
response = make_response(
|
||||||
|
rf"""
|
||||||
var currentTheme = {json.dumps(cur_theme['file'])};
|
var currentTheme = {json.dumps(cur_theme['file'])};
|
||||||
var currentThemeOptions = {json.dumps(cur_theme.get('options', []))};
|
var currentThemeOptions = {json.dumps(cur_theme.get('options', []))};
|
||||||
''')
|
"""
|
||||||
|
)
|
||||||
response.headers["Content-Type"] = "text/javascript"
|
response.headers["Content-Type"] = "text/javascript"
|
||||||
|
|
||||||
# Theme name that is not valid at this moment: delete cookie
|
# Theme name that is not valid at this moment: delete cookie
|
||||||
|
@ -166,7 +169,8 @@ def location_dish(location_id, dish_id) -> str:
|
||||||
dish = loc.dish_by_id(dish_id)
|
dish = loc.dish_by_id(dish_id)
|
||||||
if dish is None:
|
if dish is None:
|
||||||
abort(404)
|
abort(404)
|
||||||
return jsonify([
|
return jsonify(
|
||||||
|
[
|
||||||
{
|
{
|
||||||
"type": c[0],
|
"type": c[0],
|
||||||
"id": c[1].id,
|
"id": c[1].id,
|
||||||
|
@ -184,7 +188,8 @@ def location_dish(location_id, dish_id) -> str:
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
for c in dish.choices
|
for c in dish.choices
|
||||||
])
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@general_bp.route("/about/")
|
@general_bp.route("/about/")
|
||||||
|
@ -204,7 +209,7 @@ def profile() -> str:
|
||||||
def favicon() -> str:
|
def favicon() -> str:
|
||||||
"Generate the favicon"
|
"Generate the favicon"
|
||||||
# pylint: disable=R1705
|
# pylint: disable=R1705
|
||||||
if not get_orders((Order.stoptime > datetime.now())):
|
if not get_orders(Order.stoptime > datetime.now()):
|
||||||
return send_from_directory(
|
return send_from_directory(
|
||||||
os.path.join(app.root_path, "static"),
|
os.path.join(app.root_path, "static"),
|
||||||
"favicon.ico",
|
"favicon.ico",
|
||||||
|
|
|
@ -1,37 +1,27 @@
|
||||||
"Script to generate the order related views of Haldis"
|
"""Script to generate the order related views of Haldis"""
|
||||||
import random
|
import random
|
||||||
|
import re
|
||||||
import typing
|
import typing
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from werkzeug.wrappers import Response
|
|
||||||
|
|
||||||
# from flask import current_app as app
|
# from flask import current_app as app
|
||||||
from flask import (
|
from flask import (Blueprint, abort, flash, redirect, render_template, request,
|
||||||
Blueprint,
|
session, url_for, wrappers)
|
||||||
abort,
|
|
||||||
flash,
|
|
||||||
redirect,
|
|
||||||
render_template,
|
|
||||||
request,
|
|
||||||
session,
|
|
||||||
url_for,
|
|
||||||
wrappers,
|
|
||||||
)
|
|
||||||
from flask_login import current_user, login_required
|
from flask_login import current_user, login_required
|
||||||
|
|
||||||
from forms import AnonOrderItemForm, OrderForm, OrderItemForm
|
from forms import AnonOrderItemForm, OrderForm, OrderItemForm
|
||||||
|
from hlds.definitions import location_definition_version, location_definitions
|
||||||
from models import Order, OrderItem, User, db
|
from models import Order, OrderItem, User, db
|
||||||
from hlds.definitions import location_definitions, location_definition_version
|
|
||||||
from notification import post_order_to_webhook
|
from notification import post_order_to_webhook
|
||||||
from utils import ignore_none
|
from utils import ignore_none, parse_euro_string
|
||||||
|
from werkzeug.wrappers import Response
|
||||||
|
|
||||||
order_bp = Blueprint("order_bp", "order")
|
order_bp = Blueprint("order_bp", "order")
|
||||||
|
|
||||||
|
|
||||||
@order_bp.route("/")
|
@order_bp.route("/")
|
||||||
def orders(form: OrderForm = None) -> str:
|
def orders(form: OrderForm = None) -> str:
|
||||||
"Generate general order view"
|
"""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()
|
form = OrderForm()
|
||||||
location_id = request.args.get("location_id")
|
location_id = request.args.get("location_id")
|
||||||
form.location_id.default = location_id
|
form.location_id.default = location_id
|
||||||
|
@ -43,7 +33,10 @@ def orders(form: OrderForm = None) -> str:
|
||||||
@order_bp.route("/create", methods=["POST"])
|
@order_bp.route("/create", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def order_create() -> typing.Union[str, Response]:
|
def order_create() -> typing.Union[str, Response]:
|
||||||
"Generate order create view"
|
"""Generate order create view"""
|
||||||
|
if not current_user.association_list():
|
||||||
|
flash("Not allowed to create an order.", "info")
|
||||||
|
abort(401)
|
||||||
orderForm = OrderForm()
|
orderForm = OrderForm()
|
||||||
orderForm.populate()
|
orderForm.populate()
|
||||||
if orderForm.validate_on_submit():
|
if orderForm.validate_on_submit():
|
||||||
|
@ -53,14 +46,14 @@ def order_create() -> typing.Union[str, Response]:
|
||||||
db.session.add(order)
|
db.session.add(order)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
post_order_to_webhook(order)
|
post_order_to_webhook(order)
|
||||||
return redirect(url_for("order_bp.order_from_id", order_id=order.id))
|
return redirect(url_for("order_bp.order_from_slug", order_slug=order.slug))
|
||||||
return orders(form=orderForm)
|
return orders(form=orderForm)
|
||||||
|
|
||||||
|
|
||||||
@order_bp.route("/<order_id>")
|
@order_bp.route("/<order_slug>")
|
||||||
def order_from_id(order_id: int, form: OrderForm = None, dish_id=None) -> str:
|
def order_from_slug(order_slug: str, form: OrderForm = None, dish_id=None) -> str:
|
||||||
"Generate order view from id"
|
"""Generate order view from id"""
|
||||||
order = Order.query.filter(Order.id == order_id).first()
|
order = Order.query.filter(Order.slug == order_slug).first()
|
||||||
if order is None:
|
if order is None:
|
||||||
abort(404)
|
abort(404)
|
||||||
if current_user.is_anonymous() and not order.public:
|
if current_user.is_anonymous() and not order.public:
|
||||||
|
@ -72,8 +65,8 @@ def order_from_id(order_id: int, form: OrderForm = None, dish_id=None) -> str:
|
||||||
form.populate(order.location)
|
form.populate(order.location)
|
||||||
if order.is_closed():
|
if order.is_closed():
|
||||||
form = None
|
form = None
|
||||||
total_price = sum([o.price for o in order.items])
|
total_price = sum(o.price or 0 for o in order.items)
|
||||||
debts = sum([o.price for o in order.items if not o.paid])
|
debts = sum(o.price or 0 for o in order.items if not o.paid)
|
||||||
|
|
||||||
dish = order.location.dish_by_id(dish_id) if order.location else None
|
dish = order.location.dish_by_id(dish_id) if order.location else None
|
||||||
|
|
||||||
|
@ -87,44 +80,44 @@ def order_from_id(order_id: int, form: OrderForm = None, dish_id=None) -> str:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@order_bp.route("/<order_id>/items")
|
@order_bp.route("/<order_slug>/items")
|
||||||
def items_shop_view(order_id: int) -> str:
|
def items_shop_view(order_slug: int) -> str:
|
||||||
"Generate order items view from id"
|
"""Generate order items view from id"""
|
||||||
order = Order.query.filter(Order.id == order_id).first()
|
order = Order.query.filter(Order.slug == order_slug).first()
|
||||||
if order is None:
|
if order is None:
|
||||||
abort(404)
|
abort(404)
|
||||||
if current_user.is_anonymous() and not order.public:
|
if current_user.is_anonymous() and not order.public:
|
||||||
flash("Please login to see this order.", "info")
|
flash("Please login to see this order.", "info")
|
||||||
abort(401)
|
abort(401)
|
||||||
total_price = sum([o.price for o in order.items])
|
total_price = sum(o.price or 0 for o in order.items)
|
||||||
return render_template("order_items.html", order=order, total_price=total_price)
|
return render_template("order_items.html", order=order, total_price=total_price)
|
||||||
|
|
||||||
|
|
||||||
@order_bp.route("/<order_id>/edit", methods=["GET", "POST"])
|
@order_bp.route("/<order_slug>/edit", methods=["GET", "POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def order_edit(order_id: int) -> typing.Union[str, Response]:
|
def order_edit(order_slug: str) -> typing.Union[str, Response]:
|
||||||
"Generate order edit view from id"
|
"""Generate order edit view from id"""
|
||||||
order = Order.query.filter(Order.id == order_id).first()
|
order = Order.query.filter(Order.slug == order_slug).first()
|
||||||
if current_user.id is not order.courier_id and not current_user.is_admin():
|
if current_user.id is not order.courier_id and not current_user.is_admin():
|
||||||
abort(401)
|
abort(401)
|
||||||
if order is None:
|
if order is None:
|
||||||
abort(404)
|
abort(404)
|
||||||
orderForm = OrderForm(obj=order)
|
order_form = OrderForm(obj=order)
|
||||||
orderForm.populate()
|
order_form.populate()
|
||||||
if orderForm.validate_on_submit():
|
if order_form.validate_on_submit():
|
||||||
orderForm.populate_obj(order)
|
order_form.populate_obj(order)
|
||||||
order.update_from_hlds()
|
order.update_from_hlds()
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return redirect(url_for("order_bp.order_from_id", order_id=order.id))
|
return redirect(url_for("order_bp.order_from_slug", order_slug=order.slug))
|
||||||
return render_template("order_edit.html", form=orderForm, order_id=order_id)
|
return render_template("order_edit.html", form=order_form, order_slug=order.slug)
|
||||||
|
|
||||||
|
|
||||||
@order_bp.route("/<order_id>/create", methods=["GET", "POST"])
|
@order_bp.route("/<order_slug>/create", methods=["GET", "POST"])
|
||||||
def order_item_create(order_id: int) -> typing.Any:
|
def order_item_create(order_slug: str) -> typing.Any:
|
||||||
# type is 'typing.Union[str, Response]', but this errors due to
|
# type is 'typing.Union[str, Response]', but this errors due to
|
||||||
# https://github.com/python/mypy/issues/7187
|
# https://github.com/python/mypy/issues/7187
|
||||||
"Add item to order from id"
|
"""Add item to order from slug"""
|
||||||
current_order = Order.query.filter(Order.id == order_id).first()
|
current_order = Order.query.filter(Order.slug == order_slug).first()
|
||||||
if current_order is None:
|
if current_order is None:
|
||||||
abort(404)
|
abort(404)
|
||||||
if current_order.is_closed():
|
if current_order.is_closed():
|
||||||
|
@ -133,12 +126,14 @@ def order_item_create(order_id: int) -> typing.Any:
|
||||||
flash("Please login to see this order.", "info")
|
flash("Please login to see this order.", "info")
|
||||||
abort(401)
|
abort(401)
|
||||||
location = current_order.location
|
location = current_order.location
|
||||||
# If location doesn't exist any more, adding items is nonsensical
|
# If location doesn't exist anymore, adding items is nonsensical
|
||||||
if not location:
|
if not location:
|
||||||
abort(404)
|
abort(404)
|
||||||
form = AnonOrderItemForm() if current_user.is_anonymous() else OrderItemForm()
|
form = AnonOrderItemForm() if current_user.is_anonymous() else OrderItemForm()
|
||||||
|
|
||||||
dish_id = request.form["dish_id"] if form.is_submitted() else request.args.get("dish")
|
dish_id = (
|
||||||
|
request.form["dish_id"] if form.is_submitted() else request.args.get("dish")
|
||||||
|
)
|
||||||
if dish_id and not location.dish_by_id(dish_id):
|
if dish_id and not location.dish_by_id(dish_id):
|
||||||
abort(404)
|
abort(404)
|
||||||
if not form.is_submitted():
|
if not form.is_submitted():
|
||||||
|
@ -179,7 +174,7 @@ def order_item_create(order_id: int) -> typing.Any:
|
||||||
return redirect(
|
return redirect(
|
||||||
url_for(
|
url_for(
|
||||||
"order_bp.order_item_create",
|
"order_bp.order_item_create",
|
||||||
order_id=order_id,
|
order_slug=current_order.slug,
|
||||||
dish=form.dish_id.data,
|
dish=form.dish_id.data,
|
||||||
user_name=user_name,
|
user_name=user_name,
|
||||||
comment=comment,
|
comment=comment,
|
||||||
|
@ -188,14 +183,13 @@ def order_item_create(order_id: int) -> typing.Any:
|
||||||
|
|
||||||
# If the form was not submitted (GET request) or the form had errors: show form again
|
# If the form was not submitted (GET request) or the form had errors: show form again
|
||||||
if not form.validate_on_submit():
|
if not form.validate_on_submit():
|
||||||
return order_from_id(order_id, form=form, dish_id=dish_id)
|
return order_from_slug(current_order.slug, form=form, dish_id=dish_id)
|
||||||
|
|
||||||
# Form was submitted and is valid
|
# Form was submitted and is valid
|
||||||
|
|
||||||
item = OrderItem()
|
item = OrderItem()
|
||||||
form.populate_obj(item)
|
form.populate_obj(item)
|
||||||
item.hlds_data_version = location_definition_version
|
item.hlds_data_version = location_definition_version
|
||||||
item.order_id = order_id
|
item.order_id = current_order.id
|
||||||
if not current_user.is_anonymous():
|
if not current_user.is_anonymous():
|
||||||
item.user_id = current_user.id
|
item.user_id = current_user.id
|
||||||
else:
|
else:
|
||||||
|
@ -230,59 +224,82 @@ def order_item_create(order_id: int) -> typing.Any:
|
||||||
|
|
||||||
db.session.add(item)
|
db.session.add(item)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash("Ordered %s" % (item.dish_name), "success")
|
flash("Ordered %s" % item.dish_name, "success")
|
||||||
return redirect(url_for("order_bp.order_from_id", order_id=order_id))
|
return redirect(url_for("order_bp.order_from_slug", order_slug=order_slug))
|
||||||
|
|
||||||
|
|
||||||
@order_bp.route("/<order_id>/<user_name>/user_paid", methods=["POST"])
|
@order_bp.route("/<order_slug>/modify_items", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
# pylint: disable=R1710
|
# pylint: disable=R1710
|
||||||
def items_user_paid(order_id: int, user_name: str) -> typing.Optional[Response]:
|
def modify_items(order_slug: str) -> typing.Optional[Response]:
|
||||||
"Indicate payment status for a user in an order"
|
if "delete_item" in request.form:
|
||||||
|
return delete_item(order_slug, int(request.form["delete_item"]))
|
||||||
|
user_names = request.form.getlist("user_names")
|
||||||
|
if request.form.get("action") == "mark_paid":
|
||||||
|
return set_items_paid(order_slug, user_names, True)
|
||||||
|
elif request.form.get("action") == "mark_unpaid":
|
||||||
|
return set_items_paid(order_slug, 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()
|
||||||
|
total_paid_items = 0
|
||||||
|
total_failed_items = 0
|
||||||
|
for user_name in user_names:
|
||||||
user = User.query.filter(User.username == user_name).first()
|
user = User.query.filter(User.username == user_name).first()
|
||||||
items: typing.List[OrderItem] = []
|
items: typing.List[OrderItem] = []
|
||||||
if user:
|
if user:
|
||||||
items = OrderItem.query.filter(
|
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()
|
).all()
|
||||||
else:
|
else:
|
||||||
items = OrderItem.query.filter(
|
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()
|
).all()
|
||||||
current_order = Order.query.filter(Order.id == order_id).first()
|
|
||||||
if current_order.courier_id == current_user.id or current_user.admin:
|
|
||||||
for item in items:
|
for item in items:
|
||||||
item.paid = True
|
if item.can_modify_payment(order.id, current_user.id):
|
||||||
|
if item.paid != paid:
|
||||||
|
item.paid = paid
|
||||||
|
total_paid_items += 1
|
||||||
|
else:
|
||||||
|
total_failed_items += 1
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash("Paid %d items for %s" % (len(items), item.for_name), "success")
|
if total_failed_items == 0:
|
||||||
return redirect(url_for("order_bp.order_from_id", order_id=order_id))
|
flash("Marked %d items as paid" % (total_paid_items,), "success")
|
||||||
abort(404)
|
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))
|
||||||
|
|
||||||
|
|
||||||
@order_bp.route("/<order_id>/<item_id>/delete", methods=["POST"])
|
@order_bp.route("/<order_slug>/<item_id>/delete", methods=["POST"])
|
||||||
# pylint: disable=R1710
|
# pylint: disable=R1710
|
||||||
def delete_item(order_id: int, item_id: int) -> typing.Any:
|
def delete_item(order_slug: str, item_id: int) -> typing.Any:
|
||||||
# type is 'typing.Optional[Response]', but this errors due to
|
# type is 'typing.Optional[Response]', but this errors due to
|
||||||
# https://github.com/python/mypy/issues/7187
|
# https://github.com/python/mypy/issues/7187
|
||||||
"Delete an item from an order"
|
"""Delete an item from an order"""
|
||||||
item = OrderItem.query.filter(OrderItem.id == item_id).first()
|
item: OrderItem = OrderItem.query.filter(OrderItem.id == item_id).first()
|
||||||
|
order: Order = Order.query.filter(Order.slug == order_slug).first()
|
||||||
user_id = None
|
user_id = None
|
||||||
if not current_user.is_anonymous():
|
if not current_user.is_anonymous():
|
||||||
user_id = current_user.id
|
user_id = current_user.id
|
||||||
if item.can_delete(order_id, user_id, session.get("anon_name", "")):
|
if item.can_delete(order.id, user_id, session.get("anon_name", "")):
|
||||||
dish_name = item.dish_name
|
dish_name = item.dish_name
|
||||||
db.session.delete(item)
|
db.session.delete(item)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash("Deleted %s" % (dish_name), "success")
|
flash("Deleted %s" % dish_name, "success")
|
||||||
return redirect(url_for("order_bp.order_from_id", order_id=order_id))
|
return redirect(url_for("order_bp.order_from_slug", order_slug=order_slug))
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
|
|
||||||
@order_bp.route("/<order_id>/volunteer", methods=["POST"])
|
@order_bp.route("/<order_slug>/volunteer", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def volunteer(order_id: int) -> Response:
|
def volunteer(order_slug: str) -> Response:
|
||||||
"Add a volunteer to an order"
|
"""Add a volunteer to an order"""
|
||||||
order = Order.query.filter(Order.id == order_id).first()
|
order = Order.query.filter(Order.slug == order_slug).first()
|
||||||
if order is None:
|
if order is None:
|
||||||
abort(404)
|
abort(404)
|
||||||
if order.courier_id is None or order.courier_id == 0:
|
if order.courier_id is None or order.courier_id == 0:
|
||||||
|
@ -291,14 +308,14 @@ def volunteer(order_id: int) -> Response:
|
||||||
flash("Thank you for volunteering!")
|
flash("Thank you for volunteering!")
|
||||||
else:
|
else:
|
||||||
flash("Volunteering not possible!")
|
flash("Volunteering not possible!")
|
||||||
return redirect(url_for("order_bp.order_from_id", order_id=order_id))
|
return redirect(url_for("order_bp.order_from_slug", order_slug=order.slug))
|
||||||
|
|
||||||
|
|
||||||
@order_bp.route("/<order_id>/close", methods=["POST"])
|
@order_bp.route("/<order_slug>/close", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def close_order(order_id: int) -> typing.Optional[Response]:
|
def close_order(order_slug: str) -> typing.Optional[Response]:
|
||||||
"Close an order"
|
"""Close an order"""
|
||||||
order = Order.query.filter(Order.id == order_id).first()
|
order = Order.query.filter(Order.slug == order_slug).first()
|
||||||
if order is None:
|
if order is None:
|
||||||
abort(404)
|
abort(404)
|
||||||
if (
|
if (
|
||||||
|
@ -310,12 +327,54 @@ def close_order(order_id: int) -> typing.Optional[Response]:
|
||||||
if courier is not None:
|
if courier is not None:
|
||||||
order.courier_id = courier.id
|
order.courier_id = courier.id
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return redirect(url_for("order_bp.order_from_id", order_id=order_id))
|
return redirect(url_for("order_bp.order_from_slug", order_slug=order_slug))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@order_bp.route("/<order_slug>/prices", methods=["GET", "POST"])
|
||||||
|
@login_required
|
||||||
|
def prices(order_slug: str) -> typing.Optional[Response]:
|
||||||
|
order = Order.query.filter(Order.slug == order_slug).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))
|
||||||
|
|
||||||
|
if request.method == "GET":
|
||||||
|
return render_template(
|
||||||
|
"order_prices.html",
|
||||||
|
order=order,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
new_prices = {}
|
||||||
|
|
||||||
|
for key, value in request.form.items():
|
||||||
|
m = re.fullmatch("item_([0-9]+)", key)
|
||||||
|
if not m:
|
||||||
|
continue
|
||||||
|
item_id = int(m.group(1))
|
||||||
|
|
||||||
|
price = parse_euro_string(value)
|
||||||
|
if not price:
|
||||||
|
flash(f"Could not recognize '{value}' as a price")
|
||||||
|
continue
|
||||||
|
|
||||||
|
new_prices[item_id] = price
|
||||||
|
|
||||||
|
for item in order.items:
|
||||||
|
new_price = new_prices.get(item.id)
|
||||||
|
if new_price is not None and new_price != item.price:
|
||||||
|
item.price = new_price
|
||||||
|
item.price_modified = datetime.now()
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return redirect(url_for("order_bp.order_from_slug", order_slug=order.slug))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def select_user(items) -> typing.Optional[User]:
|
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
|
user = None
|
||||||
# remove non users
|
# remove non users
|
||||||
items = [i for i in items if i.user_id]
|
items = [i for i in items if i.user_id]
|
||||||
|
@ -334,19 +393,20 @@ def select_user(items) -> typing.Optional[User]:
|
||||||
|
|
||||||
|
|
||||||
def get_orders(expression=None) -> typing.List[Order]:
|
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] = []
|
order_list: typing.List[OrderForm] = []
|
||||||
if expression is None:
|
if expression is None:
|
||||||
expression = (datetime.now() > Order.starttime) & (
|
expression = ((datetime.now() > Order.starttime) & (
|
||||||
Order.stoptime
|
Order.stoptime
|
||||||
> datetime.now()
|
> datetime.now()
|
||||||
# pylint: disable=C0121
|
# pylint: disable=C0121
|
||||||
) | (Order.stoptime == None)
|
) | (Order.stoptime == None)
|
||||||
|
) & (Order.association.in_(current_user.association_list()))
|
||||||
if not current_user.is_anonymous():
|
if not current_user.is_anonymous():
|
||||||
order_list = Order.query.filter(expression).all()
|
order_list = Order.query.filter(expression).all()
|
||||||
else:
|
else:
|
||||||
order_list = Order.query.filter(
|
order_list = Order.query.filter(
|
||||||
# pylint: disable=C0121
|
# pylint: disable=C0121
|
||||||
(expression & (Order.public == True))
|
expression & (Order.public == True) & (Order.association.in_(current_user.association_list()))
|
||||||
).all()
|
).all()
|
||||||
return order_list
|
return order_list
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
"Script to generate the stats related views of Haldis"
|
"Script to generate the stats related views of Haldis"
|
||||||
|
from fatmodels import FatLocation, FatOrder, FatOrderItem, FatUser
|
||||||
from flask import Blueprint
|
from flask import Blueprint
|
||||||
from flask import current_app as app
|
from flask import current_app as app
|
||||||
from flask import render_template
|
from flask import render_template
|
||||||
|
|
||||||
from fatmodels import FatLocation, FatOrder, FatOrderItem, FatUser
|
|
||||||
|
|
||||||
stats_blueprint = Blueprint("stats_blueprint", __name__)
|
stats_blueprint = Blueprint("stats_blueprint", __name__)
|
||||||
|
|
||||||
|
|
||||||
|
|
16
app/waitress_wsgi.py
Normal file
16
app/waitress_wsgi.py
Normal 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)
|
17
docker-compose.override.yml
Normal file
17
docker-compose.override.yml
Normal 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
31
docker-compose.yml
Normal 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:
|
|
@ -25,7 +25,7 @@ syn keyword hldsChoiceType single_choice multi_choice nextgroup=hldsBlockIdAf
|
||||||
syn match hldsBlockId "^[a-z0-9_-]\+: "
|
syn match hldsBlockId "^[a-z0-9_-]\+: "
|
||||||
syn match hldsBlockIdAftrKywrd "[a-z0-9_-]\+: " contained
|
syn match hldsBlockIdAftrKywrd "[a-z0-9_-]\+: " contained
|
||||||
|
|
||||||
syn match _doubleSpace " \+" nextgroup=hldsTag,hldsPrice
|
syn match _space " \+" nextgroup=hldsTag,hldsPrice
|
||||||
syn match hldsTag "{[a-z0-9_-]\+}\( \|$\)" contained nextgroup=hldsTag,hldsPrice
|
syn match hldsTag "{[a-z0-9_-]\+}\( \|$\)" contained nextgroup=hldsTag,hldsPrice
|
||||||
syn match hldsPrice "€ *[0-9]\+\(\.[0-9]\+\|\)\( \|$\)" contained
|
syn match hldsPrice "€ *[0-9]\+\(\.[0-9]\+\|\)\( \|$\)" contained
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ E="${normal}"
|
||||||
if [ ! -d "venv" ]; then
|
if [ ! -d "venv" ]; then
|
||||||
PYTHON_VERSION=$(cat .python-version)
|
PYTHON_VERSION=$(cat .python-version)
|
||||||
echo -e "${B} No venv found, creating a new one with version ${PYTHON_VERSION} ${E}"
|
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
|
fi
|
||||||
source venv/bin/activate
|
source venv/bin/activate
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
cd "$(dirname "$0")/app"
|
cd "$(dirname "$0")/app"
|
||||||
cp database/* .
|
|
||||||
../venv/bin/python create_database.py setup_database
|
env python create_database.py setup_database
|
||||||
rm -f add_* create_database.py muhscheme.txt
|
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
|
||||||
|
|
2
pylint-requirement.txt
Normal file
2
pylint-requirement.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
pylint-flask
|
||||||
|
pylint-flask-sqlalchemy
|
|
@ -12,3 +12,5 @@ black
|
||||||
pymysql
|
pymysql
|
||||||
pyyaml
|
pyyaml
|
||||||
tatsu<5.6 # >=5.6 needs Python >=3.8
|
tatsu<5.6 # >=5.6 needs Python >=3.8
|
||||||
|
microsoftgraph-python
|
||||||
|
sentry-sdk[flask]
|
||||||
|
|
|
@ -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:
|
# To update, run:
|
||||||
#
|
#
|
||||||
# pip-compile
|
# pip-compile
|
||||||
|
@ -11,11 +11,15 @@ appdirs==1.4.4
|
||||||
black==21.6b0
|
black==21.6b0
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
blinker==1.4
|
blinker==1.4
|
||||||
# via flask-debugtoolbar
|
# via
|
||||||
|
# flask-debugtoolbar
|
||||||
|
# sentry-sdk
|
||||||
cachelib==0.1.1
|
cachelib==0.1.1
|
||||||
# via flask-oauthlib
|
# via flask-oauthlib
|
||||||
certifi==2021.5.30
|
certifi==2021.5.30
|
||||||
# via requests
|
# via
|
||||||
|
# requests
|
||||||
|
# sentry-sdk
|
||||||
chardet==4.0.0
|
chardet==4.0.0
|
||||||
# via requests
|
# via requests
|
||||||
click==7.1.2
|
click==7.1.2
|
||||||
|
@ -24,6 +28,19 @@ click==7.1.2
|
||||||
# flask
|
# flask
|
||||||
dominate==2.6.0
|
dominate==2.6.0
|
||||||
# via flask-bootstrap
|
# 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
|
flask-admin==1.5.8
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
flask-bootstrap==3.3.7.1
|
flask-bootstrap==3.3.7.1
|
||||||
|
@ -44,18 +61,6 @@ flask-sqlalchemy==2.5.1
|
||||||
# flask-migrate
|
# flask-migrate
|
||||||
flask-wtf==0.15.1
|
flask-wtf==0.15.1
|
||||||
# via -r requirements.in
|
# 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
|
greenlet==1.1.0
|
||||||
# via sqlalchemy
|
# via sqlalchemy
|
||||||
idna==2.10
|
idna==2.10
|
||||||
|
@ -74,6 +79,8 @@ markupsafe==2.0.1
|
||||||
# jinja2
|
# jinja2
|
||||||
# mako
|
# mako
|
||||||
# wtforms
|
# wtforms
|
||||||
|
microsoftgraph-python==1.1.3
|
||||||
|
# via -r requirements.in
|
||||||
mypy-extensions==0.4.3
|
mypy-extensions==0.4.3
|
||||||
# via black
|
# via black
|
||||||
oauthlib==2.1.0
|
oauthlib==2.1.0
|
||||||
|
@ -92,10 +99,14 @@ pyyaml==5.4.1
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
regex==2021.4.4
|
regex==2021.4.4
|
||||||
# via black
|
# via black
|
||||||
|
requests==2.25.1
|
||||||
|
# via
|
||||||
|
# microsoftgraph-python
|
||||||
|
# requests-oauthlib
|
||||||
requests-oauthlib==1.1.0
|
requests-oauthlib==1.1.0
|
||||||
# via flask-oauthlib
|
# via flask-oauthlib
|
||||||
requests==2.25.1
|
sentry-sdk[flask]==1.10.1
|
||||||
# via requests-oauthlib
|
# via -r requirements.in
|
||||||
six==1.16.0
|
six==1.16.0
|
||||||
# via python-dateutil
|
# via python-dateutil
|
||||||
sqlalchemy==1.4.18
|
sqlalchemy==1.4.18
|
||||||
|
@ -106,8 +117,10 @@ tatsu==4.4.0
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
toml==0.10.2
|
toml==0.10.2
|
||||||
# via black
|
# via black
|
||||||
urllib3==1.26.5
|
urllib3==1.26.12
|
||||||
# via requests
|
# via
|
||||||
|
# requests
|
||||||
|
# sentry-sdk
|
||||||
visitor==0.1.3
|
visitor==0.1.3
|
||||||
# via flask-bootstrap
|
# via flask-bootstrap
|
||||||
werkzeug==1.0.1
|
werkzeug==1.0.1
|
||||||
|
|
Loading…
Reference in a new issue