diff --git a/flaschengeist/plugins/pricelist/__init__.py b/flaschengeist/plugins/pricelist/__init__.py index 3eb7dd2..ef19807 100644 --- a/flaschengeist/plugins/pricelist/__init__.py +++ b/flaschengeist/plugins/pricelist/__init__.py @@ -2,13 +2,13 @@ from flask import Blueprint, jsonify, request, current_app from werkzeug.local import LocalProxy -from werkzeug.exceptions import BadRequest, Forbidden +from werkzeug.exceptions import BadRequest, Forbidden, Unauthorized -from flaschengeist.plugins import Plugin -from flaschengeist.utils.decorators import login_required -from flaschengeist.utils.HTTP import no_content -from flaschengeist.models.session import Session +from flaschengeist import logger from flaschengeist.controller import userController +from flaschengeist.plugins import Plugin +from flaschengeist.utils.decorators import login_required, extract_session +from flaschengeist.utils.HTTP import no_content from . import models from . import pricelist_controller, permissions @@ -16,6 +16,7 @@ from . import pricelist_controller, permissions class PriceListPlugin(Plugin): name = "pricelist" + permissions = permissions.permissions blueprint = Blueprint(name, __name__, url_prefix="/pricelist") plugin = LocalProxy(lambda: current_app.config["FG_PLUGINS"][PriceListPlugin.name]) models = models @@ -29,6 +30,17 @@ class PriceListPlugin(Plugin): @PriceListPlugin.blueprint.route("/drink-types", methods=["GET"]) @PriceListPlugin.blueprint.route("/drink-types/", methods=["GET"]) def get_drink_types(identifier=None): + """Get DrinkType(s) + + Route: ``/pricelist/drink-types`` | Method: ``GET`` + Route: ``/pricelist/drink-types/`` | Method: ``GET`` + + Args: + identifier: If querying a spicific DrinkType + + Returns: + JSON encoded (list of) DrinkType(s) or HTTP-error + """ if identifier is None: result = pricelist_controller.get_drink_types() else: @@ -39,6 +51,18 @@ def get_drink_types(identifier=None): @PriceListPlugin.blueprint.route("/drink-types", methods=["POST"]) @login_required(permission=permissions.CREATE_TYPE) def new_drink_type(current_session): + """Create new DrinkType + + Route ``/pricelist/drink-types`` | Method: ``POST`` + + POST-data: ``{name: string}`` + + Args: + current_session: Session sent with Authorization Header + + Returns: + JSON encoded DrinkType or HTTP-error + """ data = request.get_json() if "name" not in data: raise BadRequest @@ -49,6 +73,19 @@ def new_drink_type(current_session): @PriceListPlugin.blueprint.route("/drink-types/", methods=["PUT"]) @login_required(permission=permissions.EDIT_TYPE) def update_drink_type(identifier, current_session): + """Modify DrinkType + + Route ``/pricelist/drink-types/`` | METHOD ``PUT`` + + POST-data: ``{name: string}`` + + Args: + identifier: Identifier of DrinkType + current_session: Session sent with Authorization Header + + Returns: + JSON encoded DrinkType or HTTP-error + """ data = request.get_json() if "name" not in data: raise BadRequest @@ -59,6 +96,17 @@ def update_drink_type(identifier, current_session): @PriceListPlugin.blueprint.route("/drink-types/", methods=["DELETE"]) @login_required(permission=permissions.DELETE_TYPE) def delete_drink_type(identifier, current_session): + """Delete DrinkType + + Route: ``/pricelist/drink-types/`` | Method: ``DELETE`` + + Args: + identifier: Identifier of DrinkType + current_session: Session sent with Authorization Header + + Returns: + HTTP-NoContent or HTTP-error + """ pricelist_controller.delete_drink_type(identifier) return no_content() @@ -66,6 +114,17 @@ def delete_drink_type(identifier, current_session): @PriceListPlugin.blueprint.route("/tags", methods=["GET"]) @PriceListPlugin.blueprint.route("/tags/", methods=["GET"]) def get_tags(identifier=None): + """Get Tag(s) + + Route: ``/pricelist/tags`` | Method: ``GET`` + Route: ``/pricelist/tags/`` | Method: ``GET`` + + Args: + identifier: Identifier of Tag + + Returns: + JSON encoded (list of) Tag(s) or HTTP-error + """ if identifier: result = pricelist_controller.get_tag(identifier) else: @@ -76,26 +135,58 @@ def get_tags(identifier=None): @PriceListPlugin.blueprint.route("/tags", methods=["POST"]) @login_required(permission=permissions.CREATE_TAG) def new_tag(current_session): + """Create Tag + + Route: ``/pricelist/tags`` | Method: ``POST`` + + POST-data: ``{name: string, color: string}`` + + Args: + current_session: Session sent with Authorization Header + + Returns: + JSON encoded Tag or HTTP-error + """ data = request.get_json() - if "name" not in data: - raise BadRequest - drink_type = pricelist_controller.create_tag(data["name"]) + drink_type = pricelist_controller.create_tag(data) return jsonify(drink_type) @PriceListPlugin.blueprint.route("/tags/", methods=["PUT"]) @login_required(permission=permissions.EDIT_TAG) def update_tag(identifier, current_session): + """Modify Tag + + Route: ``/pricelist/tags/`` | Methods: ``PUT`` + + POST-data: ``{name: string, color: string}`` + + Args: + identifier: Identifier of Tag + current_session: Session sent with Authorization Header + + Returns: + JSON encoded Tag or HTTP-error + """ data = request.get_json() - if "name" not in data: - raise BadRequest - tag = pricelist_controller.rename_tag(identifier, data["name"]) + tag = pricelist_controller.update_tag(identifier, data) return jsonify(tag) @PriceListPlugin.blueprint.route("/tags/", methods=["DELETE"]) @login_required(permission=permissions.DELETE_TAG) def delete_tag(identifier, current_session): + """Delete Tag + + Route: ``/pricelist/tags/`` | Methods: ``DELETE`` + + Args: + identifier: Identifier of Tag + current_session: Session sent with Authorization Header + + Returns: + HTTP-NoContent or HTTP-error + """ pricelist_controller.delete_tag(identifier) return no_content() @@ -103,95 +194,351 @@ def delete_tag(identifier, current_session): @PriceListPlugin.blueprint.route("/drinks", methods=["GET"]) @PriceListPlugin.blueprint.route("/drinks/", methods=["GET"]) def get_drinks(identifier=None): + """Get Drink(s) + + Route: ``/pricelist/drinks`` | Method: ``GET`` + Route: ``/pricelist/drinks/`` | Method: ``GET`` + + Args: + identifier: Identifier of Drink + + Returns: + JSON encoded (list of) Drink(s) or HTTP-error + """ + public = True + try: + extract_session() + public = False + except Unauthorized: + public = True + if identifier: - result = pricelist_controller.get_drink(identifier) + result = pricelist_controller.get_drink(identifier, public=public) else: - result = pricelist_controller.get_drinks() + result = pricelist_controller.get_drinks(public=public) + logger.debug(f"GET drink {result}") return jsonify(result) @PriceListPlugin.blueprint.route("/drinks/search/", methods=["GET"]) def search_drinks(name): - return jsonify(pricelist_controller.get_drinks(name)) + """Search Drink + + Route: ``/pricelist/drinks/search/`` | Method: ``GET`` + + Args: + name: Name to search + + Returns: + JSON encoded list of Drinks or HTTP-error + """ + public = True + try: + extract_session() + public = False + except Unauthorized: + public = True + return jsonify(pricelist_controller.get_drinks(name, public=public)) @PriceListPlugin.blueprint.route("/drinks", methods=["POST"]) @login_required(permission=permissions.CREATE) def create_drink(current_session): + """Create Drink + + Route: ``/pricelist/drinks`` | Method: ``POST`` + + POST-data : + ``{ + article_id?: string + cost_per_package?: float, + cost_per_volume?: float, + name: string, + package_size?: number, + receipt?: list[string], + tags?: list[Tag], + type: DrinkType, + uuid?: string, + volume?: float, + volumes?: list[ + { + ingredients?: list[{ + id: int + drink_ingredient?: { + ingredient_id: int, + volume: float + }, + extra_ingredient?: { + id: number, + } + }], + prices?: list[ + { + price: float + public: boolean + } + ], + volume: float + } + ] + }`` + + Args: + current_session: Session sent with Authorization Header + + Returns: + JSON encoded Drink or HTTP-error + """ data = request.get_json() return jsonify(pricelist_controller.set_drink(data)) @PriceListPlugin.blueprint.route("/drinks/", methods=["PUT"]) -def update_drink(identifier): +@login_required(permission=permissions.EDIT) +def update_drink(identifier, current_session): + """Modify Drink + + Route: ``/pricelist/drinks/`` | Method: ``PUT`` + + POST-data : + ``{ + article_id?: string + cost_per_package?: float, + cost_per_volume?: float, + name: string, + package_size?: number, + receipt?: list[string], + tags?: list[Tag], + type: DrinkType, + uuid?: string, + volume?: float, + volumes?: list[ + { + ingredients?: list[{ + id: int + drink_ingredient?: { + ingredient_id: int, + volume: float + }, + extra_ingredient?: { + id: number, + } + }], + prices?: list[ + { + price: float + public: boolean + } + ], + volume: float + } + ] + }`` + + Args: + identifier: Identifier of Drink + current_session: Session sent with Authorization Header + + Returns: + JSON encoded Drink or HTTP-error + """ data = request.get_json() + logger.debug(f"update drink {data}") return jsonify(pricelist_controller.update_drink(identifier, data)) @PriceListPlugin.blueprint.route("/drinks/", methods=["DELETE"]) -def delete_drink(identifier): +@login_required(permission=permissions.DELETE) +def delete_drink(identifier, current_session): + """Delete Drink + + Route: ``/pricelist/drinks/`` | Method: ``DELETE`` + + Args: + identifier: Identifier of Drink + current_session: Session sent with Authorization Header + + Returns: + HTTP-NoContent or HTTP-error + """ pricelist_controller.delete_drink(identifier) return no_content() @PriceListPlugin.blueprint.route("/prices/", methods=["DELETE"]) -def delete_price(identifier): +@login_required(permission=permissions.DELETE_PRICE) +def delete_price(identifier, current_session): + """Delete Price + + Route: ``/pricelist/prices/`` | Methods: ``DELETE`` + + Args: + identifier: Identiefer of Price + current_session: Session sent with Authorization Header + + Returns: + HTTP-NoContent or HTTP-error + """ pricelist_controller.delete_price(identifier) return no_content() @PriceListPlugin.blueprint.route("/volumes/", methods=["DELETE"]) -def delete_volume(identifier): +@login_required(permission=permissions.DELETE_VOLUME) +def delete_volume(identifier, current_session): + """Delete DrinkPriceVolume + + Route: ``/pricelist/volumes/`` | Method: ``DELETE`` + + Args: + identifier: Identifier of DrinkPriceVolume + current_session: Session sent with Authorization Header + + Returns: + HTTP-NoContent or HTTP-error + """ pricelist_controller.delete_volume(identifier) return no_content() @PriceListPlugin.blueprint.route("/ingredients/extraIngredients", methods=["GET"]) -def get_extra_ingredients(): +@login_required() +def get_extra_ingredients(current_session): + """Get ExtraIngredients + + Route: ``/pricelist/ingredients/extraIngredients`` | Method: ``GET`` + + Args: + current_session: Session sent with Authorization Header + + Returns: + JSON encoded list of ExtraIngredients or HTTP-error + """ return jsonify(pricelist_controller.get_extra_ingredients()) @PriceListPlugin.blueprint.route("/ingredients/", methods=["DELETE"]) -def delete_ingredient(identifier): +@login_required(permission=permissions.DELETE_INGREDIENTS_DRINK) +def delete_ingredient(identifier, current_session): + """Delete Ingredient + + Route: ``/pricelist/ingredients/`` | Method: ``DELETE`` + + Args: + identifier: Identifier of Ingredient + current_session: Session sent with Authorization Header + + Returns: + HTTP-NoContent or HTTP-error + """ pricelist_controller.delete_ingredient(identifier) return no_content() @PriceListPlugin.blueprint.route("/ingredients/extraIngredients", methods=["POST"]) -def set_extra_ingredient(): +@login_required(permission=permissions.EDIT_INGREDIENTS) +def set_extra_ingredient(current_session): + """Create ExtraIngredient + + Route: ``/pricelist/ingredients/extraIngredients`` | Method: ``POST`` + + POST-data: ``{ name: string, price: float }`` + + Args: + current_session: Session sent with Authorization Header + + Returns: + JSON encoded ExtraIngredient or HTTP-error + """ data = request.get_json() return jsonify(pricelist_controller.set_extra_ingredient(data)) @PriceListPlugin.blueprint.route("/ingredients/extraIngredients/", methods=["PUT"]) -def update_extra_ingredient(identifier): +@login_required(permission=permissions.EDIT_INGREDIENTS) +def update_extra_ingredient(identifier, current_session): + """Modify ExtraIngredient + + Route: ``/pricelist/ingredients/extraIngredients`` | Method: ``PUT`` + + POST-data: ``{ name: string, price: float }`` + + Args: + identifier: Identifier of ExtraIngredient + current_session: Session sent with Authorization Header + + Returns: + JSON encoded ExtraIngredient or HTTP-error + """ data = request.get_json() return jsonify(pricelist_controller.update_extra_ingredient(identifier, data)) @PriceListPlugin.blueprint.route("/ingredients/extraIngredients/", methods=["DELETE"]) -def delete_extra_ingredient(identifier): +@login_required(permission=permissions.DELETE_INGREDIENTS) +def delete_extra_ingredient(identifier, current_session): + """Delete ExtraIngredient + + Route: ``/pricelist/ingredients/extraIngredients`` | Method: ``DELETE`` + + Args: + identifier: Identifier of ExtraIngredient + current_session: Session sent with Authorization Header + + Returns: + HTTP-NoContent or HTTP-error + """ pricelist_controller.delete_extra_ingredient(identifier) return no_content() -@PriceListPlugin.blueprint.route("/settings/min_prices", methods=["POST", "GET"]) -def pricelist_settings_min_prices(): - if request.method == "GET": - # TODO: Handle if no prices are set! - return jsonify(PriceListPlugin.plugin.get_setting("min_prices")) - else: - data = request.get_json() - if not isinstance(data, list) or not all(isinstance(n, int) for n in data): - raise BadRequest - data.sort() - PriceListPlugin.plugin.set_setting("min_prices", data) - return no_content() +@PriceListPlugin.blueprint.route("/settings/min_prices", methods=["GET"]) +@login_required() +def get_pricelist_settings_min_prices(current_session): + """Get MinPrices + + Route: ``/pricelist/settings/min_prices`` | Method: ``GET`` + + Args: + current_session: Session sent with Authorization Header + + Returns: + JSON encoded list of MinPrices + """ + # TODO: Handle if no prices are set! + try: + min_prices = PriceListPlugin.plugin.get_setting("min_prices") + except KeyError: + min_prices = [] + return jsonify(min_prices) + + +@PriceListPlugin.blueprint.route("/settings/min_prices", methods=["POST"]) +@login_required(permission=permissions.EDIT_MIN_PRICES) +def post_pricelist_settings_min_prices(current_session): + """Create MinPrices + + Route: ``/pricelist/settings/min_prices`` | Method: ``POST`` + + POST-data: ``list[int]`` + + Args: + current_session: Session sent with Authorization Header + + Returns: + HTTP-NoContent or HTTP-error + """ + data = request.get_json() + if not isinstance(data, list) or not all(isinstance(n, int) for n in data): + raise BadRequest + data.sort() + PriceListPlugin.plugin.set_setting("min_prices", data) + return no_content() @PriceListPlugin.blueprint.route("/users//pricecalc_columns", methods=["GET", "PUT"]) @login_required() -def get_columns(userid, current_session: Session): +def get_columns(userid, current_session): """Get pricecalc_columns of an user Route: ``/users//pricelist/pricecac_columns`` | Method: ``GET`` or ``PUT`` @@ -219,3 +566,112 @@ def get_columns(userid, current_session: Session): user.set_attribute("pricecalc_columns", data) userController.persist() return no_content() + +@PriceListPlugin.blueprint.route("/users//pricecalc_columns_order", methods=["GET", "PUT"]) +@login_required() +def get_columns_order(userid, current_session): + """Get pricecalc_columns_order of an user + + Route: ``/users//pricelist/pricecac_columns_order`` | 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 object 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_order", [])) + else: + data = request.get_json() + if not isinstance(data, list) or not all(isinstance(n, str) for mop in data for n in mop.values()): + raise BadRequest + user.set_attribute("pricecalc_columns_order", data) + userController.persist() + return no_content() + + +@PriceListPlugin.blueprint.route("/users//pricelist", methods=["GET", "PUT"]) +@login_required() +def get_priclist_setting(userid, current_session): + """Get pricelistsetting of an user + + Route: ``/pricelist/user//pricelist`` | Method: ``GET`` or ``PUT`` + + POST-data: on ``PUT`` ``{value: boolean}`` + + Args: + userid: Userid identifying the user + current_session: Session sent wth Authorization Header + + Returns: + GET: JSON object containing the value as boolean or HTTP-error + PUT: HTTP-NoContent 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("pricelist_view", {"value": False})) + else: + data = request.get_json() + if not isinstance(data, dict) or not "value" in data or not isinstance(data["value"], bool): + raise BadRequest + user.set_attribute("pricelist_view", data) + userController.persist() + return no_content() + + +@PriceListPlugin.blueprint.route("/drinks//picture", methods=["POST", "GET", "DELETE"]) +@login_required(permission=permissions.EDIT) +def set_picture(identifier, current_session): + """Get, Create, Delete Drink Picture + + Route: ``/pricelist//picture`` | Method: ``GET,POST,DELETE`` + + POST-data: (if remaining) ``Form-Data: mime: 'image/*'`` + + Args: + identifier: Identifier of Drink + current_session: Session sent with Authorization + + Returns: + Picture or HTTP-error + """ + if request.method == "DELETE": + pricelist_controller.delete_drink_picture(identifier) + return no_content() + + file = request.files.get("file") + if file: + picture = models._Picture() + picture.mimetype = file.content_type + picture.binary = bytearray(file.stream.read()) + return jsonify(pricelist_controller.save_drink_picture(identifier, picture)) + else: + raise BadRequest + + +@PriceListPlugin.blueprint.route("/picture/", methods=["GET"]) +def _get_picture(identifier): + """Get Picture + + Args: + identifier: Identifier of Picture + + Returns: + Picture or HTTP-error + """ + if request.method == "GET": + size = request.args.get("size") + response = pricelist_controller.get_drink_picture(identifier, size) + return response.make_conditional(request) diff --git a/flaschengeist/plugins/pricelist/models.py b/flaschengeist/plugins/pricelist/models.py index 75eaf45..ffde797 100644 --- a/flaschengeist/plugins/pricelist/models.py +++ b/flaschengeist/plugins/pricelist/models.py @@ -26,6 +26,7 @@ class Tag(db.Model, ModelSerializeMixin): __tablename__ = "drink_tag" id: int = db.Column("id", db.Integer, primary_key=True) name: str = db.Column(db.String(30), nullable=False, unique=True) + color: str = db.Column(db.String(7), nullable=False) class DrinkType(db.Model, ModelSerializeMixin): @@ -51,6 +52,9 @@ class DrinkPrice(db.Model, ModelSerializeMixin): public: bool = db.Column(db.Boolean, default=True) description: Optional[str] = db.Column(db.String(30)) + def __repr__(self): + return f"DrinkPric({self.id},{self.price},{self.public},{self.description})" + class ExtraIngredient(db.Model, ModelSerializeMixin): """ @@ -123,6 +127,9 @@ class DrinkPriceVolume(db.Model, ModelSerializeMixin): 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) + def __repr__(self): + return f"DrinkPriceVolume({self.id},{self.drink_id},{self.prices})" + class Drink(db.Model, ModelSerializeMixin): """ @@ -138,7 +145,8 @@ class Drink(db.Model, ModelSerializeMixin): cost_per_volume: Optional[float] = db.Column(db.Numeric(precision=5, scale=3, asdecimal=False)) cost_per_package: Optional[float] = db.Column(db.Numeric(precision=5, scale=3, asdecimal=False)) - uuid = db.Column(db.String(36)) + uuid: str = db.Column(db.String(36)) + receipt: Optional[list[str]] = db.Column(db.PickleType(protocol=4)) _type_id = db.Column("type_id", db.Integer, db.ForeignKey("drink_type.id")) @@ -146,6 +154,9 @@ class Drink(db.Model, ModelSerializeMixin): type: Optional[DrinkType] = db.relationship("DrinkType", foreign_keys=[_type_id]) volumes: list[DrinkPriceVolume] = db.relationship(DrinkPriceVolume) + def __repr__(self): + return f"Drink({self.id},{self.name},{self.volumes})" + class _Picture: """Wrapper class for pictures binaries""" diff --git a/flaschengeist/plugins/pricelist/permissions.py b/flaschengeist/plugins/pricelist/permissions.py index b92ab9a..a94b62b 100644 --- a/flaschengeist/plugins/pricelist/permissions.py +++ b/flaschengeist/plugins/pricelist/permissions.py @@ -10,6 +10,18 @@ DELETE = "drink_delete" CREATE_TAG = "drink_tag_create" """Can create and edit Tags""" +EDIT_PRICE = "edit_price" +DELETE_PRICE = "delete_price" + +EDIT_VOLUME = "edit_volume" +DELETE_VOLUME = "delete_volume" + +EDIT_INGREDIENTS_DRINK = "edit_ingredients_drink" +DELETE_INGREDIENTS_DRINK = "delete_ingredients_drink" + +EDIT_INGREDIENTS = "edit_ingredients" +DELETE_INGREDIENTS = "delete_ingredients" + EDIT_TAG = "drink_tag_edit" DELETE_TAG = "drink_tag_delete" @@ -20,4 +32,6 @@ EDIT_TYPE = "drink_type_edit" DELETE_TYPE = "drink_type_delete" +EDIT_MIN_PRICES = "edit_min_prices" + permissions = [value for key, value in globals().items() if not key.startswith("_")] diff --git a/flaschengeist/plugins/pricelist/pricelist_controller.py b/flaschengeist/plugins/pricelist/pricelist_controller.py index 7de5d66..1c1491e 100644 --- a/flaschengeist/plugins/pricelist/pricelist_controller.py +++ b/flaschengeist/plugins/pricelist/pricelist_controller.py @@ -5,9 +5,11 @@ from uuid import uuid4 from flaschengeist import logger from flaschengeist.config import config from flaschengeist.database import db -from flaschengeist.utils.picture import save_picture, get_picture +from flaschengeist.utils.picture import save_picture, get_picture, delete_picture +from flaschengeist.utils.decorators import extract_session from .models import Drink, DrinkPrice, Ingredient, Tag, DrinkType, DrinkPriceVolume, DrinkIngredient, ExtraIngredient +from .permissions import EDIT_VOLUME, EDIT_PRICE, EDIT_INGREDIENTS_DRINK def update(): @@ -31,9 +33,13 @@ def get_tag(identifier): return ret -def create_tag(name): +def create_tag(data): try: - tag = Tag(name=name) + if "id" in data: + data.pop("id") + allowed_keys = Tag().serialize().keys() + values = {key: value for key, value in data.items() if key in allowed_keys} + tag = Tag(**values) db.session.add(tag) update() return tag @@ -41,9 +47,12 @@ def create_tag(name): raise BadRequest("Name already exists") -def rename_tag(identifier, new_name): +def update_tag(identifier, data): tag = get_tag(identifier) - tag.name = new_name + allowed_keys = Tag().serialize().keys() + values = {key: value for key, value in data.items() if key in allowed_keys} + for key, value in values.items(): + setattr(tag, key, value) try: update() except IntegrityError: @@ -105,13 +114,34 @@ def delete_drink_type(identifier): raise BadRequest("DrinkType still in use") -def get_drinks(name=None): +def _create_public_drink(drink): + _volumes = [] + for volume in drink.volumes: + _prices = [] + for price in volume.prices: + price: DrinkPrice + if price.public: + _prices.append(price) + volume.prices = _prices + if len(volume.prices) > 0: + _volumes.append(volume) + drink.volumes = _volumes + if len(drink.volumes) > 0: + return drink + return None + + +def get_drinks(name=None, public=False): if name: - return Drink.query.filter(Drink.name.contains(name)).all() - return Drink.query.all() + drinks = Drink.query.filter(Drink.name.contains(name)).all() + drinks = Drink.query.all() + if public: + return [_create_public_drink(drink) for drink in drinks if _create_public_drink(drink)] + return drinks -def get_drink(identifier): +def get_drink(identifier, public=False): + drink = None if isinstance(identifier, int): drink = Drink.query.get(identifier) elif isinstance(identifier, str): @@ -120,6 +150,8 @@ def get_drink(identifier): raise BadRequest("Invalid identifier type for Drink") if drink is None: raise NotFound + if public: + return _create_public_drink(drink) return drink @@ -129,11 +161,17 @@ def set_drink(data): def update_drink(identifier, data): try: + session = extract_session() if "id" in data: data.pop("id") volumes = data.pop("volumes") if "volumes" in data else None + tags = [] if "tags" in data: - data.pop("tags") + _tags = data.pop("tags") + if isinstance(_tags, list): + for _tag in _tags: + if isinstance(_tag, dict) and "id" in _tag: + tags.append(get_tag(_tag["id"])) drink_type = data.pop("type") if isinstance(drink_type, dict) and "id" in drink_type: drink_type = drink_type["id"] @@ -149,19 +187,24 @@ def update_drink(identifier, data): if drink_type: drink.type = drink_type - if volumes is not None: - set_volumes(volumes, drink) + if volumes is not None and session.user_.has_permission(EDIT_VOLUME): + drink.volumes = [] + drink.volumes = set_volumes(volumes) + if len(tags) > 0: + drink.tags = tags db.session.commit() return drink except (NotFound, KeyError): raise BadRequest -def set_volumes(volumes, drink): +def set_volumes(volumes): + retVal = [] if not isinstance(volumes, list): raise BadRequest for volume in volumes: - drink.volumes.append(set_volume(volume)) + retVal.append(set_volume(volume)) + return retVal def delete_drink(identifier): @@ -181,6 +224,7 @@ def get_volumes(drink_id=None): def set_volume(data): + session = extract_session() allowed_keys = DrinkPriceVolume().serialize().keys() values = {key: value for key, value in data.items() if key in allowed_keys} prices = None @@ -200,9 +244,9 @@ def set_volume(data): for key, value in values.items(): setattr(volume, key, value if value != "" else None) - if prices: + if prices and session.user_.has_permission(EDIT_PRICE): set_prices(prices, volume) - if ingredients: + if ingredients and session.user_.has_permission(EDIT_INGREDIENTS_DRINK): set_ingredients(ingredients, volume) return volume @@ -244,7 +288,9 @@ def get_prices(volume_id=None): def set_price(data): - allowed_keys = DrinkPrice().serialize().keys() + allowed_keys = list(DrinkPrice().serialize().keys()) + allowed_keys.append("description") + logger.debug(f"allowed_key {allowed_keys}") values = {key: value for key, value in data.items() if key in allowed_keys} price_id = values.pop("id", -1) if price_id < 0: @@ -357,16 +403,33 @@ def delete_extra_ingredient(identifier): def save_drink_picture(identifier, file): drink = get_drink(identifier) - if not drink.uuid: - drink.uuid = str(uuid4()) - db.session.commit() + old_uuid = None + if drink.uuid: + old_uuid = drink.uuid + drink.uuid = str(uuid4()) + db.session.commit() path = config["pricelist"]["path"] save_picture(file, f"{path}/{drink.uuid}") + if old_uuid: + delete_picture(f"{path}/{old_uuid}") + return drink def get_drink_picture(identifier, size=None): - drink = get_drink(identifier) - if not drink.uuid: - raise BadRequest path = config["pricelist"]["path"] - return get_picture(f"{path}/{drink.uuid}") + drink = None + if isinstance(identifier, int): + drink = get_drink(identifier) + if isinstance(identifier, str): + drink = Drink.query.filter(Drink.uuid == identifier).one_or_none() + if drink: + return get_picture(f"{path}/{drink.uuid}", size) + raise FileNotFoundError + + +def delete_drink_picture(identifier): + drink = get_drink(identifier) + if drink.uuid: + delete_picture(f"{config['pricelist']['path']}/{drink.uuid}") + drink.uuid = None + db.session.commit() diff --git a/flaschengeist/utils/no-image.png b/flaschengeist/utils/no-image.png new file mode 100644 index 0000000..240507b Binary files /dev/null and b/flaschengeist/utils/no-image.png differ diff --git a/flaschengeist/utils/picture.py b/flaschengeist/utils/picture.py index 968e764..3f2dc40 100644 --- a/flaschengeist/utils/picture.py +++ b/flaschengeist/utils/picture.py @@ -1,7 +1,8 @@ -import os, sys +import os, sys, shutil, io from PIL import Image from flask import Response from werkzeug.exceptions import BadRequest +from ..utils.HTTP import no_content thumbnail_sizes = ((32, 32), (64, 64), (128, 128), (256, 256), (512, 512)) @@ -19,7 +20,6 @@ def save_picture(picture, path): if file_type != "png": image.save(f"{filename}.png", "PNG") os.remove(f"{filename}.{file_type}") - image.show() for thumbnail_size in thumbnail_sizes: work_image = image.copy() work_image.thumbnail(thumbnail_size) @@ -27,12 +27,29 @@ def save_picture(picture, path): def get_picture(path, size=None): - if size: - with open(f"{path}/drink-{size}.png", "rb") as file: - image = file.read() - else: - with open(f"{path}/drink.png", "rb") as file: - image = file.read() - response = Response(image, mimetype="image/png") - response.add_etag() - return response + try: + if size: + if os.path.isfile(f"{path}/drink-{size}.png"): + with open(f"{path}/drink-{size}.png", "rb") as file: + image = file.read() + else: + _image = Image.open(f"{path}/drink.png") + _image.thumbnail((int(size), int(size))) + with io.BytesIO() as file: + _image.save(file, format="PNG") + image = file.getvalue() + else: + with open(f"{path}/drink.png", "rb") as file: + image = file.read() + response = Response(image, mimetype="image/png") + response.add_etag() + return response + except: + raise FileNotFoundError + + +def delete_picture(path): + try: + shutil.rmtree(path) + except FileNotFoundError: + pass