Compare commits

..

6 commits

Author SHA1 Message Date
Midgard 76ac07742e
Allow anonymous order creation 2021-07-08 12:02:50 +02:00
Midgard f0561bcd71
Remove pills introduced in previous commit 2021-06-21 02:08:32 +02:00
Midgard d7dc926992
Aggregate comments 2021-06-21 02:05:11 +02:00
Midgard d20ce9803e
Improve design
Remove main CSS from shop view page. Make it theme independent and
maximize contrast.

Improve spacing in "add item" list.

Refactor old term "showcase" to "shop_view" in code.
2021-06-21 00:46:29 +02:00
Midgard d0699b3716
HLDS: change :: to double space and require it
Require double space before tags and price, like in the plain text
accounting format of ledger. This makes it easier to differentiate
between prices mentioned in descriptions and the price for the dish.
2021-06-20 23:57:50 +02:00
Midgard a1a68a3fd6
Add space at bottom in show view
On big orders the bottom of the page was awkward.
2021-06-20 21:47:24 +02:00
69 changed files with 562 additions and 1187 deletions

View file

@ -5,8 +5,6 @@
# 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
@ -30,7 +28,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=pylint_flask_sqlalchemy,pylint_flask load-plugins=
# Pickle collected data for later comparisons. # Pickle collected data for later comparisons.
persistent=yes persistent=yes
@ -62,7 +60,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,C0415 disable=E0401,E0611,C0103,W0511,W0611
# 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

View file

@ -1 +1 @@
3.9.2 3.5.3

View file

@ -1 +0,0 @@
python 3.9.2

View file

@ -14,7 +14,6 @@ Be lazier today!
## Local setup ## Local setup
There is a special script to get started with the project. Just run it in the root of the project. There is a special script to get started with the project. Just run it in the root of the project.
Note: this script might require you to install a certain python version, you can do this using your favorite tool e.g. [pyenv](https://github.com/pyenv/pyenv#simple-python-version-management-pyenv)
./first-setup.sh ./first-setup.sh
@ -26,7 +25,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, note that you might want to put your name in the `HALDIS_ADMINS` in `app/config.py` You can now still seed the database by running
./populate-db.sh ./populate-db.sh

View file

View file

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

View file

@ -1,22 +1,19 @@
"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",)
@ -25,45 +22,27 @@ 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", "association"] column_list = ["starttime", "stoptime", "location_name", "location_id", "courier"]
column_labels = { column_labels = {
"starttime": "Start Time", "starttime": "Start Time", "stoptime": "Closing Time",
"stoptime": "Closing Time", "location_id": "HLDS Location ID"}
"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_id", "order.location_name", "user_name", "user", "dish_name", "dish_id", "comment", "price", "paid",
"slug", "hlds_data_version"
"order.location_name",
"user_name",
"user",
"dish_name",
"dish_id",
"comment",
"price",
"paid",
"hlds_data_version",
] ]
column_labels = { column_labels = {
"order_id": "Order", "order_id": "Order", "order.location_name": "Order's Location",
"order.location_name": "Order's Location", "user_name": "Anon. User", "user_id": "Registered User",
"user_name": "Anon. User", "hlds_data_version": "HLDS Data Version", "dish_id": "HLDS Dish ID"}
"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:

View file

@ -1,33 +1,34 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Main Haldis script""" "Main Haldis script"
import logging import logging
import sentry_sdk from logging.handlers import TimedRotatingFileHandler
import typing import typing
from datetime import datetime from datetime import datetime
from logging.handlers import TimedRotatingFileHandler
from admin import init_admin try:
from config import Configuration import airbrake
from flask import Flask, render_template, Response except ImportError:
airbrake = None
from flask import Flask, render_template
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_oauthlib.client import OAuth, OAuthException
from flask_script import Manager, Server from flask_script import Manager, Server
from login import init_login
from markupsafe import Markup from markupsafe import Markup
from admin import init_admin
from login import init_login
from models import db from models import db
from models.anonymous_user import AnonymouseUser from models.anonymous_user import AnonymouseUser
from sentry_sdk.integrations.flask import FlaskIntegration from utils import euro_string, price_range_string
from utils import euro_string, price_range_string, ignore_none
from zeus import init_oauth from zeus import init_oauth
def register_plugins(app: Flask) -> Manager: def register_plugins(app: Flask) -> Manager:
"""Register the plugins to the app""" "Register Airbrake and logrotation plugins"
# pylint: disable=W0612 # pylint: disable=W0612
if not app.debug: if not app.debug:
timedFileHandler = TimedRotatingFileHandler( timedFileHandler = TimedRotatingFileHandler(
@ -40,6 +41,26 @@ def register_plugins(app: Flask) -> Manager:
loglogger.addHandler(timedFileHandler) loglogger.addHandler(timedFileHandler)
app.logger.addHandler(timedFileHandler) app.logger.addHandler(timedFileHandler)
if app.config["AIRBRAKE_ID"]:
if airbrake is None:
raise Exception(
"Airbrake support was requested (AIRBRAKE_ID is present in config), "
"but could not import airbrake. Make sure `airbrake` is installed"
)
airbrakelogger = logging.getLogger("airbrake")
airbrake_obj = airbrake.Airbrake(
project_id=app.config["AIRBRAKE_ID"], api_key=app.config["AIRBRAKE_KEY"]
)
# Change URL in a hacky way to make this work for our errbit
airbrake_obj._api_url = "http://errbit.awesomepeople.tv/api/v3/projects/{}/notices".format( # pylint: disable=protected-access
airbrake_obj.project_id
)
airbrakelogger.addHandler(airbrake.AirbrakeHandler(airbrake=airbrake_obj))
app.logger.addHandler(airbrake.AirbrakeHandler(airbrake=airbrake_obj))
# Initialize SQLAlchemy # Initialize SQLAlchemy
db.init_app(app) db.init_app(app)
@ -72,8 +93,7 @@ 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_HTTPONLY=True, SESSION_COOKIE_SAMESITE="Lax",
SESSION_COOKIE_SAMESITE="Lax",
) )
if not app.debug: if not app.debug:
@ -83,8 +103,7 @@ 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]:
@ -96,15 +115,15 @@ 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 login import auth_bp
from views.debug import debug_bp
from views.general import general_bp
from views.order import order_bp from views.order import order_bp
from views.general import general_bp
from views.stats import stats_blueprint from views.stats import stats_blueprint
from views.debug import debug_bp
from login import auth_bp
from zeus import oauth_bp from zeus import oauth_bp
application.register_blueprint(general_bp, url_prefix="/") application.register_blueprint(general_bp, url_prefix="/")
@ -118,8 +137,7 @@ def add_routes(application: Flask) -> None:
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(
@ -154,37 +172,19 @@ 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)
def create_app(): app = Flask(__name__)
"""Initializer for the Flask app object"""
app = Flask(__name__)
@app.route('/robots.txt') # Load the config file
def noindex(): app.config.from_object("config.Configuration")
r = Response(response="User-Agent: *\nDisallow: /\n", status=200, mimetype="text/plain")
r.headers["Content-Type"] = "text/plain; charset=utf-8"
return r
# Load the config file
app.config.from_object("config.Configuration")
app_manager = register_plugins(app) app_manager = register_plugins(app)
add_handlers(app) add_handlers(app)
add_routes(app) add_routes(app)
add_template_filters(app) add_template_filters(app)
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__":
if Configuration.SENTRY_DSN: app_manager.run()
sentry_sdk.init(
dsn=Configuration.SENTRY_DSN,
integrations=[FlaskIntegration()]
)
app, app_mgr = create_app()
app_mgr.run()

View file

@ -8,10 +8,10 @@ class Configuration:
SQLALCHEMY_DATABASE_URI = "sqlite:///haldis.db" SQLALCHEMY_DATABASE_URI = "sqlite:///haldis.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"
AIRBRAKE_ID = ""
AIRBRAKE_KEY = ""

View file

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

View file

@ -1,10 +1,6 @@
"""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,
@ -15,13 +11,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
@ -29,12 +25,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(f"Adding {entry_set}.") print("Adding {}.".format(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()
@ -42,19 +38,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 = list(entry_sets) available = [entry_set for entry_set in entry_sets]
def add_numbers() -> str: def add_numbers() -> str:
return " ".join( return " ".join(
[f"{loc}({i}), " for i, loc in enumerate(available)] ["{}({}), ".format(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(f"Available: {add_numbers()} : ") answer = input("Available: {} : ".format(add_numbers()))
if answer.lower() == "a": if answer.lower() == "a":
add_all() add_all()
available = [] available = []
@ -62,7 +58,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(f"Adding {available[answer_index]}.") print("Adding {}.".format(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:
@ -72,7 +68,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():

View file

@ -9,7 +9,6 @@ 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

View file

@ -1,31 +1,23 @@
"Module used for everything related to the fat versions of models"
import typing import typing
from hlds.definitions import location_definitions
from hlds.models import Dish, Location
from models import Order, OrderItem, User
from sqlalchemy.sql import desc, func from sqlalchemy.sql import desc, func
from hlds.definitions import location_definitions
from hlds.models import Location, Dish
from models import Order, OrderItem, User
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
@ -36,7 +28,6 @@ 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
@ -44,15 +35,16 @@ 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):
"Function to get the total of all items per order" return (
return (Order.query.join(OrderItem).group_by(Order.id).with_entities( Order.query.join(OrderItem)
Order.id, .group_by(Order.id)
func.count(OrderItem.user_id).label("total"))) .with_entities(Order.id, func.count(OrderItem.user_id).label("total"))
)
class FatUser(User, FatModel): class FatUser(User, FatModel):
"Fat version of the User model" pass
class FatOrderItem(OrderItem, FatModel): class FatOrderItem(OrderItem, FatModel):
"Fat version of the OrderItem model" pass

View file

@ -1,16 +1,25 @@
"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 request, session from flask import session, request
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 hlds.definitions import location_definitions from wtforms import (
from hlds.models import Choice, Dish, Location DateTimeField,
from models import User SelectField,
SelectMultipleField,
StringField,
SubmitField,
FieldList,
validators,
)
from utils import euro_string, price_range_string from utils import euro_string, price_range_string
from wtforms import (DateTimeField, FieldList, SelectField, from hlds.definitions import location_definitions
SelectMultipleField, StringField, SubmitField, validators) from hlds.models import Location, Dish, Choice
from models import User
class OrderForm(Form): class OrderForm(Form):
@ -24,25 +33,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():
self.courier_id.choices = [ self.courier_id.choices = [(0, None)] + (
(0, None), [(u.id, u.username) for u in User.query.order_by("username")] if current_user.is_admin()
(current_user.id, current_user.username), else [(current_user.id, current_user.username)] if current_user.is_authenticated()
] + [ else []
(u.id, u.username) for u in User.query.order_by("username") if u.id != current_user.id )
]
else:
self.courier_id.choices = [
(0, None),
(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)
@ -55,7 +56,6 @@ 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")
@ -82,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

View file

@ -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 Choice, Location, Option from .models import Location, Choice, Option

View file

@ -1,11 +1,11 @@
# Import this class to load the standard HLDS definitions # Import this class to load the standard HLDS definitions
import subprocess
from os import path from os import path
from typing import List from typing import List
import subprocess
from .models import Location
from .parser import parse_all_directory from .parser import parse_all_directory
from .models import Location
__all__ = ["location_definitions", "location_definition_version"] __all__ = ["location_definitions", "location_definition_version"]

View file

@ -1,28 +1,26 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
from typing import Any, Iterable, List, Mapping, Optional, Tuple from typing import Iterable, List, Tuple, Mapping, Any, Optional
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:
# pylint: disable=consider-using-f-string return " :: {}".format(" ".join(["{" + tag + "}" for tag in tags])) \
return " :: {}".format(" ".join(["{" + tag + "}" if tags \
for tag in tags])) if tags else "" else ""
def _format_price(price: int) -> str: def _format_price(price: int) -> str:
return f" {euro_string(price)}" if price else "" return " {}".format(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 f"{type_} {choice}" return "{} {}".format(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
@ -31,17 +29,15 @@ 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,
f" -- {self.description}" if self.description else "", " -- {}".format(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
@ -52,7 +48,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,
f" -- {self.description}" if self.description else "", " -- {}".format(self.description) if self.description else "",
"\n\t\t".join(map(str, self.options)), "\n\t\t".join(map(str, self.options)),
) )
@ -61,7 +57,6 @@ 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
@ -75,7 +70,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,
f" -- {self.description}" if self.description else "", " -- {}".format(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)),
@ -91,20 +86,14 @@ 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__(
def __init__(self, self, id_, *, name, dishes, osm=None, address=None, telephone=None, website=None
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
@ -118,18 +107,24 @@ 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 ("============================\n" return (
"{0.id}: {0.name}" "============================\n"
"{1}\n" "{0.id}: {0.name}"
"============================\n" "{1}\n"
"\n" "============================\n"
"{2}").format( "\n"
self, "{2}"
"".join(f"\n\t{k} {v}" for k, v in ( ).format(
("osm", self.osm), self,
("address", self.address), "".join(
("telephone", self.telephone), "\n\t{} {}".format(k, v)
("website", self.website), for k, v in (
) if v is not None), ("osm", self.osm),
"\n".join(map(str, self.dishes)), ("address", self.address),
("telephone", self.telephone),
("website", self.website),
) )
if v is not None
),
"\n".join(map(str, self.dishes)),
)

View file

@ -1,17 +1,16 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import itertools
from copy import deepcopy
from glob import glob from glob import glob
from os import path from os import path
from typing import Iterable, List, Tuple, Union import itertools
from copy import deepcopy
from typing import Iterable, List, Union, Tuple
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,17 +57,6 @@ class HldsSemanticActions:
for option in first_single_choice.options: for option in first_single_choice.options:
option.price += dish.price option.price += dish.price
dish.price = 0 dish.price = 0
dishes = list(dishes)
dishes.append(
Dish(
"custom",
name="Vrije keuze",
description="Zet wat je wil in comment",
price=0,
tags=[],
choices=[],
)
)
attributes = {att["key"]: att["value"] for att in ast["attributes"]} attributes = {att["key"]: att["value"] for att in ast["attributes"]}
@ -148,7 +136,7 @@ def parse(menu: str) -> List[Location]:
def parse_file(filename: str) -> List[Location]: def parse_file(filename: str) -> List[Location]:
with open(filename) as file_handle: with open(filename, "r") as file_handle:
return parse(file_handle.read()) return parse(file_handle.read())

View file

@ -1,8 +1,9 @@
"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 models import User
from werkzeug.wrappers import Response from werkzeug.wrappers import Response
from models import User
from zeus import zeus_login from zeus import zeus_login
auth_bp = Blueprint("auth_bp", __name__) auth_bp = Blueprint("auth_bp", __name__)

View file

@ -1,8 +1,10 @@
"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

View file

@ -1,30 +0,0 @@
"""add slug
Revision ID: 29ccbe077c57
Revises: 55013fe95bea
Create Date: 2022-05-20 19:46:11.924218
"""
# revision identifiers, used by Alembic.
revision = '29ccbe077c57'
down_revision = '55013fe95bea'
from alembic import op
import sqlalchemy as sa
from sqlalchemy.sql import text
def upgrade():
op.add_column('order', sa.Column(
'slug',
sa.String(length=8),
nullable=False,
# Default: random alphanumerical string
server_default=text('SUBSTRING(MD5(RAND()) FROM 1 FOR 7)')
))
op.create_unique_constraint('order_slug_unique', 'order', ['slug'])
def downgrade():
op.drop_constraint('order_slug_unique', 'order', type_='unique')
op.drop_column('order', 'slug')

View file

@ -1,21 +0,0 @@
"""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')

View file

@ -12,11 +12,11 @@ revision = "9159a6fed021"
down_revision = "150252c1cdb1" down_revision = "150252c1cdb1"
from itertools import chain from itertools import chain
import sqlalchemy as sa
from alembic import op from alembic import op
import sqlalchemy as sa
from sqlalchemy.sql import table, column, text
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,106 +50,71 @@ LOCATION_LEGACY_TO_HLDS = {
def upgrade(): def upgrade():
# First the simple actions # First the simple actions
op.create_table( op.create_table("order_item_choice",
"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( sa.ForeignKeyConstraint(["order_item_id"], ["order_item.id"], ),
["order_item_id"], sa.PrimaryKeyConstraint("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( op.add_column("order_item", sa.Column("dish_id", sa.String(length=64), 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("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 = table("order_item",
"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( op.execute(text("""
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.execute(text("ALTER TABLE order_item DROP FOREIGN KEY order_item_ibfk_3"))
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.execute(text("ALTER TABLE `order` DROP FOREIGN KEY order_ibfk_2")) op.execute(text("ALTER TABLE `order` DROP FOREIGN KEY order_ibfk_2"))
op.alter_column( op.alter_column("order", "location_id", new_column_name="legacy_location_id",
"order", type_=sa.Integer, nullable=True)
"location_id", op.add_column("order", sa.Column("location_id", sa.String(length=64), nullable=True))
new_column_name="legacy_location_id", op.add_column("order", sa.Column("location_name", sa.String(length=128), nullable=True))
type_=sa.Integer,
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 = table("order",
"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 = [
order.update() order.update()
.where(order.c.legacy_location_id == old_id) .where(order.c.legacy_location_id == old_id)
.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)"""

View file

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

View file

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

View file

@ -1,14 +1,10 @@
"AnonymouseUser for people who are not logged in the normal way" "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

View file

@ -1,26 +1,16 @@
"""Script for everything Order related in the database""" "Script for everything Order related in the database"
import typing import typing
from collections import defaultdict
from datetime import datetime from datetime import datetime
import secrets from collections import defaultdict
import string
from hlds.definitions import location_definitions
from utils import first from utils import first
from hlds.definitions import location_definitions
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))
@ -28,8 +18,6 @@ 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(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")
@ -43,9 +31,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 f"Order {self.id} @ {self.location.name or 'None'}" return "Order %d @ %s" % (self.id, self.location.name or "None")
else: else:
return f"Order {self.id}" return "Order %d" % (self.id)
def update_from_hlds(self) -> None: def update_from_hlds(self) -> None:
""" """
@ -58,21 +46,19 @@ 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] = {} group: typing.Dict[str, typing.List] = dict()
# 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] = []
@ -84,17 +70,12 @@ 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( def group_by_dish(self) \
self, -> typing.List[typing.Tuple[str, int, typing.List[typing.Tuple[str, typing.List]]]]:
) -> typing.List[ "Group items of an Order by dish"
typing.Tuple[str, int, typing.List[typing.Tuple[str, typing.List]]] group: typing.Dict[str, typing.Dict[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)
@ -106,17 +87,16 @@ 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
@ -125,13 +105,3 @@ 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)

View file

@ -1,16 +1,15 @@
"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 hlds.definitions import location_definitions
from utils import first from utils import first
from hlds.definitions import location_definitions
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"))
@ -18,25 +17,24 @@ 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", choices = db.relationship("OrderItemChoice", backref="order_item", lazy="dynamic")
backref="order_item",
lazy="dynamic")
def __getattr__(self, name): def __getattr__(self, name):
if name == "dish": if name == "dish":
location_id = (Order.query.filter( location_id = (
Order.id == self.order_id).first().location_id) Order.query.filter(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( return first(filter(lambda d: d.id == self.dish_id, location.dishes))
filter(lambda d: d.id == self.dish_id, location.dishes)) else:
raise ValueError(f"No Location found with id: {location_id}") raise ValueError("No Location found with id: " + location_id)
raise AttributeError() raise AttributeError()
@property @property
@ -47,7 +45,11 @@ class OrderItem(db.Model):
return self.user_name return self.user_name
def __repr__(self) -> str: def __repr__(self) -> str:
return "Order {self.order_id or 0}: {self.for_name} wants {self.dish_name or 'None'}" return "Order %d: %s wants %s" % (
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:
""" """
@ -61,7 +63,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():
@ -74,12 +76,3 @@ 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

View file

@ -1,4 +1,3 @@
"Script for everything OrderItemChoice related in the database"
from datetime import datetime from datetime import datetime
from .database import db from .database import db
@ -6,7 +5,6 @@ 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(
@ -18,8 +16,7 @@ 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 f"{self.name}: {self.value}" return "{}: {}".format(self.name, self.value)

View file

@ -1,6 +1,4 @@
"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
@ -10,10 +8,6 @@ class User(db.Model):
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)
# Assocation logic
associations = db.Column(db.String(255), nullable=False, server_default="")
# Relations
runs = db.relation( runs = db.relation(
"Order", "Order",
backref="courier", backref="courier",
@ -22,17 +16,11 @@ class User(db.Model):
) )
orderItems = db.relationship("OrderItem", backref="user", lazy="dynamic") orderItems = db.relationship("OrderItem", backref="user", lazy="dynamic")
def association_list(self) -> List[str]: def configure(self, username: str, admin: bool, bias: int) -> None:
return self.associations.split(",") "Configure the User"
def configure(self, username: str, admin: bool, bias: int, 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.associations = ",".join(associations)
# pylint: disable=C0111, R0201 # pylint: disable=C0111, R0201
def is_authenticated(self) -> bool: def is_authenticated(self) -> bool:
@ -51,4 +39,4 @@ class User(db.Model):
return str(self.id) return str(self.id)
def __repr__(self) -> str: def __repr__(self) -> str:
return f"{self.username}" return "%s" % self.username

View file

@ -11,29 +11,28 @@ 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, C0209 # pylint: disable=C0301
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_slug", order_slug=order.slug, _external=True), url_for("order_bp.order_from_id", order_id=order.id, _external=True),
order.location_name, order.location_name,
remaining_minutes(order.stoptime), remaining_minutes(order.stoptime),
order.courier.username, order.courier.username.title(),
) )
# pylint: disable=C0209
return "<!channel|@channel> New order for {}. Deadline in {} minutes. <{}|Open here.>".format( 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_slug", order_slug=order.slug, _external=True), url_for("order_bp.order_from_id", order_id=order.id, _external=True),
) )
def post_order_to_webhook(order: Order) -> None: 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"])
@ -41,10 +40,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().__init__() super(WebhookSenderThread, self).__init__()
self.message = message self.message = message
self.url = url self.url = url
@ -52,7 +51,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:
@ -60,9 +59,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 = int(delta.total_seconds() // 60) minutes = delta.total_seconds() // 60
return f"{minutes:02}" return "%02d" % minutes

View file

@ -1,8 +1,9 @@
#!/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

View file

@ -18,9 +18,7 @@ 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 create_app from app import app as application
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__":

View file

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/static/icons/mstile-150x150.png"/>
<TileColor>#1c1c1c</TileColor>
</tile>
</msapplication>
</browserconfig>

View file

@ -146,13 +146,8 @@ a {
.navbar-default .navbar-brand:focus { .navbar-default .navbar-brand:focus {
color: var(--gray0); color: var(--gray0);
} }
hr { hr{
border-top: 1px solid var(--gray2); border-top: 1px solid var(--gray2);
margin: 0;
margin-bottom: 23px;
}
footer {
padding: 23px 0;
} }
h1, h2, h3, h4, h5, h6{ h1, h2, h3, h4, h5, h6{
@ -243,9 +238,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: "";
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

View file

@ -1,32 +0,0 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="282.000000pt" height="282.000000pt" viewBox="0 0 282.000000 282.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.14, written by Peter Selinger 2001-2017
</metadata>
<g transform="translate(0.000000,282.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M0 1410 l0 -1410 1410 0 1410 0 0 1410 0 1410 -312 0 -313 -1 -254
-409 c-140 -226 -257 -410 -261 -410 -4 0 -11 15 -17 33 -47 140 -103 288
-103 271 0 -11 -9 -47 -20 -79 -11 -31 -17 -61 -14 -66 3 -5 1 -9 -4 -9 -19 0
-20 -43 -6 -138 8 -54 18 -104 21 -112 3 -8 5 -19 4 -24 -3 -11 59 -397 66
-416 3 -8 5 -19 4 -24 -3 -12 18 -146 26 -166 4 -8 5 -19 4 -24 -3 -11 59
-397 66 -416 3 -8 5 -19 4 -24 -4 -16 22 -161 30 -170 4 -5 -14 -27 -41 -50
-27 -22 -103 -90 -169 -149 l-120 -108 -99 90 c-55 50 -102 87 -106 83 -4 -4
-6 -2 -5 3 2 6 -22 35 -54 65 l-57 55 15 94 c9 51 15 98 14 105 -1 6 1 18 4
26 7 19 49 284 46 296 -1 5 1 16 4 24 7 19 49 284 46 296 -1 5 1 16 4 24 7 19
49 284 46 296 -1 5 1 16 4 24 3 8 16 78 28 155 l21 139 -28 94 c-15 51 -31 89
-35 85 -4 -4 -30 -69 -57 -143 -26 -74 -52 -135 -56 -135 -5 0 -122 182 -260
405 l-253 405 -316 3 -317 2 0 -1410z"/>
<path d="M978 2675 c183 -80 338 -145 343 -145 5 0 9 -8 10 -17 0 -11 3 -13 6
-5 3 8 29 12 78 12 49 0 75 -4 78 -12 4 -9 6 -8 6 2 1 10 117 65 336 159 184
80 335 146 335 148 0 2 -343 3 -762 3 l-763 0 333 -145z"/>
<path d="M1260 1084 l29 -5 4 -92 3 -92 0 95 -1 95 -32 3 c-32 2 -32 2 -3 -4z"/>
<path d="M1550 1084 l30 -5 3 -227 3 -227 -1 230 0 230 -32 3 c-32 2 -32 2 -3
-4z"/>
<path d="M1294 740 c0 -58 1 -81 3 -52 2 28 2 76 0 105 -2 28 -3 5 -3 -53z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

File diff suppressed because one or more lines are too long

View file

@ -4,7 +4,7 @@ L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
}).addTo(map); }).addTo(map);
let base_request_uri = "https://photon.komoot.io/api/?limit=1&q="; let base_request_uri = "https://photon.komoot.de/api/?limit=1&q=";
function performRequest(url, location, success_callback) { function performRequest(url, location, success_callback) {
var request = new XMLHttpRequest(); var request = new XMLHttpRequest();

View file

@ -1,23 +0,0 @@
/**
* PWA (progressive web app) support
*/
function ready() {
// Make sure the browser supports service workers
if ("serviceWorker" in navigator) {
// Register a new service worker.
navigator.serviceWorker
.register("/static/js/sw.js")
.then((registration) => {
console.log(
"[PWA] Service worker registered with scope: ",
registration.scope
);
})
.catch((error) => {
console.error("[PWA] Service worker registration failed: ", error);
});
}
}
// Load on document load
document.addEventListener("DOMContentLoaded", ready);

File diff suppressed because one or more lines are too long

View file

@ -1,13 +0,0 @@
// Service worker for PWA support
// Install Hook
// Triggered when the PWA is installed by the browser.
self.addEventListener("install", () => {
console.log("[Service Worker] Installed");
});
// Activate Hook
// Triggered when the PWA is activated by the browser.
self.addEventListener("activate", () => {
console.log("[Service Worker] Activated");
});

View file

@ -6,7 +6,7 @@
const YEAR = 60 * 60 * 24 * 365; const YEAR = 60 * 60 * 24 * 365;
const storeCookieAndReload = (name, value) => { const storeCookieAndReload = (name, value) => {
document.cookie = name + " = " + value + "; Path=/; Secure; SameSite=strict; Max-Age=" + (50 * YEAR); document.cookie = name + " = " + value + "; Path=/; Max-Age=" + (50 * YEAR);
location.reload(); location.reload();
} }

View file

@ -1,22 +0,0 @@
{
"name": "Haldis",
"short_name": "Haldis",
"description": "Zeus WPI's food ordering system",
"theme_color": "#ff7f00",
"background_color": "#1c1c1c",
"display": "standalone",
"scope": "/",
"start_url": "/",
"icons": [
{
"src": "/static/icons/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/static/icons/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}

View file

@ -9,15 +9,13 @@
('general_bp.about', 'About'), ('general_bp.about', 'About'),
('stats_blueprint.stats', 'Stats'), ('stats_blueprint.stats', 'Stats'),
] -%} ] -%}
{% if current_user.is_admin() -%} {% if current_user.is_admin() -%}
{% set navbar = navbar + [('admin.index', 'Admin')] -%} {% set navbar = navbar + [('admin.index', 'Admin')] -%}
{% endif -%} {% endif -%}
{% set active_page = active_page|default('index') -%} {% set active_page = active_page|default('index') -%}
{% block title %} {% block title %}
Haldis - {{ active_page|capitalize }} Haldis - {{ active_page|capitalize }}
{% if title %} {% if title %}
- {{ title }} - {{ title }}
{% endif %} {% endif %}
@ -35,26 +33,6 @@
<script type="text/javascript" src="{{ url_for('general_bp.current_theme_js') }}"></script> <script type="text/javascript" src="{{ url_for('general_bp.current_theme_js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/theme.js') }}"></script> <script type="text/javascript" src="{{ url_for('static', filename='js/theme.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/timer.js') }}"></script> <script type="text/javascript" src="{{ url_for('static', filename='js/timer.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/pwa.js') }}"></script>
{% endblock %}
{% block head %}
{{ super() }}
<!-- Manifest (for PWA support) -->
<link rel="manifest" href="{{ url_for('static', filename='manifest.json') }}">
<!-- Favicons -->
<link rel="apple-touch-icon" sizes="180x180" href="{{ url_for('static', filename='icons/apple-touch-icon.png') }}">
<link rel="icon" type="image/png" sizes="32x32" href="{{ url_for('static', filename='icons/favicon-32x32.png') }}">
<link rel="icon" type="image/png" sizes="16x16" href="{{ url_for('static', filename='icons/favicon-16x16.png') }}">
<link rel="mask-icon" href="{{ url_for('static', filename='icons/safari-pinned-tab.svg') }}" color="#5bbad5">
<link rel="shortcut icon" href="{{ url_for('static', filename='icons/favicon.ico') }}">
<!-- Theme colors -->
<meta name="msapplication-TileColor" content="#1c1c1c">
<meta name="msapplication-config" content="{{ url_for('static', filename='browserconfig.xml') }}">
<meta name="theme-color" content="#1c1c1c">
{% endblock %} {% endblock %}
{% block navbar %} {% block navbar %}
@ -106,7 +84,7 @@
<div class="footerWrapper"> <div class="footerWrapper">
<div class="pull-left">Made with ❤ by <a href="https://zeus.gent/">Zeus WPI</a></div> <div class="pull-left">Made with ❤ by <a href="https://zeus.gent/">Zeus WPI</a></div>
<div class="changeThemeButton" id="themeChange"><!-- Populated with JS --></div> <div class="changeThemeButton" id="themeChange"><!-- Populated with JS --></div>
<div class="pull-right"><a href="https://github.com/ZeusWPI/Haldis">© {{ ""|year }}</a></div> <div class="pull-right"><a href="http://github.com/ZeusWPI/Haldis">© {{ ""|year }}</a></div>
</div> </div>
</div> </div>
</footer> </footer>

View file

@ -7,8 +7,8 @@
<div class="row location_data" xmlns="http://www.w3.org/1999/html"> <div class="row location_data" xmlns="http://www.w3.org/1999/html">
<div class="col-md-push-1 col-md-5 darker"> <div class="col-md-push-1 col-md-5 darker">
<h3>{{ location.name }}</h3> <h3>{{ location.name }}</h3>
{% if location.address %}<span class="glyphicon glyphicon-home"></span>{{ location.address }}<br/>{% endif %} {% if location.address %}<span class="glyphicon glyphicon-home"></span> {{ location.address }}<br/>{% endif %}
{% if location.telephone %}<span class="glyphicon glyphicon-phone"></span><a href="tel:{{ location.telephone }}">{{ location.telephone }}</a><br/>{% endif %} {% if location.telephone %}<span class="glyphicon glyphicon-phone"></span>{{ location.telephone }}<br/>{% endif %}
{% if location.website %}<span class="glyphicon glyphicon-link"></span> <a href="{{ location.website}}">{{ location.website }}</a> <br/>{% endif %} {% if location.website %}<span class="glyphicon glyphicon-link"></span> <a href="{{ location.website}}">{{ location.website }}</a> <br/>{% endif %}
{% if location.osm %}<span class="glyphicon glyphicon-map-marker"></span> <a href="{{ location.osm}}">{{ location.osm }}</a> <br/>{% endif %} {% if location.osm %}<span class="glyphicon glyphicon-map-marker"></span> <a href="{{ location.osm}}">{{ location.osm }}</a> <br/>{% endif %}
{% if not current_user.is_anonymous() %} {% if not current_user.is_anonymous() %}

View file

@ -12,39 +12,18 @@
{% 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 class="row"> <header>
<div class="col-md-2" style="padding-top: 2em"> <h2 id="order-title">Order {{ order.id }}</h2>
<div id="qrcode"></div>
<script type="text/javascript">
var qrcode = new QRCode(document.getElementById("qrcode"), {
text: "{{ url_for("order_bp.order_from_slug", order_slug=order.slug, _external=True) }}",
width: 128,
height: 128,
colorDark : "#000000",
colorLight : "#ffffff",
correctLevel : QRCode.CorrectLevel.H
});
</script>
</div>
<div class="col-md-10">
<h2 id="order-title">Order {{ order.id }}</h2>
<div class="location"> <div class="location">
{% if order.location %} {% if order.location %}
<a href="{{ url_for('general_bp.location', location_id=order.location_id) }}">{{ order.location_name }}</a> <a href="{{ url_for('general_bp.location', location_id=order.location_id) }}">{{ order.location_name }}</a>
{% else %} {% else %}
{{ order.location_name }} {{ order.location_name }}
{% endif %} {% endif %}
</div>
<div>
Unique order link: <code>{{ url_for("order_bp.order_from_slug", order_slug=order.slug, _external=True) }}</code>
</div>
</div> </div>
</header> </header>
@ -57,7 +36,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_slug=order.slug, item_id=item.id) }}" method="post" style="display:inline"> <form action="{{ url_for('order_bp.delete_item', order_id=order.id, item_id=item.id) }}" method="post" style="display:inline">
<button class="btn btn-link btn-sm" type="submit" style="padding: 0 0.5em;"><span class="glyphicon glyphicon-remove"></span></button> <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 %}
@ -86,7 +65,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_slug=order.slug) }}" id="dish_{{ dish.id }}"> <form method="post" action="{{ url_for('order_bp.order_item_create', order_id=order.id) }}" id="dish_{{ dish.id }}">
{{ form.csrf_token }} {{ form.csrf_token }}
<input type="hidden" name="dish_id" value="{{ dish.id }}" /> <input type="hidden" name="dish_id" value="{{ dish.id }}" />
@ -155,66 +134,60 @@
<div class="box" id="order_info"> <div class="box" id="order_info">
<h3>Order information</h3> <h3>Order information</h3>
<div class="row"> <dl>
<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>
<dt>Order closes</dt> <dt>Order closes</dt>
<dd> <dd>
{% if order.stoptime %} {% if order.stoptime %}
{% set stoptimefmt = ( {% set stoptimefmt = (
"%H:%M" if order.stoptime.date() == order.starttime.date() "%H:%M" if order.stoptime.date() == order.starttime.date()
else "%Y-%m-%d, %H:%M" else "%Y-%m-%d, %H:%M"
) %} ) %}
{{ order.stoptime.strftime(stoptimefmt) }} ({{ order.stoptime|countdown }}) {{ order.stoptime.strftime(stoptimefmt) }} ({{ order.stoptime|countdown }})
{% else %} {% else %}
Never Never
{% endif %} {% endif %}
</dd> </dd>
</div>
<div>
<dt>Location</dt>
<dd>
{% if order.location %}
<a href="{{ url_for('general_bp.location', location_id=order.location_id) }}">{{ order.location_name }}</a>
{% else %}
{{ order.location_name }}
{% endif %}
</dd>
<dt>Courier</dt>
<dd>
{% if order.courier == None %}
{% if not current_user.is_anonymous() %}
<form action="{{ url_for('order_bp.volunteer', order_slug=order.slug) }}" method="post" style="display:inline">
<input type="submit" class="btn btn-primary btn-sm" value="Volunteer"></input>
</form>
{% else %}No-one yet{% endif %}
{% else %}
{{ order.courier.username }}
{% endif %}
</dd>
</div>
</dl>
<div class="col-md-2 col-lg-4">
<img src="https://dsa.ugent.be/api/verenigingen/{{ order.association }}/logo" class="img-responsive align-top" style="max-width:200px;width:100%">
</div> </div>
<div>
<dt>Location</dt>
<dd>
{% if order.location %}
<a href="{{ url_for('general_bp.location', location_id=order.location_id) }}">{{ order.location_name }}</a>
{% else %}
{{ order.location_name }}
{% endif %}
</dd>
<dt>Courier</dt>
<dd>
{% if order.courier == None %}
{% if not current_user.is_anonymous() %}
<form action="{{ url_for('order_bp.volunteer', order_id=order.id) }}" method="post" style="display:inline">
<input type="submit" class="btn btn-primary btn-sm" value="Volunteer"></input>
</form>
{% else %}No-one yet{% endif %}
{% else %}
{{ order.courier.username }}
{% endif %}
</dd>
</div>
</dl>
<div>
{% if order.can_close(current_user.id) -%}
<form action="{{ url_for('order_bp.close_order', order_id=order.id) }}" method="post" style="display:inline">
<input type="submit" class="btn btn-danger" value="Close"></input>
</form>
{% endif %}
{% if courier_or_admin %}
<a class="btn" href="{{ url_for('order_bp.order_edit', order_id=order.id) }}">Edit</a>
{%- endif %}
</div> </div>
{% if order.can_close(current_user.id) -%}
<form action="{{ url_for('order_bp.close_order', order_slug=order.slug) }}" method="post" style="display:inline">
<input type="submit" class="btn btn-danger" value="Close"></input>
</form>
{% endif %}
{% if courier_or_admin %}
<a class="btn" href="{{ url_for('order_bp.order_edit', order_slug=order.slug) }}">Edit</a>
{%- endif %}
</div> </div>
<div class="box" id="how_to_order"> <div class="box" id="how_to_order">
@ -223,7 +196,7 @@
{% if order.location.telephone %} {% if order.location.telephone %}
<div> <div>
<dt>Telephone</dt> <dt>Telephone</dt>
<dd><a href="tel:{{ order.location.telephone }}">{{ order.location.telephone }}</a></dd> <dd><a href="tel://{{ order.location.telephone }}">{{ order.location.telephone }}</a></dd>
</div> </div>
{% endif %} {% endif %}
@ -285,7 +258,7 @@
<div class="footer"> <div class="footer">
Total {{ order.items.count() }} items — {{ total_price|euro }} Total {{ order.items.count() }} items — {{ total_price|euro }}
&nbsp; &nbsp;
<a class="btn btn-sm" href="{{ url_for('order_bp.items_shop_view', order_slug=order.slug) }}">Shop view</a> <a class="btn btn-sm" href="{{ url_for('order_bp.items_shop_view', order_id=order.id) }}">Shop view</a>
</div> </div>
</div> </div>
</div> </div>
@ -294,7 +267,6 @@
<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>
@ -304,37 +276,35 @@
<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_names" value="{{ user_name }}" <input type="checkbox" name="{{ user_name }}"
{{ "disabled" if not order.can_modify_payment(current_user.id) }}> {{ "disabled" if paid }} style="{{ 'opacity: 0.5' if paid }}">
<span class="price" style="{{ 'opacity: 0.5' if paid }}"> <span class="price">{{ order_items | map(attribute="price") | sum | euro }}</span>
{{ order_items | map(attribute="price") | ignore_none | sum | euro }}
</span>
{% if paid %}<span class="glyphicon glyphicon-ok" style="opacity: 0.5"></span>{% endif %} {% if paid %}paid{% endif %}
</td> </td>
<td style="{{ 'opacity: 0.5' if paid }}">{{ user_name }}</td> <td>{{ 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', '')) -%}
<button class="btn btn-link btn-sm" type="submit" name="delete_item" value="{{ item.id }}" style="padding: 0 0.5em;"><span class="glyphicon glyphicon-remove"></span></button> <form action="{{ url_for('order_bp.delete_item', order_id=order.id, item_id=item.id) }}" method="post" style="display:inline">
<button class="btn btn-link btn-sm" type="submit" style="padding: 0 0.5em;"><span class="glyphicon glyphicon-remove"></span></button>
</form>
{% else %} {% else %}
<span class="glyphicon glyphicon-remove" style="color: var(--gray3); padding: 0 0.5em; cursor: not-allowed"></span> <span class="glyphicon glyphicon-remove" style="color: var(--gray3); padding: 0 0.5em"></span>
{%- endif %} {%- endif %}
</div> </div>
<div class="price_aligned"> <div class="price_aligned">{{ item.price|euro }}</div>
{{ 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>
@ -344,21 +314,11 @@
</table> </table>
<div class="footer"> <div class="footer">
{% if order.can_modify_payment(current_user.id) %}
On selected: On selected:
<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-ok"></span> Mark paid (TODO)</button>
<button name="action" value="mark_unpaid" class="btn btn-sm">Mark unpaid</button> <button class="btn btn-sm"><span class="glyphicon glyphicon-piggy-bank"></span> Tab (TODO)</button>
{% endif %} <button class="btn btn-sm"><span class="glyphicon glyphicon-qrcode"></span> QR code (TODO)</button>
{% if order.can_modify_prices(current_user.id) %}
&nbsp; <span style="border-left: 1px solid var(--gray0); display: inline-block;">&nbsp;</span>&nbsp;
<a href="{{ url_for('order_bp.prices', order_slug=order.slug) }}" class="btn btn-sm">
<span class="glyphicon glyphicon-pencil"></span> Edit prices
</a>
{% endif %}
</div> </div>
</form>
</div> </div>
</section> </section>
{% endblock %} {% endblock %}
@ -453,7 +413,7 @@ dl {
margin: 0.3em 0.5em; margin: 0.3em 0.5em;
} }
.main li { li {
line-height: 1.5; line-height: 1.5;
margin: 0.4em 0; margin: 0.4em 0;
} }

View file

@ -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_slug=order_slug) }}"> <form method="post" action="{{ url_for('.order_edit', order_id=order_id) }}">
{{ 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>

View file

@ -1,132 +0,0 @@
{% 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 %}

View file

@ -14,14 +14,14 @@
{% endfor %} {% endfor %}
{% else %} {% else %}
<h4>No orders available.</h4> <h4>No orders available.</h4>
{% if not current_user.is_anonymous() %} {% if form %}
To create an order, fill in the form on the right. To create an order, fill in the form on the right.
{% else %} {% else %}
Login to create an order, or ask someone else. Login to create an order, or ask someone else.
{% endif %} {% endif %}
{%- endif %} {%- endif %}
</div> </div>
{% if not current_user.is_anonymous() %} {% if form %}
<div class="col-md-push-1 col-md-6"> <div class="col-md-push-1 col-md-6">
<h3>Create new order</h3> <h3>Create new order</h3>
<div class="row darker"> <div class="row darker">
@ -38,11 +38,6 @@
{{ 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') }}

View file

@ -1,18 +1,15 @@
{% macro render_order(order) -%} {% macro render_order(order) -%}
<div class="row order_row"> <div class="row order_row">
<div class="col-md-6 order_data"> <div class="col-md-8 col-lg-9 order_data">
<h5>{{ order.location_name }}</h5> <h5>{{ order.location_name }}</h5>
<b class="amount_of_orders">{{ order.items.count() }} items ordered for {{ order.association }}</b></p> <b class="amount_of_orders">{{ order.items.count() }} orders</b></p>
<p class="time_data"> <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-3"> <div class="col-md-4 col-lg-3 expand_button_wrapper">
<img src="https://dsa.ugent.be/api/verenigingen/{{ order.association }}/logo" class="img-responsive align-bottom" style="max-width:200px;width:100%"> <a class="btn btn-primary btn-block align-bottom expand_button" href="{{ url_for('order_bp.order_from_id', order_id=order.id) }}">Expand</a>
</div>
<div 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 %}

View file

@ -1,31 +1,20 @@
"Script which contains several utils for Haldis" "Script which contains several utils for Haldis"
import re from typing import Iterable
from typing import Iterable, Optional
def euro_string(value: Optional[int], unit="") -> str: def euro_string(value: int) -> 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 f"{unit}{euro}.{cents:02}" return "{}.{:02}".format(euro, cents)
return f"{unit}{euro}" else:
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(
@ -44,5 +33,4 @@ 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)

View file

@ -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] = f"[{arg}]" options[arg] = "[{0}]".format(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(
f"{rule.endpoint:50s} {methods:20s} {url}" "{:50s} {:20s} {}".format(rule.endpoint, methods, url)
) )
output.append(line) output.append(line)

View file

@ -1,26 +1,32 @@
"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
from typing import Optional
import yaml import yaml
from flask import Blueprint, Flask, abort from typing import Optional
from flask import Flask, render_template, make_response
from flask import request, jsonify
from flask import Blueprint, abort
from flask import current_app as app from flask import current_app as app
from flask import (jsonify, make_response, render_template, request, from flask import send_from_directory, url_for
send_from_directory, url_for) from flask_login import login_required
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")) as _stream: with open(os.path.join(os.path.dirname(__file__), "themes.yml"), "r") 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"]
@ -31,12 +37,10 @@ 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( "home.html", orders=get_orders(), recently_closed=recently_closed
((datetime.now() > Order.starttime) & (Order.stoptime > datetime.now()) | (Order.stoptime == None))
), recently_closed=recently_closed
) )
@ -56,7 +60,7 @@ def is_theme_active(theme, now):
return start_datetime <= now <= end_datetime return start_datetime <= now <= end_datetime
raise Exception(f"Unknown theme type {theme_type}") raise Exception("Unknown theme type {}".format(theme_type))
def get_theme_css(theme, options): def get_theme_css(theme, options):
@ -67,18 +71,13 @@ 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 ( assert option in THEME_OPTIONS, f"Theme `{theme_name}` uses undefined option `{option}`"
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 = ( value = chosen_value if chosen_value in possible_values \
chosen_value
if chosen_value in possible_values
else THEME_OPTIONS[option]["_default"] else THEME_OPTIONS[option]["_default"]
)
filename += "_" + value filename += "_" + value
@ -120,15 +119,13 @@ 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( response = make_response(rf'''
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
@ -169,27 +166,25 @@ 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, "name": c[1].name,
"name": c[1].name, "description": c[1].description,
"description": c[1].description, "options": [
"options": [ {
{ "id": o.id,
"id": o.id, "name": o.name,
"name": o.name, "description": o.description,
"description": o.description, "price": o.price,
"price": o.price, "tags": o.tags,
"tags": o.tags, }
} for o in c[1].options
for o in c[1].options ],
], }
} for c in dish.choices
for c in dish.choices ])
]
)
@general_bp.route("/about/") @general_bp.route("/about/")
@ -209,7 +204,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",

View file

@ -1,27 +1,37 @@
"""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 flask import current_app as app
from flask import (Blueprint, abort, flash, redirect, render_template, request,
session, url_for, wrappers)
from flask_login import current_user, login_required
from forms import AnonOrderItemForm, OrderForm, OrderItemForm
from hlds.definitions import location_definition_version, location_definitions
from models import Order, OrderItem, User, db
from notification import post_order_to_webhook
from utils import ignore_none, parse_euro_string
from werkzeug.wrappers import Response from werkzeug.wrappers import Response
# from flask import current_app as app
from flask import (
Blueprint,
abort,
flash,
redirect,
render_template,
request,
session,
url_for,
wrappers,
)
from flask_login import current_user, login_required
from forms import AnonOrderItemForm, OrderForm, OrderItemForm
from models import Order, OrderItem, User, db
from hlds.definitions import location_definitions, location_definition_version
from notification import post_order_to_webhook
from utils import ignore_none
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 current_user.association_list(): if form is None:
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
@ -33,10 +43,7 @@ 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():
@ -46,14 +53,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_slug", order_slug=order.slug)) return redirect(url_for("order_bp.order_from_id", order_id=order.id))
return orders(form=orderForm) return orders(form=orderForm)
@order_bp.route("/<order_slug>") @order_bp.route("/<order_id>")
def order_from_slug(order_slug: str, form: OrderForm = None, dish_id=None) -> str: def order_from_id(order_id: int, form: OrderForm = None, dish_id=None) -> str:
"""Generate order view from id""" "Generate order view from id"
order = Order.query.filter(Order.slug == order_slug).first() order = Order.query.filter(Order.id == order_id).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:
@ -65,8 +72,8 @@ def order_from_slug(order_slug: str, form: OrderForm = None, dish_id=None) -> st
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 or 0 for o in order.items) total_price = sum([o.price for o in order.items])
debts = sum(o.price or 0 for o in order.items if not o.paid) debts = sum([o.price 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
@ -80,44 +87,44 @@ def order_from_slug(order_slug: str, form: OrderForm = None, dish_id=None) -> st
) )
@order_bp.route("/<order_slug>/items") @order_bp.route("/<order_id>/items")
def items_shop_view(order_slug: int) -> str: def items_shop_view(order_id: int) -> str:
"""Generate order items view from id""" "Generate order items view from id"
order = Order.query.filter(Order.slug == order_slug).first() order = Order.query.filter(Order.id == order_id).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 or 0 for o in order.items) total_price = sum([o.price 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_slug>/edit", methods=["GET", "POST"]) @order_bp.route("/<order_id>/edit", methods=["GET", "POST"])
@login_required @login_required
def order_edit(order_slug: str) -> typing.Union[str, Response]: def order_edit(order_id: int) -> typing.Union[str, Response]:
"""Generate order edit view from id""" "Generate order edit view from id"
order = Order.query.filter(Order.slug == order_slug).first() order = Order.query.filter(Order.id == order_id).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)
order_form = OrderForm(obj=order) orderForm = OrderForm(obj=order)
order_form.populate() orderForm.populate()
if order_form.validate_on_submit(): if orderForm.validate_on_submit():
order_form.populate_obj(order) orderForm.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_slug", order_slug=order.slug)) return redirect(url_for("order_bp.order_from_id", order_id=order.id))
return render_template("order_edit.html", form=order_form, order_slug=order.slug) return render_template("order_edit.html", form=orderForm, order_id=order_id)
@order_bp.route("/<order_slug>/create", methods=["GET", "POST"]) @order_bp.route("/<order_id>/create", methods=["GET", "POST"])
def order_item_create(order_slug: str) -> typing.Any: def order_item_create(order_id: int) -> 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 slug""" "Add item to order from id"
current_order = Order.query.filter(Order.slug == order_slug).first() current_order = Order.query.filter(Order.id == order_id).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():
@ -126,14 +133,12 @@ def order_item_create(order_slug: str) -> 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 anymore, adding items is nonsensical # If location doesn't exist any more, 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 = ( dish_id = request.form["dish_id"] if form.is_submitted() else request.args.get("dish")
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():
@ -174,7 +179,7 @@ def order_item_create(order_slug: str) -> typing.Any:
return redirect( return redirect(
url_for( url_for(
"order_bp.order_item_create", "order_bp.order_item_create",
order_slug=current_order.slug, order_id=order_id,
dish=form.dish_id.data, dish=form.dish_id.data,
user_name=user_name, user_name=user_name,
comment=comment, comment=comment,
@ -183,13 +188,14 @@ def order_item_create(order_slug: str) -> typing.Any:
# If the form was not submitted (GET request) or the form had errors: show form again # If 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_slug(current_order.slug, form=form, dish_id=dish_id) return order_from_id(order_id, form=form, dish_id=dish_id)
# Form was submitted and is valid # 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 = current_order.id item.order_id = 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:
@ -224,82 +230,59 @@ def order_item_create(order_slug: str) -> 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_slug", order_slug=order_slug)) return redirect(url_for("order_bp.order_from_id", order_id=order_id))
@order_bp.route("/<order_slug>/modify_items", methods=["POST"]) @order_bp.route("/<order_id>/<user_name>/user_paid", methods=["POST"])
@login_required @login_required
# pylint: disable=R1710 # pylint: disable=R1710
def modify_items(order_slug: str) -> typing.Optional[Response]: def items_user_paid(order_id: int, user_name: str) -> typing.Optional[Response]:
if "delete_item" in request.form: "Indicate payment status for a user in an order"
return delete_item(order_slug, int(request.form["delete_item"])) user = User.query.filter(User.username == user_name).first()
user_names = request.form.getlist("user_names") items: typing.List[OrderItem] = []
if request.form.get("action") == "mark_paid": if user:
return set_items_paid(order_slug, user_names, True) items = OrderItem.query.filter(
elif request.form.get("action") == "mark_unpaid": (OrderItem.user_id == user.id) & (OrderItem.order_id == order_id)
return set_items_paid(order_slug, user_names, False) ).all()
else: else:
abort(404) items = OrderItem.query.filter(
return None (OrderItem.user_name == user_name) & (OrderItem.order_id == order_id)
).all()
def set_items_paid(order_slug: str, user_names: typing.Iterable[str], paid: bool): current_order = Order.query.filter(Order.id == order_id).first()
order = Order.query.filter(Order.slug == order_slug).first() if current_order.courier_id == current_user.id or current_user.admin:
total_paid_items = 0
total_failed_items = 0
for user_name in user_names:
user = User.query.filter(User.username == user_name).first()
items: typing.List[OrderItem] = []
if user:
items = OrderItem.query.filter(
(OrderItem.user_id == user.id) & (OrderItem.order_id == order.id)
).all()
else:
items = OrderItem.query.filter(
(OrderItem.user_name == user_name) & (OrderItem.order_id == order.id)
).all()
for item in items: for item in items:
if item.can_modify_payment(order.id, current_user.id): item.paid = True
if item.paid != paid:
item.paid = paid
total_paid_items += 1
else:
total_failed_items += 1
db.session.commit()
if total_failed_items == 0:
flash("Marked %d items as paid" % (total_paid_items,), "success")
else:
flash("Failed to mark %d items as paid (succeeded in marking %d items as paid)" % (total_failed_items, total_paid_items), "error")
return redirect(url_for("order_bp.order_from_slug", order_slug=order_slug))
@order_bp.route("/<order_slug>/<item_id>/delete", methods=["POST"])
# pylint: disable=R1710
def delete_item(order_slug: str, item_id: int) -> typing.Any:
# type is 'typing.Optional[Response]', but this errors due to
# https://github.com/python/mypy/issues/7187
"""Delete an item from an order"""
item: OrderItem = OrderItem.query.filter(OrderItem.id == item_id).first()
order: Order = Order.query.filter(Order.slug == order_slug).first()
user_id = None
if not current_user.is_anonymous():
user_id = current_user.id
if item.can_delete(order.id, user_id, session.get("anon_name", "")):
dish_name = item.dish_name
db.session.delete(item)
db.session.commit() db.session.commit()
flash("Deleted %s" % dish_name, "success") flash("Paid %d items for %s" % (len(items), item.for_name), "success")
return redirect(url_for("order_bp.order_from_slug", order_slug=order_slug)) return redirect(url_for("order_bp.order_from_id", order_id=order_id))
abort(404) abort(404)
@order_bp.route("/<order_slug>/volunteer", methods=["POST"]) @order_bp.route("/<order_id>/<item_id>/delete", methods=["POST"])
# pylint: disable=R1710
def delete_item(order_id: int, item_id: int) -> typing.Any:
# type is 'typing.Optional[Response]', but this errors due to
# https://github.com/python/mypy/issues/7187
"Delete an item from an order"
item = OrderItem.query.filter(OrderItem.id == item_id).first()
user_id = None
if not current_user.is_anonymous():
user_id = current_user.id
if item.can_delete(order_id, user_id, session.get("anon_name", "")):
dish_name = item.dish_name
db.session.delete(item)
db.session.commit()
flash("Deleted %s" % (dish_name), "success")
return redirect(url_for("order_bp.order_from_id", order_id=order_id))
abort(404)
@order_bp.route("/<order_id>/volunteer", methods=["POST"])
@login_required @login_required
def volunteer(order_slug: str) -> Response: def volunteer(order_id: int) -> Response:
"""Add a volunteer to an order""" "Add a volunteer to an order"
order = Order.query.filter(Order.slug == order_slug).first() order = Order.query.filter(Order.id == order_id).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:
@ -308,14 +291,14 @@ def volunteer(order_slug: str) -> 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_slug", order_slug=order.slug)) return redirect(url_for("order_bp.order_from_id", order_id=order_id))
@order_bp.route("/<order_slug>/close", methods=["POST"]) @order_bp.route("/<order_id>/close", methods=["POST"])
@login_required @login_required
def close_order(order_slug: str) -> typing.Optional[Response]: def close_order(order_id: int) -> typing.Optional[Response]:
"""Close an order""" "Close an order"
order = Order.query.filter(Order.slug == order_slug).first() order = Order.query.filter(Order.id == order_id).first()
if order is None: if order is None:
abort(404) abort(404)
if ( if (
@ -327,54 +310,12 @@ def close_order(order_slug: str) -> 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_slug", order_slug=order_slug)) return redirect(url_for("order_bp.order_from_id", order_id=order_id))
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]
@ -393,20 +334,19 @@ 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) & (Order.association.in_(current_user.association_list())) (expression & (Order.public == True))
).all() ).all()
return order_list return order_list

View file

@ -1,9 +1,10 @@
"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__)

View file

@ -1,20 +1,21 @@
"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, from flask import Blueprint, current_app, flash, redirect, request, session, url_for
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
from models import User, db
from werkzeug.wrappers import Response from werkzeug.wrappers import Response
from models import User, db
oauth_bp = Blueprint("oauth_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("oauth_bp.authorized", _external=True)
)
@oauth_bp.route("/login/zeus/authorized") @oauth_bp.route("/login/zeus/authorized")
@ -24,8 +25,10 @@ def authorized() -> typing.Any:
"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:
# pylint: disable=C0301 return "Access denied: reason=%s error=%s" % (
return f"Access denied: reason={request.args['error']} error={request.args['error_description']}" request.args["error"],
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}"
@ -77,7 +80,7 @@ def login_and_redirect_user(user) -> Response:
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, associations=["zeus"]) user.configure(username, False, 1)
db.session.add(user) db.session.add(user)
db.session.commit() db.session.commit()
return user return user

View file

@ -1,4 +1,4 @@
#!/usr/bin/env bash #!/bin/bash
set -euo pipefail set -euo pipefail
# A simple file to run all instructions from the README # A simple file to run all instructions from the README
## this should be run in the root of the repository ## this should be run in the root of the repository
@ -10,9 +10,8 @@ B="\n${bold}"
E="${normal}" E="${normal}"
if [ ! -d "venv" ]; then if [ ! -d "venv" ]; then
PYTHON_VERSION=$(cat .python-version) echo -e "${B} No venv found, creating a new one ${E}"
echo -e "${B} No venv found, creating a new one with version ${PYTHON_VERSION} ${E}" python3 -m venv venv
python3 -m virtualenv -p $PYTHON_VERSION venv
fi fi
source venv/bin/activate source venv/bin/activate
@ -23,6 +22,14 @@ pip install pip-tools
echo -e "${B} Downloading dependencies ${E}" echo -e "${B} Downloading dependencies ${E}"
pip-sync pip-sync
echo -en "${B} Do you want to install support for the Airbrake API for error logging? If you don't have an Errbit server or Airbrake account, answer no. (y/N) ${E}"
read confirm
if [ "$confirm" = y ]; then
pip install airbrake
else
echo "Not installing airbrake"
fi
if [ ! -f app/config.py ]; then if [ ! -f app/config.py ]; then
echo -e "${B} Copying config template. All custom config options can be set in the config.py file ${E}" echo -e "${B} Copying config template. All custom config options can be set in the config.py file ${E}"
cp app/config.example.py app/config.py cp app/config.example.py app/config.py

View file

@ -1,4 +1,4 @@
#!/usr/bin/env bash #!/bin/bash
set -euo pipefail set -euo pipefail
# args = map(lambda arg: arg if x[0] in "/-" else pwd+arg, sys.argv[1:]) # args = map(lambda arg: arg if x[0] in "/-" else pwd+arg, sys.argv[1:])

View file

@ -1,9 +1,7 @@
#!/usr/bin/env bash #!/bin/bash
set -euo pipefail set -euo pipefail
cd "$(dirname "$0")/app" cd "$(dirname "$0")/app"
cp database/* .
env python create_database.py setup_database ../venv/bin/python create_database.py setup_database
latest_revision=$(env python app.py db heads | sed "s/ (head)$//") rm -f add_* create_database.py muhscheme.txt
echo Stamping db at $latest_revision
env python app.py db stamp $latest_revision

View file

@ -1,2 +0,0 @@
pylint-flask
pylint-flask-sqlalchemy

View file

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

View file

@ -1,5 +1,5 @@
# #
# This file is autogenerated by pip-compile with python 3.9 # This file is autogenerated by pip-compile
# To update, run: # To update, run:
# #
# pip-compile # pip-compile
@ -11,15 +11,11 @@ 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 # via flask-debugtoolbar
# 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 # via requests
# requests
# sentry-sdk
chardet==4.0.0 chardet==4.0.0
# via requests # via requests
click==7.1.2 click==7.1.2
@ -28,19 +24,6 @@ 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
@ -61,6 +44,18 @@ 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
@ -97,12 +92,10 @@ 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 requests-oauthlib
requests-oauthlib==1.1.0 requests-oauthlib==1.1.0
# via flask-oauthlib # via flask-oauthlib
sentry-sdk[flask]==1.10.1 requests==2.25.1
# via -r requirements.in # via requests-oauthlib
six==1.16.0 six==1.16.0
# via python-dateutil # via python-dateutil
sqlalchemy==1.4.18 sqlalchemy==1.4.18
@ -113,10 +106,8 @@ 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.12 urllib3==1.26.5
# via # via requests
# 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