Merge branch 'pluginify' of groeger-clan.duckdns.org:newgeruecht into pluginify

This commit is contained in:
Ferdinand Thiessen 2021-03-19 02:04:04 +01:00
commit d6475604e9
15 changed files with 567 additions and 168 deletions

View File

@ -14,8 +14,10 @@ class ModelSerializeMixin:
return False return False
import typing import typing
if typing.get_origin(self.__class__.__annotations__[param]) is typing.Union and \
typing.get_args(self.__class__.__annotations__[param])[1] is None: if typing.get_origin(self.__class__.__annotations__[param]) is typing.Union and typing.get_args(
self.__class__.__annotations__[param]
)[1] is type(None):
return getattr(self, param) is None return getattr(self, param) is None
def serialize(self): def serialize(self):

View File

@ -1,3 +1,5 @@
from __future__ import annotations # TODO: Remove if python requirement is >= 3.10
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from . import ModelSerializeMixin, UtcDateTime from . import ModelSerializeMixin, UtcDateTime

View File

@ -1,3 +1,5 @@
from __future__ import annotations # TODO: Remove if python requirement is >= 3.10
from ..database import db from ..database import db

View File

@ -1,3 +1,5 @@
from __future__ import annotations # TODO: Remove if python requirement is >= 3.10
from flask import url_for from flask import url_for
from typing import Optional from typing import Optional
from datetime import date, datetime from datetime import date, datetime
@ -6,6 +8,7 @@ from sqlalchemy.orm.collections import attribute_mapped_collection
from ..database import db from ..database import db
from . import ModelSerializeMixin, UtcDateTime from . import ModelSerializeMixin, UtcDateTime
association_table = db.Table( association_table = db.Table(
"user_x_role", "user_x_role",
db.Column("user_id", db.Integer, db.ForeignKey("user.id")), db.Column("user_id", db.Integer, db.ForeignKey("user.id")),
@ -30,7 +33,7 @@ class Role(db.Model, ModelSerializeMixin):
__tablename__ = "role" __tablename__ = "role"
id: int = db.Column(db.Integer, primary_key=True) id: int = db.Column(db.Integer, primary_key=True)
name: str = db.Column(db.String(30), unique=True) name: str = db.Column(db.String(30), unique=True)
permissions: [Permission] = db.relationship("Permission", secondary=role_permission_association_table) permissions: list[Permission] = db.relationship("Permission", secondary=role_permission_association_table)
class User(db.Model, ModelSerializeMixin): class User(db.Model, ModelSerializeMixin):
@ -55,11 +58,11 @@ class User(db.Model, ModelSerializeMixin):
lastname: str = db.Column(db.String(50), nullable=False) lastname: str = db.Column(db.String(50), nullable=False)
mail: str = db.Column(db.String(60), nullable=False) mail: str = db.Column(db.String(60), nullable=False)
birthday: Optional[date] = db.Column(db.Date) birthday: Optional[date] = db.Column(db.Date)
roles: [str] = [] roles: list[str] = []
permissions: Optional[type([str])] = None permissions: Optional[list[str]] = None
avatar_url: Optional[str] = "" avatar_url: Optional[str] = ""
roles_: [Role] = db.relationship("Role", secondary=association_table, cascade="save-update, merge") roles_: list[Role] = db.relationship("Role", secondary=association_table, cascade="save-update, merge")
_id = db.Column("id", db.Integer, primary_key=True) _id = db.Column("id", db.Integer, primary_key=True)
_sessions = db.relationship("Session", back_populates="_user") _sessions = db.relationship("Session", back_populates="_user")
_attributes = db.relationship( _attributes = db.relationship(

View File

@ -62,9 +62,11 @@ class Plugin:
Value stored in database (native python) Value stored in database (native python)
""" """
try: try:
setting = _PluginSetting.query\ setting = (
.filter(_PluginSetting.plugin == self._plugin_name)\ _PluginSetting.query.filter(_PluginSetting.plugin == self._plugin_name)
.filter(_PluginSetting.name == name).one() .filter(_PluginSetting.name == name)
.one()
)
return setting.value return setting.value
except sqlalchemy.orm.exc.NoResultFound: except sqlalchemy.orm.exc.NoResultFound:
if "default" in kwargs: if "default" in kwargs:
@ -78,9 +80,11 @@ class Plugin:
name: String identifying the setting name: String identifying the setting
value: Value to be stored value: Value to be stored
""" """
setting = _PluginSetting.query \ setting = (
.filter(_PluginSetting.plugin == self._plugin_name) \ _PluginSetting.query.filter(_PluginSetting.plugin == self._plugin_name)
.filter(_PluginSetting.name == name).one_or_none() .filter(_PluginSetting.name == name)
.one_or_none()
)
if setting is not None: if setting is not None:
setting.value = value setting.value = value
else: else:

View File

@ -23,10 +23,12 @@ class AuthPlain(AuthPlugin):
self.modify_user(admin, None, "admin") self.modify_user(admin, None, "admin")
db.session.add(admin) db.session.add(admin)
db.session.commit() db.session.commit()
logger.warning("New administrator user was added, please change the password or remove it before going into" logger.warning(
"New administrator user was added, please change the password or remove it before going into"
"production mode. Initial credentials:\n" "production mode. Initial credentials:\n"
"name: admin\n" "name: admin\n"
"password: admin") "password: admin"
)
def login(self, user: User, password: str): def login(self, user: User, password: str):
if user.has_attribute("password"): if user.has_attribute("password"):

View File

@ -1,6 +1,7 @@
from __future__ import annotations # TODO: Remove if python requirement is >= 3.10
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional
from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.ext.hybrid import hybrid_property
from flaschengeist.database import db from flaschengeist.database import db

View File

@ -5,10 +5,13 @@ from http.client import NO_CONTENT
from flaschengeist.plugins import Plugin from flaschengeist.plugins import Plugin
from flaschengeist.utils.decorators import login_required from flaschengeist.utils.decorators import login_required
from werkzeug.exceptions import BadRequest from werkzeug.exceptions import BadRequest, Forbidden
from . import models from . import models
from . import pricelist_controller, permissions from . import pricelist_controller, permissions
from ...controller import userController
from ...models.session import Session
from ...utils.HTTP import no_content
pricelist_bp = Blueprint("pricelist", __name__, url_prefix="/pricelist") pricelist_bp = Blueprint("pricelist", __name__, url_prefix="/pricelist")
@ -120,8 +123,135 @@ def search_drinks(name):
@login_required(permission=permissions.CREATE) @login_required(permission=permissions.CREATE)
def create_drink(current_session): def create_drink(current_session):
data = request.get_json() data = request.get_json()
if not all(item in data for item in ["name", "volume", "cost_price"]) or not all( return jsonify(pricelist_controller.set_drink(data))
item in data for item in ["name", "ingredients"]
):
raise BadRequest("No correct Keys to create drink") @pricelist_bp.route("/drinks/<int:identifier>", methods=["PUT"])
return jsonify(pricelist_controller.create_drink(data)) def update_drink(identifier):
data = request.get_json()
return jsonify(pricelist_controller.update_drink(identifier, data))
@pricelist_bp.route("/drinks/<int:identifier>", methods=["DELETE"])
def delete_drink(identifier):
pricelist_controller.delete_drink(identifier)
return "", NO_CONTENT
@pricelist_bp.route("/prices", methods=["GET"])
@pricelist_bp.route("/prices/<int:identifier>", methods=["GET"])
def get_prices(identifier=None):
if identifier:
result = pricelist_controller.get_price(identifier)
else:
result = pricelist_controller.get_prices()
return jsonify(result)
@pricelist_bp.route("/volumes/<int:identifier>/prices", methods=["POST"])
def create_price(identifier):
data = request.get_json()
return jsonify(pricelist_controller.set_price(identifier, data))
@pricelist_bp.route("/prices/<int:identifier>", methods=["PUT"])
def modify_price(identifier):
data = request.get_json()
return jsonify(pricelist_controller.update_price(identifier, data))
@pricelist_bp.route("/prices/<int:identifier>", methods=["DELETE"])
def delete_price(identifier):
pricelist_controller.delete_price(identifier)
return "", NO_CONTENT
@pricelist_bp.route("/drinks/<int:identifier>/volumes", methods=["POST"])
def set_volume(identifier):
data = request.get_json()
return jsonify(pricelist_controller.set_volume(identifier, data))
@pricelist_bp.route("/volumes/<int:identifier>/ingredients", methods=["POST"])
def set_ingredient(identifier):
data = request.get_json()
return jsonify(pricelist_controller.set_ingredient(data, identifier))
@pricelist_bp.route("/volumes/<int:identifier>", methods=["PUT"])
def update_volume(identifier):
data = request.get_json()
return jsonify(pricelist_controller.update_volume(identifier, data))
@pricelist_bp.route("/volumes/<int:identifier>", methods=["DELETE"])
def delete_volume(identifier):
pricelist_controller.delete_volume(identifier)
return "", NO_CONTENT
@pricelist_bp.route("/ingredients/extraIngredients", methods=["GET"])
def get_extraIngredients():
return jsonify(pricelist_controller.get_extra_ingredients())
@pricelist_bp.route("/ingredients/<int:identifier>", methods=["PUT"])
def update_ingredient(identifier):
data = request.get_json()
return jsonify(pricelist_controller.update_ingredient(identifier, data))
@pricelist_bp.route("/ingredients/<int:identifier>", methods=["DELETE"])
def delete_ingredient(identifier):
pricelist_controller.delete_ingredient(identifier)
return "", NO_CONTENT
@pricelist_bp.route("/ingredients/extraIngredients", methods=["POST"])
def set_extra_ingredient():
data = request.get_json()
return jsonify(pricelist_controller.set_extra_ingredient(data))
@pricelist_bp.route("/ingredients/extraIngredients/<int:identifier>", methods=["PUT"])
def update_extra_ingredient(identifier):
data = request.get_json()
return jsonify(pricelist_controller.update_extra_ingredient(identifier, data))
@pricelist_bp.route("/ingredients/extraIngredients/<int:identifier>", methods=["DELETE"])
def delete_extra_ingredient(identifier):
pricelist_controller.delete_extra_ingredient(identifier)
return "", NO_CONTENT
@pricelist_bp.route("/users/<userid>/pricecalc_columns", methods=["GET", "PUT"])
@login_required()
def get_columns(userid, current_session: Session):
"""Get pricecalc_columns of an user
Route: ``/users/<userid>/pricelist/pricecac_columns`` | Method: ``GET`` or ``PUT``
POST-data: On ``PUT`` json encoded array of floats
Args:
userid: Userid identifying the user
current_session: Session sent with Authorization Header
Returns:
GET: JSON object containing the shortcuts as float array or HTTP error
PUT: HTTP-created or HTTP error
"""
if userid != current_session._user.userid:
raise Forbidden
user = userController.get_user(userid)
if request.method == "GET":
return jsonify(user.get_attribute("pricecalc_columns", []))
else:
data = request.get_json()
if not isinstance(data, list) or not all(isinstance(n, str) for n in data):
raise BadRequest
data.sort(reverse=True)
user.set_attribute("pricecalc_columns", data)
userController.persist()
return no_content()

View File

@ -1,7 +1,9 @@
from __future__ import annotations # TODO: Remove if python requirement is >= 3.10
from flaschengeist.database import db from flaschengeist.database import db
from flaschengeist.models import ModelSerializeMixin from flaschengeist.models import ModelSerializeMixin
from typing import Optional from typing import Optional, Union
drink_tag_association = db.Table( drink_tag_association = db.Table(
"drink_x_tag", "drink_x_tag",
@ -43,28 +45,82 @@ class DrinkPrice(db.Model, ModelSerializeMixin):
__tablename__ = "drink_price" __tablename__ = "drink_price"
id: int = db.Column("id", db.Integer, primary_key=True) id: int = db.Column("id", db.Integer, primary_key=True)
volume: float = db.Column(db.Numeric(precision=5, scale=2, asdecimal=False), nullable=False)
price: float = db.Column(db.Numeric(precision=5, scale=2, asdecimal=False)) price: float = db.Column(db.Numeric(precision=5, scale=2, asdecimal=False))
drink_id = db.Column("drink_id", db.Integer, db.ForeignKey("drink.id")) volume_id_ = db.Column("volume_id", db.Integer, db.ForeignKey("drink_price_volume.id"))
drink = db.relationship("Drink", back_populates="prices") volume = db.relationship("DrinkPriceVolume", back_populates="prices")
no_auto: bool = db.Column(db.Boolean, default=False)
public: bool = db.Column(db.Boolean, default=True) public: bool = db.Column(db.Boolean, default=True)
description: Optional[str] = db.Column(db.String(30)) description: Optional[str] = db.Column(db.String(30))
round_step: float = db.Column(db.Numeric(precision=3, scale=2, asdecimal=False), nullable=False, default=0.5)
class Ingredient(db.Model, ModelSerializeMixin): class ExtraIngredient(db.Model, ModelSerializeMixin):
""" """
Drink Build ExtraIngredient
"""
__tablename__ = "extra_ingredient"
id: int = db.Column("id", db.Integer, primary_key=True)
name: str = db.Column(db.String(30), unique=True, nullable=False)
price: float = db.Column(db.Numeric(precision=5, scale=2, asdecimal=False))
class DrinkIngredient(db.Model, ModelSerializeMixin):
"""
Drink Ingredient
""" """
__tablename__ = "drink_ingredient" __tablename__ = "drink_ingredient"
id: int = db.Column("id", db.Integer, primary_key=True) id: int = db.Column("id", db.Integer, primary_key=True)
volume: float = db.Column(db.Numeric(precision=5, scale=2, asdecimal=False), nullable=False) volume: float = db.Column(db.Numeric(precision=5, scale=2, asdecimal=False), nullable=False)
drink_parent_id: int = db.Column("drink_parent_id", db.Integer, db.ForeignKey("drink.id"))
drink_parent = db.relationship("Drink", foreign_keys=drink_parent_id)
drink_ingredient_id: int = db.Column("drink_ingredient_id", db.Integer, db.ForeignKey("drink.id")) drink_ingredient_id: int = db.Column("drink_ingredient_id", db.Integer, db.ForeignKey("drink.id"))
drink_ingredient: "Drink" = db.relationship("Drink", foreign_keys=drink_ingredient_id) # drink_ingredient: Drink = db.relationship("Drink")
# price: float = 0
# @property
# def price(self):
# try:
# return self.drink_ingredient.cost_price_pro_volume * self.volume
# except AttributeError:
# pass
class Ingredient(db.Model, ModelSerializeMixin):
"""
Ingredient Associationtable
"""
__tablename__ = "ingredient_association"
id: int = db.Column("id", db.Integer, primary_key=True)
volume_id = db.Column(db.Integer, db.ForeignKey("drink_price_volume.id"))
drink_ingredient_id = db.Column(db.Integer, db.ForeignKey("drink_ingredient.id"))
drink_ingredient: Optional[DrinkIngredient] = db.relationship(DrinkIngredient)
extra_ingredient_id = db.Column(db.Integer, db.ForeignKey("extra_ingredient.id"))
extra_ingredient: Optional[ExtraIngredient] = db.relationship(ExtraIngredient)
class MinPrices(ModelSerializeMixin):
"""
MinPrices
"""
percentage: float
price: float
class DrinkPriceVolume(db.Model, ModelSerializeMixin):
"""
Drink Volumes and Prices
"""
__tablename__ = "drink_price_volume"
id: int = db.Column("id", db.Integer, primary_key=True)
drink_id = db.Column(db.Integer, db.ForeignKey("drink.id"), nullable=False)
volume: float = db.Column(db.Numeric(precision=5, scale=2, asdecimal=False))
min_prices: list[MinPrices] = []
# ingredients: list[Ingredient] = []
prices: list[DrinkPrice] = db.relationship(DrinkPrice, back_populates="volume", cascade="all,delete,delete-orphan")
ingredients: list[Ingredient] = db.relationship("Ingredient", foreign_keys=Ingredient.volume_id)
class Drink(db.Model, ModelSerializeMixin): class Drink(db.Model, ModelSerializeMixin):
@ -74,17 +130,15 @@ class Drink(db.Model, ModelSerializeMixin):
__tablename__ = "drink" __tablename__ = "drink"
id: int = db.Column("id", db.Integer, primary_key=True) id: int = db.Column("id", db.Integer, primary_key=True)
article_id: Optional[str] = db.Column(db.String(64))
package_size: Optional[int] = db.Column(db.Integer)
name: str = db.Column(db.String(60), nullable=False) name: str = db.Column(db.String(60), nullable=False)
volume: float = db.Column(db.Numeric(precision=5, scale=2, asdecimal=False)) volume: Optional[float] = db.Column(db.Numeric(precision=5, scale=2, asdecimal=False))
cost_price: float = db.Column(db.Numeric(precision=5, scale=2, asdecimal=False)) cost_price_pro_volume: Optional[float] = db.Column(db.Numeric(precision=5, scale=3, asdecimal=False))
discount: float = db.Column(db.Numeric(precision=3, scale=2, asdecimal=False), nullable=False) cost_price_package_netto: Optional[float] = db.Column(db.Numeric(precision=5, scale=3, asdecimal=False))
extra_charge: Optional[float] = db.Column(db.Numeric(precision=3, scale=2, asdecimal=False), default=0)
prices: [DrinkPrice] = db.relationship( _type_id = db.Column("type_id", db.Integer, db.ForeignKey("drink_type.id"))
"DrinkPrice", back_populates="drink", cascade="all,delete,delete-orphan", order_by=[DrinkPrice.volume]
) tags: Optional[list[Tag]] = db.relationship("Tag", secondary=drink_tag_association, cascade="save-update, merge")
ingredients: [Ingredient] = db.relationship( type: Optional[DrinkType] = db.relationship("DrinkType", foreign_keys=[_type_id])
"Ingredient", back_populates="drink_parent", foreign_keys=Ingredient.drink_parent_id volumes: list[DrinkPriceVolume] = db.relationship(DrinkPriceVolume)
)
tags: [Optional[Tag]] = db.relationship("Tag", secondary=drink_tag_association, cascade="save-update, merge")
type_id_ = db.Column("type_id", db.Integer, db.ForeignKey("drink_type.id"))
type = db.relationship("DrinkType")

View File

@ -3,7 +3,7 @@ from sqlalchemy.exc import IntegrityError
from flaschengeist import logger from flaschengeist import logger
from flaschengeist.database import db from flaschengeist.database import db
from .models import Drink, DrinkPrice, Ingredient, Tag, DrinkType from .models import Drink, DrinkPrice, Ingredient, Tag, DrinkType, DrinkPriceVolume, DrinkIngredient, ExtraIngredient
from math import ceil from math import ceil
@ -103,56 +103,6 @@ def delete_drink_type(identifier):
raise BadRequest("DrinkType still in use") raise BadRequest("DrinkType still in use")
def round_price(price, round_step):
return round(ceil(float(price) / round_step) * round_step * 100) / 100
def calc_prices(drink, prices):
retVal = []
if len(drink.ingredients) > 0:
return calc_price_by_ingredients(drink, prices)
allowed_keys = DrinkPrice().serialize().keys()
for price in prices:
values = {key: value for key, value in price.items() if key in allowed_keys}
if values.get("no_auto"):
retVal.append(DrinkPrice(**values))
else:
volume = float(values.pop("volume"))
if "price" in values:
values.pop("price")
_price = float(drink.cost_price) / float(drink.volume) * volume
_price += _price * float(drink.discount)
if drink.extra_charge:
_price += float(drink.extra_charge)
_price = round_price(_price, float(price.get("round_step")))
retVal.append(DrinkPrice(volume=volume, price=_price, **values))
return retVal
def calc_price_by_ingredients(drink, prices):
allowed_keys = DrinkPrice().serialize().keys()
retVal = []
for price in prices:
values = {key: value for key, value in price.items() if key in allowed_keys}
if values.get("no_auto"):
retVal.append(DrinkPrice(**values))
else:
volume = float(values.pop("volume"))
if "price" in values:
values.pop("price")
_price = 0
for ingredient in drink.ingredients:
_price = (
float(ingredient.drink_ingredient.cost_price)
/ float(ingredient.drink_ingredient.volume)
* float(ingredient.volume)
)
_price += _price * float(drink.discount) + float(drink.extra_charge)
_price = round_price(_price, price.get("round_step"))
retVal.append(DrinkPrice(volume=volume, price=_price, **values))
return retVal
def get_drinks(name=None): def get_drinks(name=None):
if name: if name:
return Drink.query.filter(Drink.name.contains(name)).all() return Drink.query.filter(Drink.name.contains(name)).all()
@ -161,49 +111,264 @@ def get_drinks(name=None):
def get_drink(identifier): def get_drink(identifier):
if isinstance(identifier, int): if isinstance(identifier, int):
retVal = Drink.query.get(identifier) return Drink.query.get(identifier)
elif isinstance(identifier, str): elif isinstance(identifier, str):
retVal = Drink.query.filter(Tag.name == identifier).one_or_none() return Drink.query.filter(Tag.name == identifier).one_or_none()
else: else:
logger.debug("Invalid identifier type for Drink") logger.debug("Invalid identifier type for Drink")
raise BadRequest raise BadRequest
if not retVal:
raise NotFound raise NotFound
return retVal
def add_prices(drink, prices): def set_drink(data):
for price in prices: allowedKeys = Drink().serialize().keys()
drink.prices.append(price) if "id" in data:
data.pop("id")
values = {key: value if value != "" else None for key, value in data.items() if key in allowedKeys}
def add_ingredients(drink, ingredients): if "volumes" in values:
for identifier, volume in ingredients: values.pop("volumes")
ingredient = Ingredient(volume=volume, drink_ingredient=get_drink(identifier)) if "tags" in values:
drink.ingredients.append(ingredient) values.pop("tags")
if "type" in values:
_type = values.pop("type")
def create_drink(data): if isinstance(_type, dict) and "id" in _type:
allowed_keys = Drink().serialize().keys() type = get_drink_type(_type.get("id"))
values = {key: value for key, value in data.items() if key in allowed_keys}
prices = values.pop("prices", [])
ingredients = values.pop("ingredients", [])
if "id" in values:
values.pop("id")
drink = Drink(**values) drink = Drink(**values)
add_ingredients(drink, ingredients) if type:
drink.prices = calc_prices(drink, prices) drink.type = type
db.session.add(drink) db.session.add(drink)
update() db.session.commit()
return drink
def update_drink(identifier, data):
allowedKeys = Drink().serialize().keys()
if "id" in data:
data.pop("id")
values = {key: value for key, value in data.items() if key in allowedKeys}
if "volumes" in values:
values.pop("volumes")
if "tags" in values:
values.pop("tags")
if "type" in values:
_type = values.pop("type")
if isinstance(_type, dict) and "id" in _type:
type = get_drink_type(_type.get("id"))
drink = get_drink(identifier)
for key, value in values.items():
setattr(drink, key, value if value != "" else None)
if type:
drink.type = type
db.session.commit()
return drink return drink
def delete_drink(identifier): def delete_drink(identifier):
drink = get_drink(identifier) drink = get_drink(identifier)
for price in drink.prices:
db.session.delete(price)
for ingredient in drink.ingredients:
db.session.delete(ingredient)
db.session.delete(drink) db.session.delete(drink)
update() db.session.commit()
def get_volume(identifier):
return DrinkPriceVolume.query.get(identifier)
def get_volumes(drink_id=None):
if drink_id:
return DrinkPriceVolume.query.filter(DrinkPriceVolume.drink_id == drink_id).all()
return DrinkPriceVolume.query.all()
def set_volume(identifier, data):
allowed_keys = DrinkPriceVolume().serialize().keys()
if "id" in data:
data.pop("id")
values = {key: value for key, value in data.items() if key in allowed_keys}
if "prices" in values:
prices = values.pop("prices")
if "ingredients" in values:
ingredients = values.pop("ingredients")
volume = DrinkPriceVolume(**values)
drink = get_drink(identifier)
if not drink:
raise BadRequest
drink.volumes.append(volume)
db.session.add(volume)
db.session.commit()
return volume
def update_volume(identifier, data):
allowed_keys = DrinkPriceVolume().serialize().keys()
if "id" in data:
data.pop("id")
values = {key: value for key, value in data.items() if key in allowed_keys}
if "prices" in values:
prices = values.pop("prices")
if "ingredients" in values:
ingredients = values.pop("ingredients")
volume = get_volume(identifier)
for key, value in values.items():
setattr(volume, key, value if value != "" else None)
db.session.commit()
return volume
def delete_volume(identifier):
volume = get_volume(identifier)
db.session.delete(volume)
db.session.commit()
def get_price(identifier):
if isinstance(identifier, int):
return DrinkPrice.query.get(identifier)
raise NotFound
def get_prices(volume_id=None):
if volume_id:
return DrinkPrice.query.filter(DrinkPrice.volume_id_ == volume_id).all()
return DrinkPrice.query.all()
def set_price(identifier, data):
allowed_keys = DrinkPrice().serialize().keys()
if "id" in data:
data.pop("id")
values = {key: value for key, value in data.items() if key in allowed_keys}
price = DrinkPrice(**values)
volume = get_volume(identifier)
if not volume:
raise BadRequest
volume.prices.append(price)
db.session.add(price)
db.session.commit()
return price
def update_price(identifier, data):
allowed_keys = DrinkPrice().serialize().keys()
if "id" in data:
data.pop("id")
values = {key: value for key, value in data.items() if key in allowed_keys}
price = get_price(identifier)
for key, value in values.items():
setattr(price, key, value)
db.session.commit()
return price
def delete_price(identifier):
price = get_price(identifier)
db.session.delete(price)
db.session.commit()
def set_drink_ingredient(data):
allowedKeys = DrinkIngredient().serialize().keys()
if "id" in data:
data.pop("id")
drink = None
if "drink_ingredient" in data:
drink_ingredient_ = data.pop("drink_ingredient")
if "id" in drink_ingredient_:
drink = get_drink(drink_ingredient_.get("id"))
values = {key: value for key, value in data.items() if key in allowedKeys}
if "price" in values:
values.pop("price")
drink_ingredient = DrinkIngredient(**values)
if drink:
drink_ingredient.drink_ingredient = drink
db.session.add(drink_ingredient)
db.session.commit()
return drink_ingredient
def get_ingredient(identifier):
return Ingredient.query.get(identifier)
def set_ingredient(data, volume_id):
allowedKeys = Ingredient().serialize().keys()
if "id" in data:
data.pop("id")
values = {key: value for key, value in data.items() if key in allowedKeys}
drink_ingredient_value = None
extra_ingredient_value = None
if "drink_ingredient" in values:
drink_ingredient_value = values.pop("drink_ingredient")
if "extra_ingredient" in values:
extra_ingredient_value = values.pop("extra_ingredient")
ingredient = Ingredient(**values)
volume = get_volume(volume_id)
if drink_ingredient_value:
ingredient.drink_ingredient = set_drink_ingredient(drink_ingredient_value)
if extra_ingredient_value:
if "id" in extra_ingredient_value:
ingredient.extra_ingredient = get_extra_ingredient(extra_ingredient_value.get("id"))
volume.ingredients.append(ingredient)
db.session.add(ingredient)
db.session.commit()
return ingredient
def update_ingredient(identifier, data):
ingredient = get_ingredient(identifier)
if "extra_ingredient" in data and isinstance(data.get("extra_ingredient"), dict):
if "id" in data.get("extra_ingredient"):
ingredient.extra_ingredient = get_extra_ingredient(data.get("extra_ingredient").get("id"))
if "drink_ingredient" in data and ingredient.drink_ingredient:
if data.get("drink_ingredient").get("drink_ingredient_id") > -1:
ingredient.drink_ingredient.drink_ingredient_id = data.get("drink_ingredient").get("drink_ingredient_id")
if "volume" in data.get("drink_ingredient"):
ingredient.drink_ingredient.volume = data.get("drink_ingredient").get("volume")
db.session.commit()
return ingredient
def delete_ingredient(identifier):
ingredient = get_ingredient(identifier)
if ingredient.drink_ingredient:
db.session.delete(ingredient.drink_ingredient)
db.session.delete(ingredient)
db.session.commit()
def get_extra_ingredients():
return ExtraIngredient.query.all()
def get_extra_ingredient(identifier):
return ExtraIngredient.query.get(identifier)
def set_extra_ingredient(data):
allowedKeys = ExtraIngredient().serialize().keys()
if "id" in data:
data.pop("id")
values = {key: value for key, value in data.items() if key in allowedKeys}
extra_ingredient = ExtraIngredient(**values)
db.session.add(extra_ingredient)
db.session.commit()
return extra_ingredient
def update_extra_ingredient(identifier, data):
allowedKeys = ExtraIngredient().serialize().keys()
if "id" in data:
data.pop("id")
values = {key: value for key, value in data.items() if key in allowedKeys}
extra_ingredient = get_extra_ingredient(identifier)
if extra_ingredient:
for key, value in data.items:
setattr(extra_ingredient, key, value)
db.session.commit()
return extra_ingredient
def delete_extra_ingredient(identifier):
extra_ingredient = get_extra_ingredient(identifier)
db.session.delete(extra_ingredient)
db.session.commit()

View File

@ -1,3 +1,5 @@
from __future__ import annotations # TODO: Remove if python requirement is >= 3.10
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional
@ -55,7 +57,7 @@ class Job(db.Model, ModelSerializeMixin):
end: Optional[datetime] = db.Column(UtcDateTime) end: Optional[datetime] = db.Column(UtcDateTime)
comment: str = db.Column(db.String(256)) comment: str = db.Column(db.String(256))
type: JobType = db.relationship("JobType") type: JobType = db.relationship("JobType")
services: [Service] = db.relationship("Service", back_populates="job_") services: list[Service] = db.relationship("Service", back_populates="job_")
required_services: float = db.Column(db.Numeric(precision=4, scale=2, asdecimal=False), nullable=False) required_services: float = db.Column(db.Numeric(precision=4, scale=2, asdecimal=False), nullable=False)
event_ = db.relationship("Event", back_populates="jobs") event_ = db.relationship("Event", back_populates="jobs")
@ -81,6 +83,6 @@ class Event(db.Model, ModelSerializeMixin):
end: datetime = db.Column(UtcDateTime) end: datetime = db.Column(UtcDateTime)
description: Optional[str] = db.Column(db.String(255)) description: Optional[str] = db.Column(db.String(255))
type: EventType = db.relationship("EventType") type: EventType = db.relationship("EventType")
jobs: [Job] = db.relationship( jobs: list[Job] = db.relationship(
"Job", back_populates="event_", cascade="all,delete,delete-orphan", order_by="[Job.start, Job.end]" "Job", back_populates="event_", cascade="all,delete,delete-orphan", order_by="[Job.start, Job.end]"
) )

View File

@ -1,4 +1,7 @@
import datetime import datetime
import sys
if sys.version_info < (3, 7):
from backports.datetime_fromisoformat import MonkeyPatch from backports.datetime_fromisoformat import MonkeyPatch
MonkeyPatch.patch_fromisoformat() MonkeyPatch.patch_fromisoformat()

View File

@ -17,8 +17,10 @@ You will also need a MySQL driver, recommended drivers are
- `mysqlclient` - `mysqlclient`
- `PyMySQL` - `PyMySQL`
`setup.py` will try to install a matching driver.
#### Windows #### Windows
Same as above, but for mysql you have to follow this guide: Same as above, but if you want to use `mysqlclient` instead of `PyMySQL` (performance?) you have to follow this guide:
https://www.radishlogic.com/coding/python-3/installing-mysqldb-for-python-3-in-windows/ https://www.radishlogic.com/coding/python-3/installing-mysqldb-for-python-3-in-windows/

View File

@ -1,33 +1,42 @@
#!/usr/bin/python3 #!/usr/bin/python3
from __future__ import annotations # TODO: Remove if python requirement is >= 3.10
import inspect import inspect
import argparse import argparse
import sys import sys
import pkg_resources import pkg_resources
from flaschengeist.config import config from flaschengeist.config import config
class PrefixMiddleware(object): class PrefixMiddleware(object):
def __init__(self, app, prefix=""):
def __init__(self, app, prefix=''):
self.app = app self.app = app
self.prefix = prefix self.prefix = prefix
def __call__(self, environ, start_response): def __call__(self, environ, start_response):
if environ['PATH_INFO'].startswith(self.prefix): if environ["PATH_INFO"].startswith(self.prefix):
environ['PATH_INFO'] = environ['PATH_INFO'][len(self.prefix):] environ["PATH_INFO"] = environ["PATH_INFO"][len(self.prefix) :]
environ['SCRIPT_NAME'] = self.prefix environ["SCRIPT_NAME"] = self.prefix
return self.app(environ, start_response) return self.app(environ, start_response)
else: else:
start_response('404', [('Content-Type', 'text/plain')]) start_response("404", [("Content-Type", "text/plain")])
return ["This url does not belong to the app.".encode()] return ["This url does not belong to the app.".encode()]
class InterfaceGenerator: class InterfaceGenerator:
known = [] known = []
classes = {} classes = {}
mapper = {"str": "string", "int": "number", "float": "number", "date": "Date", "datetime": "Date", "NoneType": "null"} mapper = {
"str": "string",
"int": "number",
"float": "number",
"date": "Date",
"datetime": "Date",
"NoneType": "null",
"bool": "boolean",
}
def __init__(self, namespace, filename): def __init__(self, namespace, filename):
self.basename = "" self.basename = ""
@ -36,30 +45,43 @@ class InterfaceGenerator:
self.this_type = None self.this_type = None
def pytype(self, cls): def pytype(self, cls):
if isinstance(cls, list): a = self._pytype(cls)
return "", "Array<{}>".format(self.pytype(cls[0])[1]) print(f"{cls} -> {a}")
if sys.version_info >= (3, 8): return a
def _pytype(self, cls):
import typing import typing
if isinstance(cls, typing.ForwardRef): origin = typing.get_origin(cls)
arguments = typing.get_args(cls)
if origin is typing.ForwardRef: # isinstance(cls, typing.ForwardRef):
return "", "this" if cls.__forward_arg__ == self.this_type else cls.__forward_arg__ return "", "this" if cls.__forward_arg__ == self.this_type else cls.__forward_arg__
if typing.get_origin(cls) == typing.Union: if origin is typing.Union:
types = typing.get_args(cls) print(f"A1: {arguments[1]}")
if len(types) == 2 and types[-1] is type(None): if len(arguments) == 2 and arguments[1] is type(None):
return "?", self.pytype(types[0])[1] return "?", self.pytype(arguments[0])[1]
else: else:
return "", "|".join([self.pytype(pt)[1] for pt in types]) return "", "|".join([self.pytype(pt)[1] for pt in arguments])
if hasattr(cls, "__name__"): if origin is list:
if cls.__name__ in self.mapper: return "", "Array<{}>".format("|".join([self.pytype(a_type)[1] for a_type in arguments]))
return "", self.mapper[cls.__name__]
name = cls.__name__ if hasattr(cls, "__name__") else cls if isinstance(cls, str) else None
if name is not None:
if name in self.mapper:
return "", self.mapper[name]
else: else:
return "", cls.__name__ return "", name
print( print(
"WARNING: This python version might not detect all types (try >= 3.8). Could not identify >{}<".format(cls) "WARNING: This python version might not detect all types (try >= 3.9). Could not identify >{}<".format(cls)
) )
return "?", "any" return "?", "any"
def walker(self, module): def walker(self, module):
if sys.version_info < (3, 9):
raise RuntimeError("Python >= 3.9 is required to export API")
import typing
if ( if (
inspect.ismodule(module[1]) inspect.ismodule(module[1])
and module[1].__name__.startswith(self.basename) and module[1].__name__.startswith(self.basename)
@ -76,11 +98,13 @@ class InterfaceGenerator:
and hasattr(module[1], "__annotations__") and hasattr(module[1], "__annotations__")
): ):
self.this_type = module[0] self.this_type = module[0]
d = { print("\n\n" + module[0] + "\n")
param: self.pytype(ptype) d = {}
for param, ptype in module[1].__annotations__.items() for param, ptype in typing.get_type_hints(module[1], globalns=None, localns=None).items():
if not param.startswith("_") and not param.endswith("_") if not param.startswith("_") and not param.endswith("_"):
} print(f"{param} ::: {ptype}")
d[param] = self.pytype(ptype)
if len(d) == 1: if len(d) == 1:
key, value = d.popitem() key, value = d.popitem()
self.classes[module[0]] = value[1] self.classes[module[0]] = value[1]

View File

@ -1,4 +1,7 @@
from setuptools import setup, find_packages from setuptools import setup, find_packages
import os
mysql_driver = "PyMySQL" if os.name == "nt" else "mysqlclient"
setup( setup(
name="flaschengeist", name="flaschengeist",
@ -10,16 +13,16 @@ setup(
packages=find_packages(), packages=find_packages(),
package_data={"": ["*.toml"]}, package_data={"": ["*.toml"]},
scripts=["run_flaschengeist"], scripts=["run_flaschengeist"],
python_requires=">=3.6", python_requires=">=3.7",
install_requires=[ install_requires=[
"Flask >= 1.1", "Flask >= 1.1",
"toml", "toml",
"sqlalchemy>=1.3", # < 1.4: https://github.com/pallets/flask-sqlalchemy/issues/885
"sqlalchemy>=1.3,<1.4",
"flask_sqlalchemy", "flask_sqlalchemy",
"flask_cors", "flask_cors",
"werkzeug", "werkzeug",
# Needed for python < 3.7 mysql_driver,
"backports-datetime-fromisoformat",
], ],
extras_require={"ldap": ["flask_ldapconn", "ldap3"], "test": ["pytest", "coverage"]}, extras_require={"ldap": ["flask_ldapconn", "ldap3"], "test": ["pytest", "coverage"]},
entry_points={ entry_points={