commit
461664f629
28 changed files with 278 additions and 233 deletions
|
@ -5,6 +5,8 @@
|
|||
# run arbitrary code.
|
||||
extension-pkg-whitelist=
|
||||
|
||||
fail-under=9
|
||||
|
||||
# Add files or directories to the blacklist. They should be base names, not
|
||||
# paths.
|
||||
ignore=CVS
|
||||
|
@ -28,7 +30,7 @@ limit-inference-results=100
|
|||
|
||||
# List of plugins (as comma separated values of python modules names) to load,
|
||||
# usually to register additional checkers.
|
||||
load-plugins=
|
||||
load-plugins=pylint_flask_sqlalchemy,pylint_flask
|
||||
|
||||
# Pickle collected data for later comparisons.
|
||||
persistent=yes
|
||||
|
@ -60,7 +62,7 @@ confidence=
|
|||
# --enable=similarities". If you want to run only the classes checker, but have
|
||||
# no Warning level messages displayed, use "--disable=all --enable=classes
|
||||
# --disable=W".
|
||||
disable=E0401,E0611,C0103,W0511,W0611
|
||||
disable=E0401,E0611,C0103,W0511,W0611,C0415
|
||||
|
||||
# Enable the message, report, category or checker with the given id(s). You can
|
||||
# either give multiple identifier separated by comma (,) or put this option
|
||||
|
|
0
app/__init__.py
Normal file
0
app/__init__.py
Normal file
|
@ -1,7 +1,7 @@
|
|||
"""Script for adding users as admin to Haldis."""
|
||||
from models import User
|
||||
|
||||
from app import db
|
||||
from models import User
|
||||
|
||||
|
||||
def add() -> None:
|
||||
|
|
29
app/admin.py
29
app/admin.py
|
@ -3,7 +3,6 @@ from flask import Flask
|
|||
from flask_admin import Admin
|
||||
from flask_admin.contrib.sqla import ModelView
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
|
||||
from models import Order, OrderItem, OrderItemChoice, User
|
||||
|
||||
|
||||
|
@ -26,8 +25,10 @@ class OrderAdminModel(ModelBaseView):
|
|||
column_default_sort = ("starttime", True)
|
||||
column_list = ["starttime", "stoptime", "location_name", "location_id", "courier"]
|
||||
column_labels = {
|
||||
"starttime": "Start Time", "stoptime": "Closing Time",
|
||||
"location_id": "HLDS Location ID"}
|
||||
"starttime": "Start Time",
|
||||
"stoptime": "Closing Time",
|
||||
"location_id": "HLDS Location ID",
|
||||
}
|
||||
form_excluded_columns = ["items", "courier_id"]
|
||||
can_delete = False
|
||||
|
||||
|
@ -36,13 +37,25 @@ class OrderItemAdminModel(ModelBaseView):
|
|||
# pylint: disable=too-few-public-methods
|
||||
column_default_sort = ("order_id", True)
|
||||
column_list = [
|
||||
"order_id", "order.location_name", "user_name", "user", "dish_name", "dish_id", "comment", "price", "paid",
|
||||
"hlds_data_version"
|
||||
"order_id",
|
||||
"order.location_name",
|
||||
"user_name",
|
||||
"user",
|
||||
"dish_name",
|
||||
"dish_id",
|
||||
"comment",
|
||||
"price",
|
||||
"paid",
|
||||
"hlds_data_version",
|
||||
]
|
||||
column_labels = {
|
||||
"order_id": "Order", "order.location_name": "Order's Location",
|
||||
"user_name": "Anon. User", "user_id": "Registered User",
|
||||
"hlds_data_version": "HLDS Data Version", "dish_id": "HLDS Dish ID"}
|
||||
"order_id": "Order",
|
||||
"order.location_name": "Order's Location",
|
||||
"user_name": "Anon. User",
|
||||
"user_id": "Registered User",
|
||||
"hlds_data_version": "HLDS Data Version",
|
||||
"dish_id": "HLDS Dish ID",
|
||||
}
|
||||
|
||||
|
||||
def init_admin(app: Flask, database: SQLAlchemy) -> None:
|
||||
|
|
18
app/app.py
18
app/app.py
|
@ -3,10 +3,11 @@
|
|||
"""Main Haldis script"""
|
||||
|
||||
import logging
|
||||
from logging.handlers import TimedRotatingFileHandler
|
||||
import typing
|
||||
from datetime import datetime
|
||||
from logging.handlers import TimedRotatingFileHandler
|
||||
|
||||
from admin import init_admin
|
||||
from flask import Flask, render_template
|
||||
from flask_bootstrap import Bootstrap, StaticCDN
|
||||
from flask_debugtoolbar import DebugToolbarExtension
|
||||
|
@ -14,10 +15,8 @@ from flask_login import LoginManager
|
|||
from flask_migrate import Migrate, MigrateCommand
|
||||
from flask_oauthlib.client import OAuth, OAuthException
|
||||
from flask_script import Manager, Server
|
||||
from markupsafe import Markup
|
||||
|
||||
from admin import init_admin
|
||||
from login import init_login
|
||||
from markupsafe import Markup
|
||||
from models import db
|
||||
from models.anonymous_user import AnonymouseUser
|
||||
from utils import euro_string, price_range_string
|
||||
|
@ -69,7 +68,8 @@ def register_plugins(app: Flask) -> Manager:
|
|||
|
||||
# Make cookies more secure
|
||||
app.config.update(
|
||||
SESSION_COOKIE_HTTPONLY=True, SESSION_COOKIE_SAMESITE="Lax",
|
||||
SESSION_COOKIE_HTTPONLY=True,
|
||||
SESSION_COOKIE_SAMESITE="Lax",
|
||||
)
|
||||
|
||||
if not app.debug:
|
||||
|
@ -95,11 +95,11 @@ def add_routes(application: Flask) -> None:
|
|||
# import views # TODO convert to blueprint
|
||||
# import views.stats # TODO convert to blueprint
|
||||
|
||||
from views.order import order_bp
|
||||
from views.general import general_bp
|
||||
from views.stats import stats_blueprint
|
||||
from views.debug import debug_bp
|
||||
from login import auth_bp
|
||||
from views.debug import debug_bp
|
||||
from views.general import general_bp
|
||||
from views.order import order_bp
|
||||
from views.stats import stats_blueprint
|
||||
from zeus import oauth_bp
|
||||
|
||||
application.register_blueprint(general_bp, url_prefix="/")
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import add_admins
|
||||
|
||||
from app import db, app_manager
|
||||
from app import app_manager, db
|
||||
|
||||
entry_sets = {
|
||||
"admins": add_admins.add,
|
||||
|
@ -27,7 +27,7 @@ def check_if_overwrite() -> bool:
|
|||
def add_all() -> None:
|
||||
"Add all possible entries in the entry_sets to the database"
|
||||
for entry_set, function in entry_sets.items():
|
||||
print("Adding {}.".format(entry_set))
|
||||
print(f"Adding {entry_set}.")
|
||||
function()
|
||||
|
||||
|
||||
|
@ -45,14 +45,14 @@ def add_to_current() -> None:
|
|||
|
||||
def add_numbers() -> str:
|
||||
return " ".join(
|
||||
["{}({}), ".format(loc, i) for i, loc in enumerate(available)]
|
||||
[f"{loc}({i}), " for i, loc in enumerate(available)]
|
||||
).rstrip(", ")
|
||||
|
||||
while input("Do you still want to add something? (Y/n) ").lower() not in no:
|
||||
print(
|
||||
"What do you want to add? (Use numbers, or A for all, or C for cancel) "
|
||||
)
|
||||
answer = input("Available: {} : ".format(add_numbers()))
|
||||
answer = input(f"Available: {add_numbers()} : ")
|
||||
if answer.lower() == "a":
|
||||
add_all()
|
||||
available = []
|
||||
|
@ -60,7 +60,7 @@ def add_to_current() -> None:
|
|||
pass
|
||||
elif answer.isnumeric() and answer in [str(x) for x in range(len(available))]:
|
||||
answer_index = int(answer)
|
||||
print("Adding {}.".format(available[answer_index]))
|
||||
print(f"Adding {available[answer_index]}.")
|
||||
entry_sets[str(available[answer_index])]()
|
||||
del available[answer_index]
|
||||
else:
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import typing
|
||||
|
||||
from sqlalchemy.sql import desc, func
|
||||
|
||||
from hlds.definitions import location_definitions
|
||||
from hlds.models import Location, Dish
|
||||
from hlds.models import Dish, Location
|
||||
from models import Order, OrderItem, User
|
||||
from sqlalchemy.sql import desc, func
|
||||
|
||||
|
||||
class FatModel:
|
||||
|
|
19
app/forms.py
19
app/forms.py
|
@ -1,25 +1,16 @@
|
|||
"Script for everything form related in Haldis"
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from flask import session, request
|
||||
from flask import request, session
|
||||
from flask_login import current_user
|
||||
from flask_wtf import FlaskForm as Form
|
||||
from wtforms import (
|
||||
DateTimeField,
|
||||
SelectField,
|
||||
SelectMultipleField,
|
||||
StringField,
|
||||
SubmitField,
|
||||
FieldList,
|
||||
validators,
|
||||
)
|
||||
|
||||
from utils import euro_string, price_range_string
|
||||
from hlds.definitions import location_definitions
|
||||
from hlds.models import Location, Dish, Choice
|
||||
from hlds.models import Choice, Dish, Location
|
||||
from models import User
|
||||
from utils import euro_string, price_range_string
|
||||
from wtforms import (DateTimeField, FieldList, SelectField,
|
||||
SelectMultipleField, StringField, SubmitField, validators)
|
||||
|
||||
|
||||
class OrderForm(Form):
|
||||
|
|
|
@ -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.
|
||||
"""
|
||||
|
||||
from .models import Location, Choice, Option
|
||||
from .models import Choice, Location, Option
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
# Import this class to load the standard HLDS definitions
|
||||
|
||||
import subprocess
|
||||
from os import path
|
||||
from typing import List
|
||||
import subprocess
|
||||
from .parser import parse_all_directory
|
||||
from .models import Location
|
||||
|
||||
from .models import Location
|
||||
from .parser import parse_all_directory
|
||||
|
||||
__all__ = ["location_definitions", "location_definition_version"]
|
||||
|
||||
|
|
|
@ -1,26 +1,28 @@
|
|||
#!/usr/bin/env python3
|
||||
# pylint: disable=too-few-public-methods
|
||||
|
||||
from typing import Iterable, List, Tuple, Mapping, Any, Optional
|
||||
from typing import Any, Iterable, List, Mapping, Optional, Tuple
|
||||
|
||||
from utils import euro_string, first
|
||||
|
||||
|
||||
def _format_tags(tags: Iterable[str]) -> str:
|
||||
return " :: {}".format(" ".join(["{" + tag + "}" for tag in tags])) \
|
||||
if tags \
|
||||
else ""
|
||||
# pylint: disable=consider-using-f-string
|
||||
return " :: {}".format(" ".join(["{" + tag + "}"
|
||||
for tag in tags])) if tags else ""
|
||||
|
||||
|
||||
def _format_price(price: int) -> str:
|
||||
return " {}".format(euro_string(price)) if price else ""
|
||||
return f" {euro_string(price)}" if price else ""
|
||||
|
||||
|
||||
def _format_type_and_choice(type_and_choice):
|
||||
type_, choice = type_and_choice
|
||||
return "{} {}".format(type_, choice)
|
||||
return f"{type_} {choice}"
|
||||
|
||||
|
||||
class Option:
|
||||
|
||||
def __init__(self, id_, *, name, description, price, tags):
|
||||
self.id: str = id_
|
||||
self.name: str = name
|
||||
|
@ -29,15 +31,17 @@ class Option:
|
|||
self.tags: List[str] = tags
|
||||
|
||||
def __str__(self):
|
||||
# pylint: disable=consider-using-f-string
|
||||
return "{0.id}: {0.name}{1}{2}{3}".format(
|
||||
self,
|
||||
" -- {}".format(self.description) if self.description else "",
|
||||
f" -- {self.description}" if self.description else "",
|
||||
_format_tags(self.tags),
|
||||
_format_price(self.price),
|
||||
)
|
||||
|
||||
|
||||
class Choice:
|
||||
|
||||
def __init__(self, id_, *, name, description, options):
|
||||
self.id: str = id_
|
||||
self.name: str = name
|
||||
|
@ -48,7 +52,7 @@ class Choice:
|
|||
def __str__(self):
|
||||
return "{0.id}: {0.name}{1}\n\t\t{2}".format(
|
||||
self,
|
||||
" -- {}".format(self.description) if self.description else "",
|
||||
f" -- {self.description}" if self.description else "",
|
||||
"\n\t\t".join(map(str, self.options)),
|
||||
)
|
||||
|
||||
|
@ -57,6 +61,7 @@ class Choice:
|
|||
|
||||
|
||||
class Dish:
|
||||
|
||||
def __init__(self, id_, *, name, description, price, tags, choices):
|
||||
self.id: str = id_
|
||||
self.name: str = name
|
||||
|
@ -70,7 +75,7 @@ class Dish:
|
|||
def __str__(self):
|
||||
return "dish {0.id}: {0.name}{1}{2}{3}\n\t{4}".format(
|
||||
self,
|
||||
" -- {}".format(self.description) if self.description else "",
|
||||
f" -- {self.description}" if self.description else "",
|
||||
_format_tags(self.tags),
|
||||
_format_price(self.price),
|
||||
"\n\t".join(map(_format_type_and_choice, self.choices)),
|
||||
|
@ -86,14 +91,20 @@ class Dish:
|
|||
return sum(
|
||||
f(option.price for option in choice.options)
|
||||
for (choice_type, choice) in self.choices
|
||||
if choice_type == "single_choice"
|
||||
)
|
||||
if choice_type == "single_choice")
|
||||
|
||||
|
||||
class Location:
|
||||
def __init__(
|
||||
self, id_, *, name, dishes, osm=None, address=None, telephone=None, website=None
|
||||
):
|
||||
|
||||
def __init__(self,
|
||||
id_,
|
||||
*,
|
||||
name,
|
||||
dishes,
|
||||
osm=None,
|
||||
address=None,
|
||||
telephone=None,
|
||||
website=None):
|
||||
self.id: str = id_
|
||||
self.name: str = name
|
||||
self.osm: Optional[str] = osm
|
||||
|
@ -107,24 +118,18 @@ class Location:
|
|||
return first(filter(lambda d: d.id == dish_id, self.dishes))
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
"============================\n"
|
||||
"{0.id}: {0.name}"
|
||||
"{1}\n"
|
||||
"============================\n"
|
||||
"\n"
|
||||
"{2}"
|
||||
).format(
|
||||
self,
|
||||
"".join(
|
||||
"\n\t{} {}".format(k, v)
|
||||
for k, v in (
|
||||
("osm", self.osm),
|
||||
("address", self.address),
|
||||
("telephone", self.telephone),
|
||||
("website", self.website),
|
||||
return ("============================\n"
|
||||
"{0.id}: {0.name}"
|
||||
"{1}\n"
|
||||
"============================\n"
|
||||
"\n"
|
||||
"{2}").format(
|
||||
self,
|
||||
"".join(f"\n\t{k} {v}" for k, v in (
|
||||
("osm", self.osm),
|
||||
("address", self.address),
|
||||
("telephone", self.telephone),
|
||||
("website", self.website),
|
||||
) if v is not None),
|
||||
"\n".join(map(str, self.dishes)),
|
||||
)
|
||||
if v is not None
|
||||
),
|
||||
"\n".join(map(str, self.dishes)),
|
||||
)
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
from glob import glob
|
||||
from os import path
|
||||
import itertools
|
||||
from copy import deepcopy
|
||||
from typing import Iterable, List, Union, Tuple
|
||||
from glob import glob
|
||||
from os import path
|
||||
from typing import Iterable, List, Tuple, Union
|
||||
|
||||
from tatsu import parse as tatsu_parse
|
||||
from tatsu.ast import AST
|
||||
from tatsu.exceptions import SemanticError
|
||||
from .models import Location, Choice, Option, Dish
|
||||
from utils import first
|
||||
|
||||
from .models import Choice, Dish, Location, Option
|
||||
|
||||
# TODO Use proper way to get resources, see https://stackoverflow.com/a/10935674
|
||||
with open(path.join(path.dirname(__file__), "hlds.tatsu")) as fh:
|
||||
|
@ -58,14 +59,16 @@ class HldsSemanticActions:
|
|||
option.price += dish.price
|
||||
dish.price = 0
|
||||
dishes = list(dishes)
|
||||
dishes.append(Dish(
|
||||
"custom",
|
||||
name="Vrije keuze",
|
||||
description="Zet wat je wil in comment",
|
||||
price=0,
|
||||
tags=[],
|
||||
choices=[],
|
||||
))
|
||||
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"]}
|
||||
|
||||
|
@ -145,7 +148,7 @@ def parse(menu: str) -> List[Location]:
|
|||
|
||||
|
||||
def parse_file(filename: str) -> List[Location]:
|
||||
with open(filename, "r") as file_handle:
|
||||
with open(filename) as file_handle:
|
||||
return parse(file_handle.read())
|
||||
|
||||
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
"Script for everything related to logging in and out"
|
||||
from flask import Blueprint, abort, redirect, session, url_for
|
||||
from flask_login import current_user, logout_user
|
||||
from werkzeug.wrappers import Response
|
||||
|
||||
from models import User
|
||||
from werkzeug.wrappers import Response
|
||||
from zeus import zeus_login
|
||||
|
||||
auth_bp = Blueprint("auth_bp", __name__)
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
"Script that runs migrations online or offline"
|
||||
from __future__ import with_statement
|
||||
|
||||
from logging.config import fileConfig
|
||||
|
||||
from alembic import context
|
||||
|
||||
# add your model's MetaData object here
|
||||
# for 'autogenerate' support
|
||||
# from myapp import mymodel
|
||||
|
|
|
@ -12,11 +12,11 @@ revision = "9159a6fed021"
|
|||
down_revision = "150252c1cdb1"
|
||||
|
||||
from itertools import chain
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.sql import table, column, text
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from hlds.definitions import location_definitions
|
||||
from sqlalchemy.sql import column, table, text
|
||||
|
||||
LOCATION_LEGACY_TO_HLDS = {
|
||||
2: "blauw_kotje",
|
||||
|
@ -50,71 +50,106 @@ LOCATION_LEGACY_TO_HLDS = {
|
|||
|
||||
def upgrade():
|
||||
# First the simple actions
|
||||
op.create_table("order_item_choice",
|
||||
op.create_table(
|
||||
"order_item_choice",
|
||||
sa.Column("id", sa.Integer, nullable=False),
|
||||
sa.Column("choice_id", sa.String(length=64), nullable=True),
|
||||
sa.Column("order_item_id", sa.Integer, nullable=False),
|
||||
sa.Column("kind", sa.String(length=1), nullable=False),
|
||||
sa.Column("name", sa.String(length=120), nullable=True),
|
||||
sa.Column("value", sa.String(length=120), nullable=True),
|
||||
sa.ForeignKeyConstraint(["order_item_id"], ["order_item.id"], ),
|
||||
sa.PrimaryKeyConstraint("id")
|
||||
sa.ForeignKeyConstraint(
|
||||
["order_item_id"],
|
||||
["order_item.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.add_column(
|
||||
"order_item",
|
||||
sa.Column("hlds_data_version", sa.String(length=40), nullable=True),
|
||||
)
|
||||
op.alter_column(
|
||||
"order", "courrier_id", new_column_name="courier_id", type_=sa.Integer
|
||||
)
|
||||
op.alter_column(
|
||||
"order_item",
|
||||
"extra",
|
||||
new_column_name="comment",
|
||||
existing_type=sa.String(254),
|
||||
type_=sa.Text,
|
||||
)
|
||||
op.alter_column(
|
||||
"order_item", "name", new_column_name="user_name", type_=sa.String(120)
|
||||
)
|
||||
op.add_column("order_item", sa.Column("hlds_data_version", sa.String(length=40), nullable=True))
|
||||
op.alter_column("order", "courrier_id", new_column_name="courier_id", type_=sa.Integer)
|
||||
op.alter_column("order_item", "extra", new_column_name="comment",
|
||||
existing_type=sa.String(254), type_=sa.Text)
|
||||
op.alter_column("order_item", "name", new_column_name="user_name", type_=sa.String(120))
|
||||
|
||||
#----------------------------------------------------------------------------------------------
|
||||
# ----------------------------------------------------------------------------------------------
|
||||
# Migrate historical product data to order items
|
||||
|
||||
# First create the new columns we will populate
|
||||
op.add_column("order_item", sa.Column("dish_id", sa.String(length=64), nullable=True))
|
||||
op.add_column("order_item", sa.Column("dish_name", sa.String(length=120), nullable=True))
|
||||
op.add_column(
|
||||
"order_item", sa.Column("dish_id", sa.String(length=64), nullable=True)
|
||||
)
|
||||
op.add_column(
|
||||
"order_item", sa.Column("dish_name", sa.String(length=120), nullable=True)
|
||||
)
|
||||
op.add_column("order_item", sa.Column("price", sa.Integer(), nullable=True))
|
||||
# Brief, ad-hoc table constructs just for our UPDATE statement, see
|
||||
# https://alembic.sqlalchemy.org/en/latest/ops.html#alembic.operations.Operations.execute
|
||||
order_item = table("order_item",
|
||||
order_item = table(
|
||||
"order_item",
|
||||
column("product_id", sa.Integer),
|
||||
column("dish_id", sa.String),
|
||||
column("dish_name", sa.String),
|
||||
column("price", sa.Integer)
|
||||
column("price", sa.Integer),
|
||||
)
|
||||
# Construct and execute queries
|
||||
op.execute(text("""
|
||||
op.execute(
|
||||
text(
|
||||
"""
|
||||
UPDATE order_item
|
||||
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)"""
|
||||
))
|
||||
)
|
||||
)
|
||||
# Historical product data migrated, drop obsolete column and table
|
||||
op.execute(text("ALTER TABLE order_item DROP FOREIGN KEY order_item_ibfk_3"))
|
||||
op.drop_column("order_item", "product_id")
|
||||
op.drop_table("product")
|
||||
|
||||
#----------------------------------------------------------------------------------------------
|
||||
# ----------------------------------------------------------------------------------------------
|
||||
# Migrate historical location data to orders
|
||||
|
||||
op.execute(text("ALTER TABLE `order` DROP FOREIGN KEY order_ibfk_2"))
|
||||
op.alter_column("order", "location_id", new_column_name="legacy_location_id",
|
||||
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))
|
||||
op.alter_column(
|
||||
"order",
|
||||
"location_id",
|
||||
new_column_name="legacy_location_id",
|
||||
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
|
||||
# https://alembic.sqlalchemy.org/en/latest/ops.html#alembic.operations.Operations.execute
|
||||
order = table("order",
|
||||
order = table(
|
||||
"order",
|
||||
column("legacy_location_id", sa.Integer),
|
||||
column("location_id", sa.String),
|
||||
column("location_name", sa.String)
|
||||
column("location_name", sa.String),
|
||||
)
|
||||
# Construct and execute queries
|
||||
new_location_id = [
|
||||
order.update()
|
||||
.where(order.c.legacy_location_id == old_id)
|
||||
.values(location_id=new_id)
|
||||
.where(order.c.legacy_location_id == old_id)
|
||||
.values(location_id=new_id)
|
||||
for old_id, new_id in LOCATION_LEGACY_TO_HLDS.items()
|
||||
]
|
||||
location_name_from_location = text("""
|
||||
location_name_from_location = text(
|
||||
"""
|
||||
UPDATE `order`
|
||||
SET location_name = (SELECT location.name FROM location
|
||||
WHERE location.id = `order`.legacy_location_id)"""
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
"Script for everything Order related in the database"
|
||||
import typing
|
||||
from datetime import datetime
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
|
||||
from utils import first
|
||||
from hlds.definitions import location_definitions
|
||||
from utils import first
|
||||
|
||||
from .database import db
|
||||
from .user import User
|
||||
|
||||
|
@ -31,9 +32,9 @@ class Order(db.Model):
|
|||
def __repr__(self) -> str:
|
||||
# pylint: disable=R1705
|
||||
if self.location:
|
||||
return "Order %d @ %s" % (self.id, self.location.name or "None")
|
||||
return f"Order {self.id} @ {self.location.name or 'None'}"
|
||||
else:
|
||||
return "Order %d" % (self.id)
|
||||
return f"Order {self.id}"
|
||||
|
||||
def update_from_hlds(self) -> None:
|
||||
"""
|
||||
|
@ -46,19 +47,21 @@ class Order(db.Model):
|
|||
self.location_name = self.location.name
|
||||
|
||||
def for_user(self, anon=None, user=None) -> typing.List:
|
||||
"Get the items for a certain user"
|
||||
return list(
|
||||
filter(
|
||||
(lambda i: i.user == user)
|
||||
if user is not None
|
||||
else (lambda i: i.user_name == anon),
|
||||
self.items
|
||||
self.items,
|
||||
)
|
||||
)
|
||||
|
||||
def group_by_user(self) -> typing.List[typing.Tuple[str, typing.List]]:
|
||||
"Group items of an Order by user"
|
||||
group: typing.Dict[str, typing.List] = dict()
|
||||
group: typing.Dict[str, typing.List] = {}
|
||||
|
||||
# pylint: disable=E1133
|
||||
for item in self.items:
|
||||
if item.for_name not in group:
|
||||
group[item.for_name] = []
|
||||
|
@ -70,12 +73,17 @@ class Order(db.Model):
|
|||
|
||||
return list(sorted(group.items(), key=lambda t: (t[0] or "", t[1] or "")))
|
||||
|
||||
def group_by_dish(self) \
|
||||
-> typing.List[typing.Tuple[str, int, typing.List[typing.Tuple[str, typing.List]]]]:
|
||||
def group_by_dish(
|
||||
self,
|
||||
) -> typing.List[
|
||||
typing.Tuple[str, int, typing.List[typing.Tuple[str, typing.List]]]
|
||||
]:
|
||||
"Group items of an Order by dish"
|
||||
group: typing.Dict[str, typing.Dict[str, typing.List]] = \
|
||||
defaultdict(lambda: defaultdict(list))
|
||||
group: typing.Dict[str, typing.Dict[str, typing.List]] = defaultdict(
|
||||
lambda: defaultdict(list)
|
||||
)
|
||||
|
||||
# pylint: disable=E1133
|
||||
for item in self.items:
|
||||
group[item.dish_name][item.comment].append(item)
|
||||
|
||||
|
@ -87,12 +95,13 @@ class Order(db.Model):
|
|||
sorted(
|
||||
(comment, sorted(items, key=lambda x: (x.for_name or "")))
|
||||
for comment, items in comment_group.items()
|
||||
)
|
||||
),
|
||||
)
|
||||
for dish_name, comment_group in group.items()
|
||||
)
|
||||
|
||||
def is_closed(self) -> bool:
|
||||
"Return whether or not the order is closed"
|
||||
return self.stoptime and datetime.now() > self.stoptime
|
||||
|
||||
def can_close(self, user_id: int) -> bool:
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
"Script for everything OrderItem related in the database"
|
||||
from datetime import datetime
|
||||
|
||||
from utils import first
|
||||
from hlds.definitions import location_definitions
|
||||
from utils import first
|
||||
|
||||
from .database import db
|
||||
from .order import Order
|
||||
from .user import User
|
||||
|
@ -21,20 +22,20 @@ class OrderItem(db.Model):
|
|||
comment = db.Column(db.Text(), nullable=True)
|
||||
hlds_data_version = db.Column(db.String(40), nullable=True)
|
||||
|
||||
choices = db.relationship("OrderItemChoice", backref="order_item", lazy="dynamic")
|
||||
choices = db.relationship("OrderItemChoice",
|
||||
backref="order_item",
|
||||
lazy="dynamic")
|
||||
|
||||
def __getattr__(self, name):
|
||||
if name == "dish":
|
||||
location_id = (
|
||||
Order.query.filter(Order.id == self.order_id).first().location_id
|
||||
)
|
||||
location_id = (Order.query.filter(
|
||||
Order.id == self.order_id).first().location_id)
|
||||
location = first(
|
||||
filter(lambda l: l.id == location_id, location_definitions)
|
||||
)
|
||||
filter(lambda l: l.id == location_id, location_definitions))
|
||||
if location:
|
||||
return first(filter(lambda d: d.id == self.dish_id, location.dishes))
|
||||
else:
|
||||
raise ValueError("No Location found with id: " + location_id)
|
||||
return first(
|
||||
filter(lambda d: d.id == self.dish_id, location.dishes))
|
||||
raise ValueError(f"No Location found with id: {location_id}")
|
||||
raise AttributeError()
|
||||
|
||||
@property
|
||||
|
@ -45,11 +46,7 @@ class OrderItem(db.Model):
|
|||
return self.user_name
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "Order %d: %s wants %s" % (
|
||||
self.order_id or 0,
|
||||
self.for_name,
|
||||
self.dish_name or "None",
|
||||
)
|
||||
return "Order {self.order_id or 0}: {self.for_name} wants {self.dish_name or 'None'}"
|
||||
|
||||
def update_from_hlds(self) -> None:
|
||||
"""
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
"Script for everything OrderItemChoice related in the database"
|
||||
from datetime import datetime
|
||||
|
||||
from .database import db
|
||||
|
@ -5,6 +6,7 @@ from .orderitem import OrderItem
|
|||
|
||||
|
||||
class OrderItemChoice(db.Model):
|
||||
"Class used for configuring the OrderItemChoice model in the database"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
choice_id = db.Column(db.String(64), nullable=True)
|
||||
order_item_id = db.Column(
|
||||
|
@ -16,7 +18,8 @@ class OrderItemChoice(db.Model):
|
|||
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
def configure(self, order: OrderItem) -> None:
|
||||
"Set the orderitem"
|
||||
self.order = order
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "{}: {}".format(self.name, self.value)
|
||||
return f"{self.name}: {self.value}"
|
||||
|
|
|
@ -39,4 +39,4 @@ class User(db.Model):
|
|||
return str(self.id)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "%s" % self.username
|
||||
return f"{self.username}"
|
||||
|
|
|
@ -16,7 +16,7 @@ def webhook_text(order: Order) -> typing.Optional[str]:
|
|||
return None
|
||||
|
||||
if order.courier is not None:
|
||||
# pylint: disable=C0301
|
||||
# pylint: disable=C0301, C0209
|
||||
return "<!channel|@channel> {3} is going to {1}, order <{0}|here>! Deadline in {2} minutes!".format(
|
||||
url_for("order_bp.order_from_id", order_id=order.id, _external=True),
|
||||
order.location_name,
|
||||
|
@ -24,6 +24,7 @@ def webhook_text(order: Order) -> typing.Optional[str]:
|
|||
order.courier.username.title(),
|
||||
)
|
||||
|
||||
# pylint: disable=C0209
|
||||
return "<!channel|@channel> New order for {}. Deadline in {} minutes. <{}|Open here.>".format(
|
||||
order.location_name,
|
||||
remaining_minutes(order.stoptime),
|
||||
|
@ -43,7 +44,7 @@ class WebhookSenderThread(Thread):
|
|||
"Extension of the Thread class, which sends a webhook for the notification"
|
||||
|
||||
def __init__(self, message: str, url: str) -> None:
|
||||
super(WebhookSenderThread, self).__init__()
|
||||
super().__init__()
|
||||
self.message = message
|
||||
self.url = url
|
||||
|
||||
|
@ -64,4 +65,4 @@ def remaining_minutes(value) -> str:
|
|||
if delta.total_seconds() < 0:
|
||||
return "0"
|
||||
minutes = delta.total_seconds() // 60
|
||||
return "%02d" % minutes
|
||||
return f"{minutes:02}"
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
from tatsu.util import asjson
|
||||
from hlds.parser import parse_files
|
||||
|
||||
|
||||
USAGE = """{0} [filename]...
|
||||
Parse HLDS files, print as JSON
|
||||
|
||||
|
|
|
@ -9,9 +9,9 @@ def euro_string(value: int) -> str:
|
|||
"""
|
||||
euro, cents = divmod(value, 100)
|
||||
if cents:
|
||||
return "€ {}.{:02}".format(euro, cents)
|
||||
return f"€ {euro}.{cents:02}"
|
||||
else:
|
||||
return "€ {}".format(euro)
|
||||
return f"€ {euro}"
|
||||
|
||||
|
||||
def price_range_string(price_range, include_upper=False):
|
||||
|
|
|
@ -17,12 +17,12 @@ def list_routes() -> str:
|
|||
for rule in app.url_map.iter_rules():
|
||||
options = {}
|
||||
for arg in rule.arguments:
|
||||
options[arg] = "[{0}]".format(arg)
|
||||
options[arg] = f"[{arg}]"
|
||||
print(rule.endpoint)
|
||||
methods = ",".join(rule.methods)
|
||||
url = url_for(rule.endpoint, **options)
|
||||
line = urllib.parse.unquote(
|
||||
"{:50s} {:20s} {}".format(rule.endpoint, methods, url)
|
||||
f"{rule.endpoint:50s} {methods:20s} {url}"
|
||||
)
|
||||
output.append(line)
|
||||
|
||||
|
|
|
@ -1,32 +1,26 @@
|
|||
"Script to generate the general views of Haldis"
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import yaml
|
||||
from typing import Optional
|
||||
|
||||
from flask import Flask, render_template, make_response
|
||||
from flask import request, jsonify
|
||||
from flask import Blueprint, abort
|
||||
import yaml
|
||||
from flask import Blueprint, Flask, abort
|
||||
from flask import current_app as app
|
||||
from flask import send_from_directory, url_for
|
||||
from flask import (jsonify, make_response, render_template, request,
|
||||
send_from_directory, url_for)
|
||||
from flask_login import login_required
|
||||
|
||||
from utils import first
|
||||
from hlds.definitions import location_definitions
|
||||
from hlds.models import Location
|
||||
from models import Order
|
||||
|
||||
from utils import first
|
||||
# import views
|
||||
from views.order import get_orders
|
||||
|
||||
import json
|
||||
from flask import jsonify
|
||||
|
||||
general_bp = Blueprint("general_bp", __name__)
|
||||
|
||||
|
||||
with open(os.path.join(os.path.dirname(__file__), "themes.yml"), "r") as _stream:
|
||||
with open(os.path.join(os.path.dirname(__file__), "themes.yml")) as _stream:
|
||||
_theme_data = yaml.safe_load(_stream)
|
||||
THEME_OPTIONS = _theme_data["options"]
|
||||
THEMES = _theme_data["themes"]
|
||||
|
@ -37,7 +31,7 @@ def home() -> str:
|
|||
"Generate the home view"
|
||||
prev_day = datetime.now() - timedelta(days=1)
|
||||
recently_closed = get_orders(
|
||||
((Order.stoptime > prev_day) & (Order.stoptime < datetime.now()))
|
||||
(Order.stoptime > prev_day) & (Order.stoptime < datetime.now())
|
||||
)
|
||||
return render_template(
|
||||
"home.html", orders=get_orders(), recently_closed=recently_closed
|
||||
|
@ -60,7 +54,7 @@ def is_theme_active(theme, now):
|
|||
|
||||
return start_datetime <= now <= end_datetime
|
||||
|
||||
raise Exception("Unknown theme type {}".format(theme_type))
|
||||
raise Exception(f"Unknown theme type {theme_type}")
|
||||
|
||||
|
||||
def get_theme_css(theme, options):
|
||||
|
@ -71,13 +65,18 @@ def get_theme_css(theme, options):
|
|||
|
||||
for option in theme.get("options", []):
|
||||
theme_name = theme["name"]
|
||||
assert option in THEME_OPTIONS, f"Theme `{theme_name}` uses undefined option `{option}`"
|
||||
assert (
|
||||
option in THEME_OPTIONS
|
||||
), f"Theme `{theme_name}` uses undefined option `{option}`"
|
||||
|
||||
chosen_value = options[option]
|
||||
possible_values = list(THEME_OPTIONS[option].keys())
|
||||
|
||||
value = chosen_value if chosen_value in possible_values \
|
||||
value = (
|
||||
chosen_value
|
||||
if chosen_value in possible_values
|
||||
else THEME_OPTIONS[option]["_default"]
|
||||
)
|
||||
|
||||
filename += "_" + value
|
||||
|
||||
|
@ -119,13 +118,15 @@ def current_theme_js():
|
|||
themes = get_active_themes()
|
||||
|
||||
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]
|
||||
|
||||
response = make_response(rf'''
|
||||
response = make_response(
|
||||
rf"""
|
||||
var currentTheme = {json.dumps(cur_theme['file'])};
|
||||
var currentThemeOptions = {json.dumps(cur_theme.get('options', []))};
|
||||
''')
|
||||
"""
|
||||
)
|
||||
response.headers["Content-Type"] = "text/javascript"
|
||||
|
||||
# Theme name that is not valid at this moment: delete cookie
|
||||
|
@ -166,25 +167,27 @@ def location_dish(location_id, dish_id) -> str:
|
|||
dish = loc.dish_by_id(dish_id)
|
||||
if dish is None:
|
||||
abort(404)
|
||||
return jsonify([
|
||||
{
|
||||
"type": c[0],
|
||||
"id": c[1].id,
|
||||
"name": c[1].name,
|
||||
"description": c[1].description,
|
||||
"options": [
|
||||
{
|
||||
"id": o.id,
|
||||
"name": o.name,
|
||||
"description": o.description,
|
||||
"price": o.price,
|
||||
"tags": o.tags,
|
||||
}
|
||||
for o in c[1].options
|
||||
],
|
||||
}
|
||||
for c in dish.choices
|
||||
])
|
||||
return jsonify(
|
||||
[
|
||||
{
|
||||
"type": c[0],
|
||||
"id": c[1].id,
|
||||
"name": c[1].name,
|
||||
"description": c[1].description,
|
||||
"options": [
|
||||
{
|
||||
"id": o.id,
|
||||
"name": o.name,
|
||||
"description": o.description,
|
||||
"price": o.price,
|
||||
"tags": o.tags,
|
||||
}
|
||||
for o in c[1].options
|
||||
],
|
||||
}
|
||||
for c in dish.choices
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@general_bp.route("/about/")
|
||||
|
@ -204,7 +207,7 @@ def profile() -> str:
|
|||
def favicon() -> str:
|
||||
"Generate the favicon"
|
||||
# pylint: disable=R1705
|
||||
if not get_orders((Order.stoptime > datetime.now())):
|
||||
if not get_orders(Order.stoptime > datetime.now()):
|
||||
return send_from_directory(
|
||||
os.path.join(app.root_path, "static"),
|
||||
"favicon.ico",
|
||||
|
|
|
@ -3,27 +3,16 @@ import random
|
|||
import typing
|
||||
from datetime import datetime
|
||||
|
||||
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 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 hlds.definitions import location_definitions, location_definition_version
|
||||
from notification import post_order_to_webhook
|
||||
from utils import ignore_none
|
||||
from werkzeug.wrappers import Response
|
||||
|
||||
order_bp = Blueprint("order_bp", "order")
|
||||
|
||||
|
@ -72,8 +61,8 @@ def order_from_id(order_id: int, form: OrderForm = None, dish_id=None) -> str:
|
|||
form.populate(order.location)
|
||||
if order.is_closed():
|
||||
form = None
|
||||
total_price = sum([o.price for o in order.items])
|
||||
debts = sum([o.price for o in order.items if not o.paid])
|
||||
total_price = sum(o.price for o in order.items)
|
||||
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
|
||||
|
||||
|
@ -96,7 +85,7 @@ def items_shop_view(order_id: int) -> str:
|
|||
if current_user.is_anonymous() and not order.public:
|
||||
flash("Please login to see this order.", "info")
|
||||
abort(401)
|
||||
total_price = sum([o.price 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)
|
||||
|
||||
|
||||
|
@ -138,7 +127,9 @@ def order_item_create(order_id: int) -> typing.Any:
|
|||
abort(404)
|
||||
form = AnonOrderItemForm() if current_user.is_anonymous() else OrderItemForm()
|
||||
|
||||
dish_id = request.form["dish_id"] if form.is_submitted() else request.args.get("dish")
|
||||
dish_id = (
|
||||
request.form["dish_id"] if form.is_submitted() else request.args.get("dish")
|
||||
)
|
||||
if dish_id and not location.dish_by_id(dish_id):
|
||||
abort(404)
|
||||
if not form.is_submitted():
|
||||
|
@ -347,6 +338,6 @@ def get_orders(expression=None) -> typing.List[Order]:
|
|||
else:
|
||||
order_list = Order.query.filter(
|
||||
# pylint: disable=C0121
|
||||
(expression & (Order.public == True))
|
||||
expression & (Order.public == True)
|
||||
).all()
|
||||
return order_list
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
"Script to generate the stats related views of Haldis"
|
||||
from fatmodels import FatLocation, FatOrder, FatOrderItem, FatUser
|
||||
from flask import Blueprint
|
||||
from flask import current_app as app
|
||||
from flask import render_template
|
||||
|
||||
from fatmodels import FatLocation, FatOrder, FatOrderItem, FatUser
|
||||
|
||||
stats_blueprint = Blueprint("stats_blueprint", __name__)
|
||||
|
||||
|
||||
|
|
15
app/zeus.py
15
app/zeus.py
|
@ -1,12 +1,12 @@
|
|||
"Script containing everything specific to ZeusWPI"
|
||||
import typing
|
||||
|
||||
from flask import Blueprint, current_app, flash, redirect, request, session, url_for
|
||||
from flask import (Blueprint, current_app, flash, redirect, request, session,
|
||||
url_for)
|
||||
from flask_login import login_user
|
||||
from flask_oauthlib.client import OAuth, OAuthException
|
||||
from werkzeug.wrappers import Response
|
||||
|
||||
from models import User, db
|
||||
from werkzeug.wrappers import Response
|
||||
|
||||
oauth_bp = Blueprint("oauth_bp", __name__)
|
||||
|
||||
|
@ -14,8 +14,7 @@ oauth_bp = Blueprint("oauth_bp", __name__)
|
|||
def zeus_login():
|
||||
"Log in using ZeusWPI"
|
||||
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")
|
||||
|
@ -25,10 +24,8 @@ def authorized() -> typing.Any:
|
|||
"Check authorized status"
|
||||
resp = current_app.zeus.authorized_response()
|
||||
if resp is None:
|
||||
return "Access denied: reason=%s error=%s" % (
|
||||
request.args["error"],
|
||||
request.args["error_description"],
|
||||
)
|
||||
# pylint: disable=C0301
|
||||
return f"Access denied: reason={request.args['error']} error={request.args['error_description']}"
|
||||
if isinstance(resp, OAuthException):
|
||||
return f"Access denied: {resp.message}<br>{resp.data}"
|
||||
|
||||
|
|
2
pylint-requirement.txt
Normal file
2
pylint-requirement.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
pylint-flask
|
||||
pylint-flask-sqlalchemy
|
Loading…
Reference in a new issue