From df1610557ffc5fb314250ff08cfa85938962c496 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Mon, 15 Mar 2021 19:56:51 +0100 Subject: [PATCH 01/15] [pricelist] break api, new model --- flaschengeist/plugins/pricelist/__init__.py | 30 ++++- flaschengeist/plugins/pricelist/models.py | 90 ++++++++++--- .../plugins/pricelist/pricelist_controller.py | 126 ++++++------------ 3 files changed, 140 insertions(+), 106 deletions(-) diff --git a/flaschengeist/plugins/pricelist/__init__.py b/flaschengeist/plugins/pricelist/__init__.py index 8c9e3d9..5df895b 100644 --- a/flaschengeist/plugins/pricelist/__init__.py +++ b/flaschengeist/plugins/pricelist/__init__.py @@ -124,4 +124,32 @@ def create_drink(current_session): 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.create_drink(data))" + + +@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("/prices/volumes/", 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 diff --git a/flaschengeist/plugins/pricelist/models.py b/flaschengeist/plugins/pricelist/models.py index 1eb9ae6..c64d6c3 100644 --- a/flaschengeist/plugins/pricelist/models.py +++ b/flaschengeist/plugins/pricelist/models.py @@ -43,28 +43,78 @@ 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): + return self.drink_ingredient.cost_price_pro_volume * self.volume + + +class Ingredient(db.Model, ModelSerializeMixin): + """ + Ingredient Associationtable + """ + + __tablename__ = "ingredient_association" + id: int = db.Column("id", db.Integer, primary_key=True) + volume_id: int = db.Column(db.Integer, db.ForeignKey("drink_price_volume.id")) + drink_ingredient_id = db.Column(db.Integer, db.ForeignKey("drink_ingredient.id")) + drink_ingredient: DrinkIngredient = db.relationship(DrinkIngredient) + extra_ingredient_id = db.Column(db.Integer, db.ForeignKey("extra_ingredient.id")) + extra_ingredient: ExtraIngredient = db.relationship(ExtraIngredient) + + +class DrinkPriceVolume(db.Model, ModelSerializeMixin): + """ + Drink Volumes and Prices + """ + + __tablename__ = "drink_price_volume" + id: int = db.Column("id", db.Integer, primary_key=True) + volume: float = db.Column(db.Numeric(precision=5, scale=2, asdecimal=False)) + prices: [DrinkPrice] = db.relationship(DrinkPrice, back_populates="volume", cascade="all,delete,delete-orphan") + ingredients: [DrinkIngredient or ExtraIngredient] = [] + _ingredients: [Ingredient] = db.relationship("Ingredient", foreign_keys=Ingredient.volume_id) + + drink_id = db.Column(db.Integer, db.ForeignKey("drink.id"), nullable=False) + + @property + def ingredients(self): + retVal = [] + for ingredient in self._ingredients: + if ingredient.drink_ingredient_id != None: + retVal.append(ingredient.drink_ingredient) + if ingredient.extra_ingredient_id != None: + retVal.append(ingredient.extra_ingredient) + return retVal class Drink(db.Model, ModelSerializeMixin): @@ -74,17 +124,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 - ) + 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)) + 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") + type: DrinkType = db.relationship("DrinkType") + + volumes: [DrinkPriceVolume] = db.relationship(DrinkPriceVolume) diff --git a/flaschengeist/plugins/pricelist/pricelist_controller.py b/flaschengeist/plugins/pricelist/pricelist_controller.py index 93c365c..d308504 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,57 @@ 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 get_volume(identifier): + return DrinkPriceVolume.query.get(identifier) -def add_ingredients(drink, ingredients): - for identifier, volume in ingredients: - ingredient = Ingredient(volume=volume, drink_ingredient=get_drink(identifier)) - drink.ingredients.append(ingredient) +def get_price(identifier): + if isinstance(identifier, int): + return DrinkPrice.query.get(identifier) + raise NotFound -def create_drink(data): - allowed_keys = Drink().serialize().keys() +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() 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) - add_ingredients(drink, ingredients) - drink.prices = calc_prices(drink, prices) - db.session.add(drink) - update() - return drink + 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 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() +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: + setattr(price, key, value) + db.session.commit() + return price + + +def delete_price(identifier): + price = get_price(identifier) + db.session.delete(price) + db.session.commit() From e26b7b8c962ff26948c230584aa80e6a5deeaa52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Mon, 15 Mar 2021 23:53:21 +0100 Subject: [PATCH 02/15] [pricelist] first step to add, modify and delete volumes --- flaschengeist/plugins/pricelist/__init__.py | 22 ++++++++- .../plugins/pricelist/pricelist_controller.py | 48 ++++++++++++++++++- 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/flaschengeist/plugins/pricelist/__init__.py b/flaschengeist/plugins/pricelist/__init__.py index 5df895b..db001e5 100644 --- a/flaschengeist/plugins/pricelist/__init__.py +++ b/flaschengeist/plugins/pricelist/__init__.py @@ -137,7 +137,7 @@ def get_prices(identifier=None): return jsonify(result) -@pricelist_bp.route("/prices/volumes/", methods=["POST"]) +@pricelist_bp.route("/volumes//prices", methods=["POST"]) def create_price(identifier): data = request.get_json() return jsonify(pricelist_controller.set_price(identifier, data)) @@ -152,4 +152,22 @@ def modify_price(identifier): @pricelist_bp.route("/prices/", methods=["DELETE"]) def delete_price(identifier): pricelist_controller.delete_price(identifier) - return NO_CONTENT + 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/", 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 diff --git a/flaschengeist/plugins/pricelist/pricelist_controller.py b/flaschengeist/plugins/pricelist/pricelist_controller.py index d308504..2e1b69f 100644 --- a/flaschengeist/plugins/pricelist/pricelist_controller.py +++ b/flaschengeist/plugins/pricelist/pricelist_controller.py @@ -124,6 +124,50 @@ 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} + + 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) + 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) @@ -138,6 +182,8 @@ def get_prices(volume_id=None): 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) @@ -155,7 +201,7 @@ def update_price(identifier, 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: + for key, value in values.items(): setattr(price, key, value) db.session.commit() return price From 0b60c27f32a520c526e8055fa0b01b12bf5cf43d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Tue, 16 Mar 2021 18:11:04 +0100 Subject: [PATCH 03/15] [pricelist] fixed set volumes --- flaschengeist/plugins/pricelist/pricelist_controller.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/flaschengeist/plugins/pricelist/pricelist_controller.py b/flaschengeist/plugins/pricelist/pricelist_controller.py index 2e1b69f..e5725b0 100644 --- a/flaschengeist/plugins/pricelist/pricelist_controller.py +++ b/flaschengeist/plugins/pricelist/pricelist_controller.py @@ -135,7 +135,10 @@ def set_volume(identifier, data): 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: From 5e0e5edf6f9a7aeac442be52104314bb7e0c6fde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Tue, 16 Mar 2021 23:27:54 +0100 Subject: [PATCH 04/15] [pricelist] new api to add, delete and modify ingredients --- flaschengeist/plugins/pricelist/__init__.py | 23 ++++++ flaschengeist/plugins/pricelist/models.py | 18 ++--- .../plugins/pricelist/pricelist_controller.py | 80 ++++++++++++++++++- 3 files changed, 107 insertions(+), 14 deletions(-) diff --git a/flaschengeist/plugins/pricelist/__init__.py b/flaschengeist/plugins/pricelist/__init__.py index db001e5..3866abf 100644 --- a/flaschengeist/plugins/pricelist/__init__.py +++ b/flaschengeist/plugins/pricelist/__init__.py @@ -161,6 +161,12 @@ def set_volume(identifier): 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() @@ -171,3 +177,20 @@ def update_volume(identifier): 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 diff --git a/flaschengeist/plugins/pricelist/models.py b/flaschengeist/plugins/pricelist/models.py index c64d6c3..1308f29 100644 --- a/flaschengeist/plugins/pricelist/models.py +++ b/flaschengeist/plugins/pricelist/models.py @@ -75,7 +75,10 @@ class DrinkIngredient(db.Model, ModelSerializeMixin): @property def price(self): - return self.drink_ingredient.cost_price_pro_volume * self.volume + try: + return self.drink_ingredient.cost_price_pro_volume * self.volume + except AttributeError: + pass class Ingredient(db.Model, ModelSerializeMixin): @@ -101,21 +104,10 @@ class DrinkPriceVolume(db.Model, ModelSerializeMixin): id: int = db.Column("id", db.Integer, primary_key=True) volume: float = db.Column(db.Numeric(precision=5, scale=2, asdecimal=False)) prices: [DrinkPrice] = db.relationship(DrinkPrice, back_populates="volume", cascade="all,delete,delete-orphan") - ingredients: [DrinkIngredient or ExtraIngredient] = [] - _ingredients: [Ingredient] = db.relationship("Ingredient", foreign_keys=Ingredient.volume_id) + ingredients: [Ingredient] = db.relationship("Ingredient", foreign_keys=Ingredient.volume_id) drink_id = db.Column(db.Integer, db.ForeignKey("drink.id"), nullable=False) - @property - def ingredients(self): - retVal = [] - for ingredient in self._ingredients: - if ingredient.drink_ingredient_id != None: - retVal.append(ingredient.drink_ingredient) - if ingredient.extra_ingredient_id != None: - retVal.append(ingredient.extra_ingredient) - return retVal - class Drink(db.Model, ModelSerializeMixin): """ diff --git a/flaschengeist/plugins/pricelist/pricelist_controller.py b/flaschengeist/plugins/pricelist/pricelist_controller.py index e5725b0..a8eb69c 100644 --- a/flaschengeist/plugins/pricelist/pricelist_controller.py +++ b/flaschengeist/plugins/pricelist/pricelist_controller.py @@ -160,7 +160,7 @@ def update_volume(identifier, data): ingredients = values.pop("ingredients") volume = get_volume(identifier) for key, value in values.items(): - setattr(volume, key, value) + setattr(volume, key, value if value != "" else None) db.session.commit() return volume @@ -214,3 +214,81 @@ 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") + if "drink_ingredient" in data: + drink_ingredient_ = data.pop("drink_ingredient") + values = {key: value for key, value in data.items() if key in allowedKeys} + drink_ingredient = DrinkIngredient(**values) + if "id" in drink_ingredient_: + drink = get_drink(drink_ingredient_.get("id")) + 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} + if "drink_ingredient" in values: + drink_ingredient_values = values.get("drink_ingredient") + if "extra_ingredient" in values: + extra_ingredient_value = values.get("extra_ingredient") + + ingredient = Ingredient(**values) + volume = get_volume(volume_id) + if drink_ingredient_values: + ingredient.drink_ingredient = set_drink_ingredient(drink_ingredient_values) + 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 and isinstance(data.get("drink_ingredient"), dict): + if "drink_ingredient" in data.get("drink_ingredient"): + if "id" in data.get("drink_ingredient").get("drink_ingredient"): + ingredient.drink_ingredient.drink_ingredient = get_drink( + data.get("drink_ingredient").get("drink_ingredient").get("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) From 642d95b2a5c6fbeac19d185d2679f94caccf9ae1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Wed, 17 Mar 2021 21:36:51 +0100 Subject: [PATCH 05/15] [pricelist] finish drinks, can add, modify and delete --- flaschengeist/plugins/pricelist/__init__.py | 18 +++++-- .../plugins/pricelist/pricelist_controller.py | 49 +++++++++++++++++++ 2 files changed, 62 insertions(+), 5 deletions(-) diff --git a/flaschengeist/plugins/pricelist/__init__.py b/flaschengeist/plugins/pricelist/__init__.py index 3866abf..89967b9 100644 --- a/flaschengeist/plugins/pricelist/__init__.py +++ b/flaschengeist/plugins/pricelist/__init__.py @@ -120,11 +120,19 @@ 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"]) diff --git a/flaschengeist/plugins/pricelist/pricelist_controller.py b/flaschengeist/plugins/pricelist/pricelist_controller.py index a8eb69c..03bf389 100644 --- a/flaschengeist/plugins/pricelist/pricelist_controller.py +++ b/flaschengeist/plugins/pricelist/pricelist_controller.py @@ -120,6 +120,55 @@ def get_drink(identifier): raise NotFound +def set_drink(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 = Drink(**values) + if type: + drink.type = type + db.session.add(drink) + 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) + db.session.delete(drink) + db.session.commit() + + def get_volume(identifier): return DrinkPriceVolume.query.get(identifier) From 26e82f02d688f32ef09f42705866dd254cf82a45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Wed, 17 Mar 2021 22:49:54 +0100 Subject: [PATCH 06/15] [pricelist] add, modify and delete for extra ingredients --- flaschengeist/plugins/pricelist/__init__.py | 18 +++++++++++ .../plugins/pricelist/pricelist_controller.py | 32 ++++++++++++++++++- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/flaschengeist/plugins/pricelist/__init__.py b/flaschengeist/plugins/pricelist/__init__.py index 89967b9..e5a15ac 100644 --- a/flaschengeist/plugins/pricelist/__init__.py +++ b/flaschengeist/plugins/pricelist/__init__.py @@ -202,3 +202,21 @@ def update_ingredient(identifier): 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 diff --git a/flaschengeist/plugins/pricelist/pricelist_controller.py b/flaschengeist/plugins/pricelist/pricelist_controller.py index 03bf389..d688178 100644 --- a/flaschengeist/plugins/pricelist/pricelist_controller.py +++ b/flaschengeist/plugins/pricelist/pricelist_controller.py @@ -124,7 +124,7 @@ def set_drink(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} + 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: @@ -341,3 +341,33 @@ def get_extra_ingredients(): 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() From 948c700e469c4ff220749e12ec58329185d24374 Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Thu, 18 Mar 2021 10:18:08 +0100 Subject: [PATCH 07/15] [setup] Fixed sqlalchemy requirement --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 5a6ecaf..34c4e84 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,8 @@ setup( 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", From 466efcf9e708a7da74701825d83731452b5ae829 Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Thu, 18 Mar 2021 11:52:51 +0100 Subject: [PATCH 08/15] [System] New annotation format for future compatibility --- flaschengeist/models/session.py | 2 ++ flaschengeist/models/setting.py | 2 ++ flaschengeist/models/user.py | 11 +++++++---- flaschengeist/plugins/balance/models.py | 3 ++- flaschengeist/plugins/pricelist/models.py | 12 ++++++++---- flaschengeist/plugins/schedule/models.py | 6 ++++-- 6 files changed, 25 insertions(+), 11 deletions(-) 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/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/models.py b/flaschengeist/plugins/pricelist/models.py index 1308f29..5adab4d 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", @@ -70,7 +72,7 @@ class DrinkIngredient(db.Model, ModelSerializeMixin): 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_ingredient_id: int = db.Column("drink_ingredient_id", db.Integer, db.ForeignKey("drink.id")) - drink_ingredient: "Drink" = db.relationship("Drink") + drink_ingredient = db.relationship("Drink") price: float = 0 @property @@ -104,7 +106,9 @@ class DrinkPriceVolume(db.Model, ModelSerializeMixin): id: int = db.Column("id", db.Integer, primary_key=True) volume: float = db.Column(db.Numeric(precision=5, scale=2, asdecimal=False)) prices: [DrinkPrice] = db.relationship(DrinkPrice, back_populates="volume", cascade="all,delete,delete-orphan") - ingredients: [Ingredient] = db.relationship("Ingredient", foreign_keys=Ingredient.volume_id) + ingredients: Union[DrinkIngredient, ExtraIngredient] = [] + # TODO: Really protected or just not exported (e.g. name_)? + _ingredients: [Ingredient] = db.relationship("Ingredient", foreign_keys=Ingredient.volume_id) drink_id = db.Column(db.Integer, db.ForeignKey("drink.id"), nullable=False) @@ -123,7 +127,7 @@ class Drink(db.Model, ModelSerializeMixin): 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)) - tags: [Optional[Tag]] = db.relationship("Tag", secondary=drink_tag_association, cascade="save-update, merge") + tags: list[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: DrinkType = db.relationship("DrinkType") 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]" ) From 900b5efff55e3e51f168415372dcb4f2a55fecfd Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Thu, 18 Mar 2021 12:20:17 +0100 Subject: [PATCH 09/15] [chore] Minor cleanup --- flaschengeist/models/__init__.py | 7 +++++-- flaschengeist/plugins/__init__.py | 16 ++++++++++------ flaschengeist/plugins/auth_plain/__init__.py | 10 ++++++---- flaschengeist/utils/datetime.py | 7 +++++-- setup.py | 4 +--- 5 files changed, 27 insertions(+), 17 deletions(-) diff --git a/flaschengeist/models/__init__.py b/flaschengeist/models/__init__.py index 59bb5b5..5f9484a 100644 --- a/flaschengeist/models/__init__.py +++ b/flaschengeist/models/__init__.py @@ -14,8 +14,11 @@ 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 isinstance(typing.get_args(self.__class__.__annotations__[param])[1], type(None)) + ): return getattr(self, param) is None def serialize(self): 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/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/setup.py b/setup.py index 34c4e84..2986a6e 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ setup( packages=find_packages(), package_data={"": ["*.toml"]}, scripts=["run_flaschengeist"], - python_requires=">=3.6", + python_requires=">=3.7", install_requires=[ "Flask >= 1.1", "toml", @@ -19,8 +19,6 @@ setup( "flask_sqlalchemy", "flask_cors", "werkzeug", - # Needed for python < 3.7 - "backports-datetime-fromisoformat", ], extras_require={"ldap": ["flask_ldapconn", "ldap3"], "test": ["pytest", "coverage"]}, entry_points={ From 85f83f46d543a37d292ad07bd1508130a500598c Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Thu, 18 Mar 2021 13:02:16 +0100 Subject: [PATCH 10/15] [Script] Enhanced and added future compatibility for API export. * Python >= 3.9 required for API export. --- flaschengeist/plugins/pricelist/models.py | 20 +++--- run_flaschengeist | 79 ++++++++++++++--------- 2 files changed, 58 insertions(+), 41 deletions(-) diff --git a/flaschengeist/plugins/pricelist/models.py b/flaschengeist/plugins/pricelist/models.py index 5adab4d..2a7e182 100644 --- a/flaschengeist/plugins/pricelist/models.py +++ b/flaschengeist/plugins/pricelist/models.py @@ -104,13 +104,13 @@ class DrinkPriceVolume(db.Model, ModelSerializeMixin): __tablename__ = "drink_price_volume" id: int = db.Column("id", db.Integer, primary_key=True) - volume: float = db.Column(db.Numeric(precision=5, scale=2, asdecimal=False)) - prices: [DrinkPrice] = db.relationship(DrinkPrice, back_populates="volume", cascade="all,delete,delete-orphan") - ingredients: Union[DrinkIngredient, ExtraIngredient] = [] - # TODO: Really protected or just not exported (e.g. name_)? - _ingredients: [Ingredient] = db.relationship("Ingredient", foreign_keys=Ingredient.volume_id) - drink_id = db.Column(db.Integer, db.ForeignKey("drink.id"), nullable=False) + volume: float = db.Column(db.Numeric(precision=5, scale=2, asdecimal=False)) + ingredients: list[DrinkIngredient, ExtraIngredient] = [] + + prices: list[DrinkPrice] = db.relationship(DrinkPrice, back_populates="volume", cascade="all,delete,delete-orphan") + # TODO: Really protected or just not exported (e.g. name_)? + _ingredients: list[Ingredient] = db.relationship("Ingredient", foreign_keys=Ingredient.volume_id) class Drink(db.Model, ModelSerializeMixin): @@ -127,8 +127,8 @@ class Drink(db.Model, ModelSerializeMixin): 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)) - tags: list[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: DrinkType = db.relationship("DrinkType") + _type_id = db.Column("type_id", db.Integer, db.ForeignKey("drink_type.id")) - volumes: [DrinkPriceVolume] = db.relationship(DrinkPriceVolume) + tags: list[Tag] = db.relationship("Tag", secondary=drink_tag_association, cascade="save-update, merge") + type: DrinkType = db.relationship("DrinkType", foreign_keys=[_type_id]) + volumes: list[DrinkPriceVolume] = db.relationship(DrinkPriceVolume) diff --git a/run_flaschengeist b/run_flaschengeist index 522fbd0..1e0e9c5 100644 --- a/run_flaschengeist +++ b/run_flaschengeist @@ -1,7 +1,9 @@ #!/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 @@ -27,7 +29,8 @@ class PrefixMiddleware(object): 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"} def __init__(self, namespace, filename): self.basename = "" @@ -36,51 +39,65 @@ 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) - and module[1].__name__ not in self.known + inspect.ismodule(module[1]) + and module[1].__name__.startswith(self.basename) + and module[1].__name__ not in self.known ): self.known.append(module[1].__name__) for cls in inspect.getmembers(module[1], lambda x: inspect.isclass(x) or inspect.ismodule(x)): self.walker(cls) elif ( - inspect.isclass(module[1]) - and module[1].__module__.startswith(self.basename) - and module[0] not in self.classes - and not module[0].startswith("_") - and hasattr(module[1], "__annotations__") + inspect.isclass(module[1]) + and module[1].__module__.startswith(self.basename) + and module[0] not in self.classes + and not module[0].startswith("_") + 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] From fb8371afb9fc00b8ae04f9a7d9c11a481138f4ef Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Thu, 18 Mar 2021 13:03:34 +0100 Subject: [PATCH 11/15] [chore] Minor cleanup --- flaschengeist/models/__init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/flaschengeist/models/__init__.py b/flaschengeist/models/__init__.py index 5f9484a..c7b94d1 100644 --- a/flaschengeist/models/__init__.py +++ b/flaschengeist/models/__init__.py @@ -15,10 +15,9 @@ class ModelSerializeMixin: import typing - if ( - typing.get_origin(self.__class__.__annotations__[param]) is typing.Union - and isinstance(typing.get_args(self.__class__.__annotations__[param])[1], type(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): From 87f9b0aa48967156c113af560968dbe16c3774a5 Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Thu, 18 Mar 2021 14:05:28 +0100 Subject: [PATCH 12/15] [setup] Install correct mysql driver --- readme.md | 4 +++- setup.py | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) 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/setup.py b/setup.py index 2986a6e..cfab611 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", @@ -19,6 +22,7 @@ setup( "flask_sqlalchemy", "flask_cors", "werkzeug", + mysql_driver ], extras_require={"ldap": ["flask_ldapconn", "ldap3"], "test": ["pytest", "coverage"]}, entry_points={ From d5ba1f023e0f726aee72917b01e2c36e74ffc70e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Thu, 18 Mar 2021 17:26:02 +0100 Subject: [PATCH 13/15] [pricelist] fixed circula issue --- flaschengeist/plugins/pricelist/models.py | 42 ++++++++++++------- .../plugins/pricelist/pricelist_controller.py | 27 ++++++------ 2 files changed, 40 insertions(+), 29 deletions(-) diff --git a/flaschengeist/plugins/pricelist/models.py b/flaschengeist/plugins/pricelist/models.py index 2a7e182..703a792 100644 --- a/flaschengeist/plugins/pricelist/models.py +++ b/flaschengeist/plugins/pricelist/models.py @@ -72,15 +72,16 @@ class DrinkIngredient(db.Model, ModelSerializeMixin): 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_ingredient_id: int = db.Column("drink_ingredient_id", db.Integer, db.ForeignKey("drink.id")) - drink_ingredient = db.relationship("Drink") - price: float = 0 + # 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 + +# @property +# def price(self): +# try: +# return self.drink_ingredient.cost_price_pro_volume * self.volume +# except AttributeError: +# pass class Ingredient(db.Model, ModelSerializeMixin): @@ -90,11 +91,20 @@ class Ingredient(db.Model, ModelSerializeMixin): __tablename__ = "ingredient_association" id: int = db.Column("id", db.Integer, primary_key=True) - volume_id: int = db.Column(db.Integer, db.ForeignKey("drink_price_volume.id")) + 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: DrinkIngredient = db.relationship(DrinkIngredient) + drink_ingredient: Optional[DrinkIngredient] = db.relationship(DrinkIngredient) extra_ingredient_id = db.Column(db.Integer, db.ForeignKey("extra_ingredient.id")) - extra_ingredient: ExtraIngredient = db.relationship(ExtraIngredient) + extra_ingredient: Optional[ExtraIngredient] = db.relationship(ExtraIngredient) + + +class MinPrices(ModelSerializeMixin): + """ + MinPrices + """ + + percentage: float + price: float class DrinkPriceVolume(db.Model, ModelSerializeMixin): @@ -106,11 +116,11 @@ class DrinkPriceVolume(db.Model, ModelSerializeMixin): 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)) - ingredients: list[DrinkIngredient, ExtraIngredient] = [] + min_prices: list[MinPrices] = [] + # ingredients: list[Ingredient] = [] prices: list[DrinkPrice] = db.relationship(DrinkPrice, back_populates="volume", cascade="all,delete,delete-orphan") - # TODO: Really protected or just not exported (e.g. name_)? - _ingredients: list[Ingredient] = db.relationship("Ingredient", foreign_keys=Ingredient.volume_id) + ingredients: list[Ingredient] = db.relationship("Ingredient", foreign_keys=Ingredient.volume_id) class Drink(db.Model, ModelSerializeMixin): @@ -129,6 +139,6 @@ class Drink(db.Model, ModelSerializeMixin): _type_id = db.Column("type_id", db.Integer, db.ForeignKey("drink_type.id")) - tags: list[Tag] = db.relationship("Tag", secondary=drink_tag_association, cascade="save-update, merge") - type: DrinkType = db.relationship("DrinkType", foreign_keys=[_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 d688178..8eceec9 100644 --- a/flaschengeist/plugins/pricelist/pricelist_controller.py +++ b/flaschengeist/plugins/pricelist/pricelist_controller.py @@ -269,12 +269,15 @@ 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 "id" in drink_ingredient_: - drink = get_drink(drink_ingredient_.get("id")) if drink: drink_ingredient.drink_ingredient = drink db.session.add(drink_ingredient) @@ -291,15 +294,16 @@ def set_ingredient(data, volume_id): 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_values = values.get("drink_ingredient") + drink_ingredient_value = values.pop("drink_ingredient") if "extra_ingredient" in values: - extra_ingredient_value = values.get("extra_ingredient") - + extra_ingredient_value = values.pop("extra_ingredient") ingredient = Ingredient(**values) volume = get_volume(volume_id) - if drink_ingredient_values: - ingredient.drink_ingredient = set_drink_ingredient(drink_ingredient_values) + 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")) @@ -315,12 +319,9 @@ def update_ingredient(identifier, data): 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 and isinstance(data.get("drink_ingredient"), dict): - if "drink_ingredient" in data.get("drink_ingredient"): - if "id" in data.get("drink_ingredient").get("drink_ingredient"): - ingredient.drink_ingredient.drink_ingredient = get_drink( - data.get("drink_ingredient").get("drink_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() From 2b35eec0fca91d22048bc54e44b2eda5a57ca9f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Thu, 18 Mar 2021 17:27:19 +0100 Subject: [PATCH 14/15] [run_flaschengeist] fixed bool definition --- run_flaschengeist | 39 +++++++++++++++++++++++---------------- setup.py | 2 +- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/run_flaschengeist b/run_flaschengeist index 1e0e9c5..ce06a1c 100644 --- a/run_flaschengeist +++ b/run_flaschengeist @@ -10,27 +10,33 @@ 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 = "" @@ -45,6 +51,7 @@ class InterfaceGenerator: def _pytype(self, cls): import typing + origin = typing.get_origin(cls) arguments = typing.get_args(cls) @@ -76,19 +83,19 @@ class InterfaceGenerator: import typing if ( - inspect.ismodule(module[1]) - and module[1].__name__.startswith(self.basename) - and module[1].__name__ not in self.known + inspect.ismodule(module[1]) + and module[1].__name__.startswith(self.basename) + and module[1].__name__ not in self.known ): self.known.append(module[1].__name__) for cls in inspect.getmembers(module[1], lambda x: inspect.isclass(x) or inspect.ismodule(x)): self.walker(cls) elif ( - inspect.isclass(module[1]) - and module[1].__module__.startswith(self.basename) - and module[0] not in self.classes - and not module[0].startswith("_") - and hasattr(module[1], "__annotations__") + inspect.isclass(module[1]) + and module[1].__module__.startswith(self.basename) + and module[0] not in self.classes + and not module[0].startswith("_") + and hasattr(module[1], "__annotations__") ): self.this_type = module[0] print("\n\n" + module[0] + "\n") diff --git a/setup.py b/setup.py index cfab611..4be654d 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ setup( "flask_sqlalchemy", "flask_cors", "werkzeug", - mysql_driver + mysql_driver, ], extras_require={"ldap": ["flask_ldapconn", "ldap3"], "test": ["pytest", "coverage"]}, entry_points={ From 8dc2defe0213876d7c54d5ca589a29fe5db0f9b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Thu, 18 Mar 2021 22:34:18 +0100 Subject: [PATCH 15/15] [pricelist] persistent save for visible columns in pricecalculation --- flaschengeist/plugins/pricelist/__init__.py | 37 ++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/flaschengeist/plugins/pricelist/__init__.py b/flaschengeist/plugins/pricelist/__init__.py index e5a15ac..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") @@ -220,3 +223,35 @@ def update_extra_ingredient(identifier): 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()