diff --git a/flaschengeist/models/__init__.py b/flaschengeist/models/__init__.py index 59bb5b5..c7b94d1 100644 --- a/flaschengeist/models/__init__.py +++ b/flaschengeist/models/__init__.py @@ -14,8 +14,10 @@ class ModelSerializeMixin: return False 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 def serialize(self): diff --git a/flaschengeist/models/session.py b/flaschengeist/models/session.py index df95b85..c170ad6 100644 --- a/flaschengeist/models/session.py +++ b/flaschengeist/models/session.py @@ -1,3 +1,5 @@ +from __future__ import annotations # TODO: Remove if python requirement is >= 3.10 + from datetime import datetime, timedelta, timezone from . import ModelSerializeMixin, UtcDateTime diff --git a/flaschengeist/models/setting.py b/flaschengeist/models/setting.py index 8b3acc4..6e8f74d 100644 --- a/flaschengeist/models/setting.py +++ b/flaschengeist/models/setting.py @@ -1,3 +1,5 @@ +from __future__ import annotations # TODO: Remove if python requirement is >= 3.10 + from ..database import db diff --git a/flaschengeist/models/user.py b/flaschengeist/models/user.py index b8e8b83..6e23e4b 100644 --- a/flaschengeist/models/user.py +++ b/flaschengeist/models/user.py @@ -1,3 +1,5 @@ +from __future__ import annotations # TODO: Remove if python requirement is >= 3.10 + from flask import url_for from typing import Optional from datetime import date, datetime @@ -6,6 +8,7 @@ from sqlalchemy.orm.collections import attribute_mapped_collection from ..database import db from . import ModelSerializeMixin, UtcDateTime + association_table = db.Table( "user_x_role", db.Column("user_id", db.Integer, db.ForeignKey("user.id")), @@ -30,7 +33,7 @@ class Role(db.Model, ModelSerializeMixin): __tablename__ = "role" id: int = db.Column(db.Integer, primary_key=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): @@ -55,11 +58,11 @@ class User(db.Model, ModelSerializeMixin): lastname: str = db.Column(db.String(50), nullable=False) mail: str = db.Column(db.String(60), nullable=False) birthday: Optional[date] = db.Column(db.Date) - roles: [str] = [] - permissions: Optional[type([str])] = None + roles: list[str] = [] + permissions: Optional[list[str]] = None 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) _sessions = db.relationship("Session", back_populates="_user") _attributes = db.relationship( diff --git a/flaschengeist/plugins/__init__.py b/flaschengeist/plugins/__init__.py index d4249a9..6665830 100644 --- a/flaschengeist/plugins/__init__.py +++ b/flaschengeist/plugins/__init__.py @@ -62,9 +62,11 @@ class Plugin: Value stored in database (native python) """ try: - setting = _PluginSetting.query\ - .filter(_PluginSetting.plugin == self._plugin_name)\ - .filter(_PluginSetting.name == name).one() + setting = ( + _PluginSetting.query.filter(_PluginSetting.plugin == self._plugin_name) + .filter(_PluginSetting.name == name) + .one() + ) return setting.value except sqlalchemy.orm.exc.NoResultFound: if "default" in kwargs: @@ -78,9 +80,11 @@ class Plugin: name: String identifying the setting value: Value to be stored """ - setting = _PluginSetting.query \ - .filter(_PluginSetting.plugin == self._plugin_name) \ - .filter(_PluginSetting.name == name).one_or_none() + setting = ( + _PluginSetting.query.filter(_PluginSetting.plugin == self._plugin_name) + .filter(_PluginSetting.name == name) + .one_or_none() + ) if setting is not None: setting.value = value else: diff --git a/flaschengeist/plugins/auth_plain/__init__.py b/flaschengeist/plugins/auth_plain/__init__.py index 21f5254..c8de367 100644 --- a/flaschengeist/plugins/auth_plain/__init__.py +++ b/flaschengeist/plugins/auth_plain/__init__.py @@ -23,10 +23,12 @@ class AuthPlain(AuthPlugin): self.modify_user(admin, None, "admin") db.session.add(admin) db.session.commit() - logger.warning("New administrator user was added, please change the password or remove it before going into" - "production mode. Initial credentials:\n" - "name: admin\n" - "password: admin") + logger.warning( + "New administrator user was added, please change the password or remove it before going into" + "production mode. Initial credentials:\n" + "name: admin\n" + "password: admin" + ) def login(self, user: User, password: str): if user.has_attribute("password"): diff --git a/flaschengeist/plugins/balance/models.py b/flaschengeist/plugins/balance/models.py index 03ab890..41c3520 100644 --- a/flaschengeist/plugins/balance/models.py +++ b/flaschengeist/plugins/balance/models.py @@ -1,6 +1,7 @@ +from __future__ import annotations # TODO: Remove if python requirement is >= 3.10 + from datetime import datetime from typing import Optional - from sqlalchemy.ext.hybrid import hybrid_property from flaschengeist.database import db diff --git a/flaschengeist/plugins/pricelist/__init__.py b/flaschengeist/plugins/pricelist/__init__.py index 8c9e3d9..b015e85 100644 --- a/flaschengeist/plugins/pricelist/__init__.py +++ b/flaschengeist/plugins/pricelist/__init__.py @@ -5,10 +5,13 @@ from http.client import NO_CONTENT from flaschengeist.plugins import Plugin from flaschengeist.utils.decorators import login_required -from werkzeug.exceptions import BadRequest +from werkzeug.exceptions import BadRequest, Forbidden from . import models 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") @@ -120,8 +123,135 @@ def search_drinks(name): @login_required(permission=permissions.CREATE) def create_drink(current_session): data = request.get_json() - if not all(item in data for item in ["name", "volume", "cost_price"]) or not all( - item in data for item in ["name", "ingredients"] - ): - raise BadRequest("No correct Keys to create drink") - return jsonify(pricelist_controller.create_drink(data)) + return jsonify(pricelist_controller.set_drink(data)) + + +@pricelist_bp.route("/drinks/", methods=["PUT"]) +def update_drink(identifier): + data = request.get_json() + return jsonify(pricelist_controller.update_drink(identifier, data)) + + +@pricelist_bp.route("/drinks/", methods=["DELETE"]) +def delete_drink(identifier): + pricelist_controller.delete_drink(identifier) + return "", NO_CONTENT + + +@pricelist_bp.route("/prices", methods=["GET"]) +@pricelist_bp.route("/prices/", 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//prices", methods=["POST"]) +def create_price(identifier): + data = request.get_json() + return jsonify(pricelist_controller.set_price(identifier, data)) + + +@pricelist_bp.route("/prices/", methods=["PUT"]) +def modify_price(identifier): + data = request.get_json() + return jsonify(pricelist_controller.update_price(identifier, data)) + + +@pricelist_bp.route("/prices/", methods=["DELETE"]) +def delete_price(identifier): + pricelist_controller.delete_price(identifier) + return "", NO_CONTENT + + +@pricelist_bp.route("/drinks//volumes", methods=["POST"]) +def set_volume(identifier): + data = request.get_json() + return jsonify(pricelist_controller.set_volume(identifier, data)) + + +@pricelist_bp.route("/volumes//ingredients", methods=["POST"]) +def set_ingredient(identifier): + data = request.get_json() + return jsonify(pricelist_controller.set_ingredient(data, identifier)) + + +@pricelist_bp.route("/volumes/", methods=["PUT"]) +def update_volume(identifier): + data = request.get_json() + return jsonify(pricelist_controller.update_volume(identifier, data)) + + +@pricelist_bp.route("/volumes/", 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/", methods=["PUT"]) +def update_ingredient(identifier): + data = request.get_json() + return jsonify(pricelist_controller.update_ingredient(identifier, data)) + + +@pricelist_bp.route("/ingredients/", 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/", 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/", methods=["DELETE"]) +def delete_extra_ingredient(identifier): + pricelist_controller.delete_extra_ingredient(identifier) + return "", NO_CONTENT + + +@pricelist_bp.route("/users//pricecalc_columns", methods=["GET", "PUT"]) +@login_required() +def get_columns(userid, current_session: Session): + """Get pricecalc_columns of an user + + Route: ``/users//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() diff --git a/flaschengeist/plugins/pricelist/models.py b/flaschengeist/plugins/pricelist/models.py index 1eb9ae6..703a792 100644 --- a/flaschengeist/plugins/pricelist/models.py +++ b/flaschengeist/plugins/pricelist/models.py @@ -1,7 +1,9 @@ +from __future__ import annotations # TODO: Remove if python requirement is >= 3.10 + from flaschengeist.database import db from flaschengeist.models import ModelSerializeMixin -from typing import Optional +from typing import Optional, Union drink_tag_association = db.Table( "drink_x_tag", @@ -43,28 +45,82 @@ class DrinkPrice(db.Model, ModelSerializeMixin): __tablename__ = "drink_price" 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)) - drink_id = db.Column("drink_id", db.Integer, db.ForeignKey("drink.id")) - drink = db.relationship("Drink", back_populates="prices") - no_auto: bool = db.Column(db.Boolean, default=False) + volume_id_ = db.Column("volume_id", db.Integer, db.ForeignKey("drink_price_volume.id")) + volume = db.relationship("DrinkPriceVolume", back_populates="prices") public: bool = db.Column(db.Boolean, default=True) 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" id: int = db.Column("id", db.Integer, primary_key=True) 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: "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): @@ -74,17 +130,15 @@ class Drink(db.Model, ModelSerializeMixin): __tablename__ = "drink" 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) - volume: float = db.Column(db.Numeric(precision=5, scale=2, asdecimal=False)) - cost_price: float = db.Column(db.Numeric(precision=5, scale=2, asdecimal=False)) - discount: float = db.Column(db.Numeric(precision=3, scale=2, asdecimal=False), nullable=False) - extra_charge: Optional[float] = db.Column(db.Numeric(precision=3, scale=2, asdecimal=False), default=0) - prices: [DrinkPrice] = db.relationship( - "DrinkPrice", back_populates="drink", cascade="all,delete,delete-orphan", order_by=[DrinkPrice.volume] - ) - ingredients: [Ingredient] = db.relationship( - "Ingredient", back_populates="drink_parent", foreign_keys=Ingredient.drink_parent_id - ) - 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") + volume: Optional[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)) + cost_price_package_netto: Optional[float] = db.Column(db.Numeric(precision=5, scale=3, asdecimal=False)) + + _type_id = db.Column("type_id", db.Integer, db.ForeignKey("drink_type.id")) + + tags: Optional[list[Tag]] = db.relationship("Tag", secondary=drink_tag_association, cascade="save-update, merge") + type: Optional[DrinkType] = db.relationship("DrinkType", foreign_keys=[_type_id]) + volumes: list[DrinkPriceVolume] = db.relationship(DrinkPriceVolume) diff --git a/flaschengeist/plugins/pricelist/pricelist_controller.py b/flaschengeist/plugins/pricelist/pricelist_controller.py index 93c365c..8eceec9 100644 --- a/flaschengeist/plugins/pricelist/pricelist_controller.py +++ b/flaschengeist/plugins/pricelist/pricelist_controller.py @@ -3,7 +3,7 @@ from sqlalchemy.exc import IntegrityError from flaschengeist import logger 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 @@ -103,56 +103,6 @@ def delete_drink_type(identifier): 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): if name: return Drink.query.filter(Drink.name.contains(name)).all() @@ -161,49 +111,264 @@ def get_drinks(name=None): def get_drink(identifier): if isinstance(identifier, int): - retVal = Drink.query.get(identifier) + return Drink.query.get(identifier) 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: logger.debug("Invalid identifier type for Drink") raise BadRequest - if not retVal: - raise NotFound - return retVal + raise NotFound -def add_prices(drink, prices): - for price in prices: - drink.prices.append(price) - - -def add_ingredients(drink, ingredients): - for identifier, volume in ingredients: - ingredient = Ingredient(volume=volume, drink_ingredient=get_drink(identifier)) - drink.ingredients.append(ingredient) - - -def create_drink(data): - allowed_keys = Drink().serialize().keys() - 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") - +def set_drink(data): + allowedKeys = Drink().serialize().keys() + if "id" in data: + data.pop("id") + values = {key: value if value != "" else None 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 = Drink(**values) - add_ingredients(drink, ingredients) - drink.prices = calc_prices(drink, prices) + if type: + drink.type = type 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 def delete_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) - 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() diff --git a/flaschengeist/plugins/schedule/models.py b/flaschengeist/plugins/schedule/models.py index 118c9b9..04c060f 100644 --- a/flaschengeist/plugins/schedule/models.py +++ b/flaschengeist/plugins/schedule/models.py @@ -1,3 +1,5 @@ +from __future__ import annotations # TODO: Remove if python requirement is >= 3.10 + from datetime import datetime from typing import Optional @@ -55,7 +57,7 @@ class Job(db.Model, ModelSerializeMixin): end: Optional[datetime] = db.Column(UtcDateTime) comment: str = db.Column(db.String(256)) 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) event_ = db.relationship("Event", back_populates="jobs") @@ -81,6 +83,6 @@ class Event(db.Model, ModelSerializeMixin): end: datetime = db.Column(UtcDateTime) description: Optional[str] = db.Column(db.String(255)) 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]" ) diff --git a/flaschengeist/utils/datetime.py b/flaschengeist/utils/datetime.py index cf97a00..9de34ad 100644 --- a/flaschengeist/utils/datetime.py +++ b/flaschengeist/utils/datetime.py @@ -1,7 +1,10 @@ import datetime -from backports.datetime_fromisoformat import MonkeyPatch +import sys -MonkeyPatch.patch_fromisoformat() +if sys.version_info < (3, 7): + from backports.datetime_fromisoformat import MonkeyPatch + + MonkeyPatch.patch_fromisoformat() def from_iso_format(date_str): diff --git a/readme.md b/readme.md index 1041e1e..c10cf58 100644 --- a/readme.md +++ b/readme.md @@ -17,8 +17,10 @@ You will also need a MySQL driver, recommended drivers are - `mysqlclient` - `PyMySQL` +`setup.py` will try to install a matching driver. + #### 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/ diff --git a/run_flaschengeist b/run_flaschengeist index 522fbd0..ce06a1c 100644 --- a/run_flaschengeist +++ b/run_flaschengeist @@ -1,33 +1,42 @@ #!/usr/bin/python3 +from __future__ import annotations # TODO: Remove if python requirement is >= 3.10 import inspect import argparse import sys + import pkg_resources from flaschengeist.config import config class PrefixMiddleware(object): - - def __init__(self, app, prefix=''): + def __init__(self, app, prefix=""): self.app = app self.prefix = prefix def __call__(self, environ, start_response): - if environ['PATH_INFO'].startswith(self.prefix): - environ['PATH_INFO'] = environ['PATH_INFO'][len(self.prefix):] - environ['SCRIPT_NAME'] = self.prefix + if environ["PATH_INFO"].startswith(self.prefix): + environ["PATH_INFO"] = environ["PATH_INFO"][len(self.prefix) :] + environ["SCRIPT_NAME"] = self.prefix return self.app(environ, start_response) 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()] class InterfaceGenerator: known = [] 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): self.basename = "" @@ -36,30 +45,43 @@ class InterfaceGenerator: self.this_type = None def pytype(self, cls): - if isinstance(cls, list): - return "", "Array<{}>".format(self.pytype(cls[0])[1]) - if sys.version_info >= (3, 8): - import typing + a = self._pytype(cls) + print(f"{cls} -> {a}") + return a - if isinstance(cls, typing.ForwardRef): - return "", "this" if cls.__forward_arg__ == self.this_type else cls.__forward_arg__ - if typing.get_origin(cls) == typing.Union: - types = typing.get_args(cls) - if len(types) == 2 and types[-1] is type(None): - return "?", self.pytype(types[0])[1] - else: - return "", "|".join([self.pytype(pt)[1] for pt in types]) - if hasattr(cls, "__name__"): - if cls.__name__ in self.mapper: - return "", self.mapper[cls.__name__] + def _pytype(self, cls): + import typing + + 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__ + if origin is typing.Union: + print(f"A1: {arguments[1]}") + if len(arguments) == 2 and arguments[1] is type(None): + return "?", self.pytype(arguments[0])[1] else: - return "", cls.__name__ + return "", "|".join([self.pytype(pt)[1] for pt in arguments]) + if origin is list: + return "", "Array<{}>".format("|".join([self.pytype(a_type)[1] for a_type in arguments])) + + 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: + return "", name 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" def walker(self, module): + if sys.version_info < (3, 9): + raise RuntimeError("Python >= 3.9 is required to export API") + import typing + if ( inspect.ismodule(module[1]) and module[1].__name__.startswith(self.basename) @@ -76,11 +98,13 @@ class InterfaceGenerator: and hasattr(module[1], "__annotations__") ): self.this_type = module[0] - d = { - param: self.pytype(ptype) - for param, ptype in module[1].__annotations__.items() - if not param.startswith("_") and not param.endswith("_") - } + print("\n\n" + module[0] + "\n") + d = {} + for param, ptype in typing.get_type_hints(module[1], globalns=None, localns=None).items(): + if not param.startswith("_") and not param.endswith("_"): + print(f"{param} ::: {ptype}") + d[param] = self.pytype(ptype) + if len(d) == 1: key, value = d.popitem() self.classes[module[0]] = value[1] diff --git a/setup.py b/setup.py index 5a6ecaf..4be654d 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,7 @@ from setuptools import setup, find_packages +import os + +mysql_driver = "PyMySQL" if os.name == "nt" else "mysqlclient" setup( name="flaschengeist", @@ -10,16 +13,16 @@ setup( packages=find_packages(), package_data={"": ["*.toml"]}, scripts=["run_flaschengeist"], - python_requires=">=3.6", + python_requires=">=3.7", install_requires=[ "Flask >= 1.1", "toml", - "sqlalchemy>=1.3", + # < 1.4: https://github.com/pallets/flask-sqlalchemy/issues/885 + "sqlalchemy>=1.3,<1.4", "flask_sqlalchemy", "flask_cors", "werkzeug", - # Needed for python < 3.7 - "backports-datetime-fromisoformat", + mysql_driver, ], extras_require={"ldap": ["flask_ldapconn", "ldap3"], "test": ["pytest", "coverage"]}, entry_points={