From 6dbb135621ab69b4a6a6558fca3efb1ce415b249 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Thu, 25 Mar 2021 20:32:21 +0100 Subject: [PATCH 01/17] [pricelist] delete picture (not ready yet) --- flaschengeist/plugins/pricelist/__init__.py | 28 +++++++++++++++- .../plugins/pricelist/pricelist_controller.py | 12 +++++-- flaschengeist/utils/picture.py | 32 +++++++++++++------ 3 files changed, 60 insertions(+), 12 deletions(-) diff --git a/flaschengeist/plugins/pricelist/__init__.py b/flaschengeist/plugins/pricelist/__init__.py index 5a8a831..d53dd30 100644 --- a/flaschengeist/plugins/pricelist/__init__.py +++ b/flaschengeist/plugins/pricelist/__init__.py @@ -11,7 +11,7 @@ from . import models from . import pricelist_controller, permissions from ...controller import userController from ...models.session import Session -from ...utils.HTTP import no_content +from ...utils.HTTP import no_content, created pricelist_bp = Blueprint("pricelist", __name__, url_prefix="/pricelist") @@ -217,3 +217,29 @@ def get_columns(userid, current_session: Session): user.set_attribute("pricecalc_columns", data) userController.persist() return no_content() + +@pricelist_bp.route("/drinks//picture", methods=["POST", "GET", "DELETE"]) +def set_picture(identifier): + + if request.method == "GET": + try: + size = request.args.get("size") + response = pricelist_controller.get_drink_picture(identifier, size) + return response.make_conditional(request) + except FileNotFoundError: + return no_content() + + 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()) + pricelist_controller.save_drink_picture(identifier, picture) + else: + raise BadRequest + + return created() \ No newline at end of file diff --git a/flaschengeist/plugins/pricelist/pricelist_controller.py b/flaschengeist/plugins/pricelist/pricelist_controller.py index d2a88be..e2c8469 100644 --- a/flaschengeist/plugins/pricelist/pricelist_controller.py +++ b/flaschengeist/plugins/pricelist/pricelist_controller.py @@ -6,7 +6,7 @@ from flaschengeist.config import config from flaschengeist.database import db from .models import Drink, DrinkPrice, Ingredient, Tag, DrinkType, DrinkPriceVolume, DrinkIngredient, ExtraIngredient -from flaschengeist.utils.picture import save_picture, get_picture +from flaschengeist.utils.picture import save_picture, get_picture, delete_picture from uuid import uuid4 @@ -383,6 +383,14 @@ def save_drink_picture(identifier, file): def get_drink_picture(identifier, size=None): drink = get_drink(identifier) if not drink.uuid: - raise BadRequest + raise FileNotFoundError path = config["pricelist"]["path"] return get_picture(f"{path}/{drink.uuid}") + +def delete_drink_picture(identifier): + drink = get_drink(identifier) + if not drink.uuid: + delete_picture(f"{config['pricelist']['path']}/{drink.uuid}") + drink.uuid = None + db.session.commit() + diff --git a/flaschengeist/utils/picture.py b/flaschengeist/utils/picture.py index 968e764..529d322 100644 --- a/flaschengeist/utils/picture.py +++ b/flaschengeist/utils/picture.py @@ -2,6 +2,7 @@ import os, sys 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)) @@ -27,12 +28,25 @@ 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((size, size)) + image = bytearray() + _image.save(bytearray, format='PNG') + 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): + os.remove(path) + From 6fce88c120f5fcf0c974f36d51a252a95b4e77db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Thu, 25 Mar 2021 23:05:17 +0100 Subject: [PATCH 02/17] [pricelist] now can delete pictures, add no-image --- .../plugins/pricelist/pricelist_controller.py | 6 +++--- flaschengeist/utils/no-image.png | Bin 0 -> 47605 bytes flaschengeist/utils/picture.py | 18 +++++++++++++----- 3 files changed, 16 insertions(+), 8 deletions(-) create mode 100644 flaschengeist/utils/no-image.png diff --git a/flaschengeist/plugins/pricelist/pricelist_controller.py b/flaschengeist/plugins/pricelist/pricelist_controller.py index e2c8469..71f9488 100644 --- a/flaschengeist/plugins/pricelist/pricelist_controller.py +++ b/flaschengeist/plugins/pricelist/pricelist_controller.py @@ -6,7 +6,7 @@ from flaschengeist.config import config from flaschengeist.database import db from .models import Drink, DrinkPrice, Ingredient, Tag, DrinkType, DrinkPriceVolume, DrinkIngredient, ExtraIngredient -from flaschengeist.utils.picture import save_picture, get_picture, delete_picture +from flaschengeist.utils.picture import save_picture, get_picture, delete_picture, get_no_image from uuid import uuid4 @@ -383,13 +383,13 @@ def save_drink_picture(identifier, file): def get_drink_picture(identifier, size=None): drink = get_drink(identifier) if not drink.uuid: - raise FileNotFoundError + return get_no_image() path = config["pricelist"]["path"] return get_picture(f"{path}/{drink.uuid}") def delete_drink_picture(identifier): drink = get_drink(identifier) - if not drink.uuid: + 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 0000000000000000000000000000000000000000..240507b03e790f6871db20f36b4cc7f0bc2c55d5 GIT binary patch literal 47605 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 RMKb(jq-UyoPun5l{{c1I8T0@E literal 0 HcmV?d00001 diff --git a/flaschengeist/utils/picture.py b/flaschengeist/utils/picture.py index 529d322..89de66f 100644 --- a/flaschengeist/utils/picture.py +++ b/flaschengeist/utils/picture.py @@ -1,4 +1,4 @@ -import os, sys +import os, sys, shutil from PIL import Image from flask import Response from werkzeug.exceptions import BadRequest @@ -20,12 +20,18 @@ 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) work_image.save(f"{filename}-{thumbnail_size[0]}.png", "PNG") +def get_no_image(): + path = os.path.dirname(__file__) + with open(f"{path}/no-image.png", "rb") as file: + image = file.read() + response = Response(image, mimetype="image/png") + response.add_etag() + return response def get_picture(path, size=None): try: @@ -45,8 +51,10 @@ def get_picture(path, size=None): response.add_etag() return response except: - raise FileNotFoundError + get_no_image() + + + def delete_picture(path): - os.remove(path) - + shutil.rmtree(path) From 2ae8bc7e0c9e06d055c98b24cf427187543f853f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Sun, 28 Mar 2021 12:47:02 +0200 Subject: [PATCH 03/17] [pricelist][picture] delete picture, if not found --- flaschengeist/plugins/pricelist/__init__.py | 21 +++++++------- flaschengeist/plugins/pricelist/models.py | 2 +- .../plugins/pricelist/pricelist_controller.py | 29 ++++++++++++++----- flaschengeist/utils/picture.py | 15 ++++------ 4 files changed, 37 insertions(+), 30 deletions(-) diff --git a/flaschengeist/plugins/pricelist/__init__.py b/flaschengeist/plugins/pricelist/__init__.py index d53dd30..c20e564 100644 --- a/flaschengeist/plugins/pricelist/__init__.py +++ b/flaschengeist/plugins/pricelist/__init__.py @@ -6,12 +6,13 @@ from http.client import NO_CONTENT from flaschengeist.plugins import Plugin from flaschengeist.utils.decorators import login_required from werkzeug.exceptions import BadRequest, Forbidden +from flaschengeist.config import config from . import models from . import pricelist_controller, permissions from ...controller import userController from ...models.session import Session -from ...utils.HTTP import no_content, created +from ...utils.HTTP import no_content pricelist_bp = Blueprint("pricelist", __name__, url_prefix="/pricelist") @@ -221,14 +222,6 @@ def get_columns(userid, current_session: Session): @pricelist_bp.route("/drinks//picture", methods=["POST", "GET", "DELETE"]) def set_picture(identifier): - if request.method == "GET": - try: - size = request.args.get("size") - response = pricelist_controller.get_drink_picture(identifier, size) - return response.make_conditional(request) - except FileNotFoundError: - return no_content() - if request.method == "DELETE": pricelist_controller.delete_drink_picture(identifier) return no_content() @@ -238,8 +231,14 @@ def set_picture(identifier): picture = models._Picture() picture.mimetype = file.content_type picture.binary = bytearray(file.stream.read()) - pricelist_controller.save_drink_picture(identifier, picture) + return jsonify(pricelist_controller.save_drink_picture(identifier, picture)) else: raise BadRequest - return created() \ No newline at end of file +@pricelist_bp.route("/picture/", methods=["GET"]) +def _get_picture(identifier): + if request.method == "GET": + size = request.args.get("size") + path = config["pricelist"]["path"] + response = pricelist_controller.get_drink_picture(identifier, size) + return response.make_conditional(request) \ No newline at end of file diff --git a/flaschengeist/plugins/pricelist/models.py b/flaschengeist/plugins/pricelist/models.py index 3d7c452..ff3fb72 100644 --- a/flaschengeist/plugins/pricelist/models.py +++ b/flaschengeist/plugins/pricelist/models.py @@ -137,7 +137,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)) - uuid = db.Column(db.String(36)) + uuid: str = db.Column(db.String(36)) _type_id = db.Column("type_id", db.Integer, db.ForeignKey("drink_type.id")) diff --git a/flaschengeist/plugins/pricelist/pricelist_controller.py b/flaschengeist/plugins/pricelist/pricelist_controller.py index 71f9488..0ce42dd 100644 --- a/flaschengeist/plugins/pricelist/pricelist_controller.py +++ b/flaschengeist/plugins/pricelist/pricelist_controller.py @@ -6,7 +6,7 @@ from flaschengeist.config import config from flaschengeist.database import db from .models import Drink, DrinkPrice, Ingredient, Tag, DrinkType, DrinkPriceVolume, DrinkIngredient, ExtraIngredient -from flaschengeist.utils.picture import save_picture, get_picture, delete_picture, get_no_image +from flaschengeist.utils.picture import save_picture, get_picture, delete_picture from uuid import uuid4 @@ -373,19 +373,32 @@ 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: - return get_no_image() 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() + try: + if drink: + return get_picture(f"{path}/{drink.uuid}", size) + except FileNotFoundError: + drink.uuid = None + db.session.commit() + raise FileNotFoundError def delete_drink_picture(identifier): drink = get_drink(identifier) diff --git a/flaschengeist/utils/picture.py b/flaschengeist/utils/picture.py index 89de66f..dc896f5 100644 --- a/flaschengeist/utils/picture.py +++ b/flaschengeist/utils/picture.py @@ -25,14 +25,6 @@ def save_picture(picture, path): work_image.thumbnail(thumbnail_size) work_image.save(f"{filename}-{thumbnail_size[0]}.png", "PNG") -def get_no_image(): - path = os.path.dirname(__file__) - with open(f"{path}/no-image.png", "rb") as file: - image = file.read() - response = Response(image, mimetype="image/png") - response.add_etag() - return response - def get_picture(path, size=None): try: if size: @@ -51,10 +43,13 @@ def get_picture(path, size=None): response.add_etag() return response except: - get_no_image() + raise FileNotFoundError def delete_picture(path): - shutil.rmtree(path) + try: + shutil.rmtree(path) + except FileNotFoundError: + pass From e8c9c6e66ca73b96b227a78224e33c3e1abcaee1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Sun, 28 Mar 2021 16:41:20 +0200 Subject: [PATCH 04/17] [pricelist][drinks] return only public drinkprices if not logged in --- flaschengeist/plugins/pricelist/__init__.py | 20 +++++++---- .../plugins/pricelist/pricelist_controller.py | 35 +++++++++++++++---- 2 files changed, 42 insertions(+), 13 deletions(-) diff --git a/flaschengeist/plugins/pricelist/__init__.py b/flaschengeist/plugins/pricelist/__init__.py index c20e564..58c1ba8 100644 --- a/flaschengeist/plugins/pricelist/__init__.py +++ b/flaschengeist/plugins/pricelist/__init__.py @@ -4,8 +4,8 @@ from flask import Blueprint, jsonify, request from http.client import NO_CONTENT from flaschengeist.plugins import Plugin -from flaschengeist.utils.decorators import login_required -from werkzeug.exceptions import BadRequest, Forbidden +from flaschengeist.utils.decorators import login_required,extract_session +from werkzeug.exceptions import BadRequest, Forbidden, Unauthorized from flaschengeist.config import config from . import models @@ -103,12 +103,18 @@ def delete_tag(identifier, current_session): @pricelist_bp.route("/drinks", methods=["GET"]) @pricelist_bp.route("/drinks/", methods=["GET"]) def get_drinks(identifier=None): - if identifier: - result = pricelist_controller.get_drink(identifier) - else: - result = pricelist_controller.get_drinks() - return jsonify(result) + public = True + try: + extract_session() + public = False + except Unauthorized: + public = True + if identifier: + result = pricelist_controller.get_drink(identifier, public=public) + else: + result = pricelist_controller.get_drinks(public=public) + return jsonify(result) @pricelist_bp.route("/drinks/search/", methods=["GET"]) def search_drinks(name): diff --git a/flaschengeist/plugins/pricelist/pricelist_controller.py b/flaschengeist/plugins/pricelist/pricelist_controller.py index 0ce42dd..925a1b2 100644 --- a/flaschengeist/plugins/pricelist/pricelist_controller.py +++ b/flaschengeist/plugins/pricelist/pricelist_controller.py @@ -105,21 +105,44 @@ def delete_drink_type(identifier): except IntegrityError: raise BadRequest("DrinkType still in use") +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): +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): - return Drink.query.get(identifier) + drink = Drink.query.get(identifier) elif isinstance(identifier, str): - return Drink.query.filter(Tag.name == identifier).one_or_none() + drink = Drink.query.filter(Tag.name == identifier).one_or_none() else: logger.debug("Invalid identifier type for Drink") raise BadRequest + if drink: + if public: + return _create_public_drink(drink) + return drink raise NotFound From faf5b0b8d0ba494b8b68d1d703ded7fda6eb9d70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Mon, 29 Mar 2021 12:41:29 +0200 Subject: [PATCH 05/17] [pricelist] add receipts --- flaschengeist/plugins/pricelist/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/flaschengeist/plugins/pricelist/models.py b/flaschengeist/plugins/pricelist/models.py index ff3fb72..b547f34 100644 --- a/flaschengeist/plugins/pricelist/models.py +++ b/flaschengeist/plugins/pricelist/models.py @@ -138,6 +138,7 @@ class Drink(db.Model, ModelSerializeMixin): cost_price_package_netto: Optional[float] = db.Column(db.Numeric(precision=5, scale=3, asdecimal=False)) uuid: str = db.Column(db.String(36)) + receipt: Optional[str] = db.Column(db.String) _type_id = db.Column("type_id", db.Integer, db.ForeignKey("drink_type.id")) From b2d843169797fb1359aba5d86f787cb94d108bbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Mon, 29 Mar 2021 20:26:15 +0200 Subject: [PATCH 06/17] [pricelist][fix] add permission to plugin --- flaschengeist/plugins/pricelist/__init__.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/flaschengeist/plugins/pricelist/__init__.py b/flaschengeist/plugins/pricelist/__init__.py index 35c3ce1..9d5151a 100644 --- a/flaschengeist/plugins/pricelist/__init__.py +++ b/flaschengeist/plugins/pricelist/__init__.py @@ -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 @@ -186,7 +187,11 @@ def delete_extra_ingredient(identifier): def pricelist_settings_min_prices(): if request.method == "GET": # TODO: Handle if no prices are set! - return jsonify(PriceListPlugin.plugin.get_setting("min_prices")) + try: + min_prices = PriceListPlugin.plugin.get_setting("min_prices") + except KeyError: + min_prices = [] + return jsonify(min_prices) else: data = request.get_json() if not isinstance(data, list) or not all(isinstance(n, int) for n in data): @@ -227,7 +232,7 @@ def get_columns(userid, current_session: Session): userController.persist() return no_content() -@PriceListPlugin.route("/drinks//picture", methods=["POST", "GET", "DELETE"]) +@PriceListPlugin.blueprint.route("/drinks//picture", methods=["POST", "GET", "DELETE"]) def set_picture(identifier): if request.method == "DELETE": @@ -243,7 +248,7 @@ def set_picture(identifier): else: raise BadRequest -@PriceListPlugin.route("/picture/", methods=["GET"]) +@PriceListPlugin.blueprint.route("/picture/", methods=["GET"]) def _get_picture(identifier): if request.method == "GET": size = request.args.get("size") From bcf1941a810545fd7ae73e9a88140da99f899f7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Mon, 29 Mar 2021 21:28:48 +0200 Subject: [PATCH 07/17] [pricelist] fix some merge issues --- flaschengeist/plugins/pricelist/__init__.py | 1 - flaschengeist/plugins/pricelist/pricelist_controller.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/flaschengeist/plugins/pricelist/__init__.py b/flaschengeist/plugins/pricelist/__init__.py index 9d5151a..dff3bb6 100644 --- a/flaschengeist/plugins/pricelist/__init__.py +++ b/flaschengeist/plugins/pricelist/__init__.py @@ -252,6 +252,5 @@ def set_picture(identifier): def _get_picture(identifier): if request.method == "GET": size = request.args.get("size") - path = PriceListPlugin.plugin["path"] response = pricelist_controller.get_drink_picture(identifier, size) return response.make_conditional(request) \ No newline at end of file diff --git a/flaschengeist/plugins/pricelist/pricelist_controller.py b/flaschengeist/plugins/pricelist/pricelist_controller.py index 6a115f5..6713b67 100644 --- a/flaschengeist/plugins/pricelist/pricelist_controller.py +++ b/flaschengeist/plugins/pricelist/pricelist_controller.py @@ -5,7 +5,7 @@ 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 .models import Drink, DrinkPrice, Ingredient, Tag, DrinkType, DrinkPriceVolume, DrinkIngredient, ExtraIngredient From 15c7a56d564721723cfae2b6e2f64cc7cbd8582d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Mon, 29 Mar 2021 22:34:05 +0200 Subject: [PATCH 08/17] [pricelist] receipt as list of strings --- flaschengeist/plugins/pricelist/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flaschengeist/plugins/pricelist/models.py b/flaschengeist/plugins/pricelist/models.py index d392168..99ef37c 100644 --- a/flaschengeist/plugins/pricelist/models.py +++ b/flaschengeist/plugins/pricelist/models.py @@ -139,7 +139,7 @@ class Drink(db.Model, ModelSerializeMixin): cost_per_package: Optional[float] = db.Column(db.Numeric(precision=5, scale=3, asdecimal=False)) uuid: str = db.Column(db.String(36)) - receipt: Optional[str] = db.Column(db.String) + receipt: Optional[list[str]] = db.Column(db.PickleType(protocol=4)) _type_id = db.Column("type_id", db.Integer, db.ForeignKey("drink_type.id")) From 5c688df39291aef12782426d2057a95dc27ad4c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Thu, 1 Apr 2021 22:45:28 +0200 Subject: [PATCH 09/17] [pricelist][tags] change model of tags, fixed tags on updateDrink --- flaschengeist/plugins/pricelist/__init__.py | 15 ++++---- flaschengeist/plugins/pricelist/models.py | 10 ++++++ .../plugins/pricelist/pricelist_controller.py | 34 ++++++++++++++----- 3 files changed, 44 insertions(+), 15 deletions(-) diff --git a/flaschengeist/plugins/pricelist/__init__.py b/flaschengeist/plugins/pricelist/__init__.py index dff3bb6..117aa3f 100644 --- a/flaschengeist/plugins/pricelist/__init__.py +++ b/flaschengeist/plugins/pricelist/__init__.py @@ -4,6 +4,7 @@ from flask import Blueprint, jsonify, request, current_app from werkzeug.local import LocalProxy from werkzeug.exceptions import BadRequest, Forbidden, Unauthorized +from flaschengeist import logger from flaschengeist.plugins import Plugin from flaschengeist.utils.decorators import login_required, extract_session from flaschengeist.utils.HTTP import no_content @@ -78,9 +79,7 @@ def get_tags(identifier=None): @login_required(permission=permissions.CREATE_TAG) def new_tag(current_session): 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) @@ -88,9 +87,7 @@ def new_tag(current_session): @login_required(permission=permissions.EDIT_TAG) def update_tag(identifier, current_session): 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) @@ -115,6 +112,7 @@ def get_drinks(identifier=None): result = pricelist_controller.get_drink(identifier, public=public) else: result = pricelist_controller.get_drinks(public=public) + logger.debug(f"GET drink {result}") return jsonify(result) @@ -133,6 +131,7 @@ def create_drink(current_session): @PriceListPlugin.blueprint.route("/drinks/", methods=["PUT"]) def update_drink(identifier): data = request.get_json() + logger.debug(f"update drink {data}") return jsonify(pricelist_controller.update_drink(identifier, data)) @@ -232,6 +231,7 @@ def get_columns(userid, current_session: Session): userController.persist() return no_content() + @PriceListPlugin.blueprint.route("/drinks//picture", methods=["POST", "GET", "DELETE"]) def set_picture(identifier): @@ -248,9 +248,10 @@ def set_picture(identifier): else: raise BadRequest + @PriceListPlugin.blueprint.route("/picture/", methods=["GET"]) def _get_picture(identifier): if request.method == "GET": size = request.args.get("size") response = pricelist_controller.get_drink_picture(identifier, size) - return response.make_conditional(request) \ No newline at end of file + return response.make_conditional(request) diff --git a/flaschengeist/plugins/pricelist/models.py b/flaschengeist/plugins/pricelist/models.py index 99ef37c..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): """ @@ -147,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/pricelist_controller.py b/flaschengeist/plugins/pricelist/pricelist_controller.py index 6713b67..fbc6965 100644 --- a/flaschengeist/plugins/pricelist/pricelist_controller.py +++ b/flaschengeist/plugins/pricelist/pricelist_controller.py @@ -31,9 +31,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 +45,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: @@ -104,6 +111,7 @@ def delete_drink_type(identifier): except IntegrityError: raise BadRequest("DrinkType still in use") + def _create_public_drink(drink): _volumes = [] for volume in drink.volumes: @@ -120,6 +128,7 @@ def _create_public_drink(drink): return drink return None + def get_drinks(name=None, public=False): if name: drinks = Drink.query.filter(Drink.name.contains(name)).all() @@ -153,8 +162,13 @@ def update_drink(identifier, data): 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"] @@ -172,6 +186,8 @@ def update_drink(identifier, data): drink.type = drink_type if volumes is not None: set_volumes(volumes, drink) + if len(tags) > 0: + drink.tags = tags db.session.commit() return drink except (NotFound, KeyError): @@ -265,7 +281,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: @@ -396,7 +414,7 @@ def get_drink_picture(identifier, size=None): if isinstance(identifier, int): drink = get_drink(identifier) if isinstance(identifier, str): - drink = Drink.query.filter(Drink.uuid==identifier).one_or_none() + drink = Drink.query.filter(Drink.uuid == identifier).one_or_none() try: if drink: return get_picture(f"{path}/{drink.uuid}", size) @@ -405,10 +423,10 @@ def get_drink_picture(identifier, size=None): db.session.commit() 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() - From 62948cd59178e98d52c9653a850d93ea35f9dcf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Wed, 14 Apr 2021 20:03:44 +0200 Subject: [PATCH 10/17] [picture][fix] any size for thumbnail --- .../plugins/pricelist/pricelist_controller.py | 10 +++------- flaschengeist/utils/picture.py | 12 ++++++------ 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/flaschengeist/plugins/pricelist/pricelist_controller.py b/flaschengeist/plugins/pricelist/pricelist_controller.py index fbc6965..32d2c33 100644 --- a/flaschengeist/plugins/pricelist/pricelist_controller.py +++ b/flaschengeist/plugins/pricelist/pricelist_controller.py @@ -415,13 +415,9 @@ def get_drink_picture(identifier, size=None): drink = get_drink(identifier) if isinstance(identifier, str): drink = Drink.query.filter(Drink.uuid == identifier).one_or_none() - try: - if drink: - return get_picture(f"{path}/{drink.uuid}", size) - except FileNotFoundError: - drink.uuid = None - db.session.commit() - raise FileNotFoundError + if drink: + return get_picture(f"{path}/{drink.uuid}", size) + raise FileNotFoundError def delete_drink_picture(identifier): diff --git a/flaschengeist/utils/picture.py b/flaschengeist/utils/picture.py index dc896f5..3f2dc40 100644 --- a/flaschengeist/utils/picture.py +++ b/flaschengeist/utils/picture.py @@ -1,4 +1,4 @@ -import os, sys, shutil +import os, sys, shutil, io from PIL import Image from flask import Response from werkzeug.exceptions import BadRequest @@ -25,6 +25,7 @@ def save_picture(picture, path): work_image.thumbnail(thumbnail_size) work_image.save(f"{filename}-{thumbnail_size[0]}.png", "PNG") + def get_picture(path, size=None): try: if size: @@ -33,9 +34,10 @@ def get_picture(path, size=None): image = file.read() else: _image = Image.open(f"{path}/drink.png") - _image.thumbnail((size, size)) - image = bytearray() - _image.save(bytearray, format='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() @@ -46,8 +48,6 @@ def get_picture(path, size=None): raise FileNotFoundError - - def delete_picture(path): try: shutil.rmtree(path) From 2d45c0dab9a46648744c3c590ea8bb6fe7e75e12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Wed, 14 Apr 2021 20:11:23 +0200 Subject: [PATCH 11/17] [pricelist] delete visibleColums --- flaschengeist/plugins/pricelist/__init__.py | 34 +-------------------- 1 file changed, 1 insertion(+), 33 deletions(-) diff --git a/flaschengeist/plugins/pricelist/__init__.py b/flaschengeist/plugins/pricelist/__init__.py index 117aa3f..0f56111 100644 --- a/flaschengeist/plugins/pricelist/__init__.py +++ b/flaschengeist/plugins/pricelist/__init__.py @@ -198,39 +198,7 @@ def pricelist_settings_min_prices(): 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): - """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() - + @PriceListPlugin.blueprint.route("/drinks//picture", methods=["POST", "GET", "DELETE"]) def set_picture(identifier): From 1d36c3ef6c069f50d52f8dc5ec19133bd6e646d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Wed, 14 Apr 2021 22:42:57 +0200 Subject: [PATCH 12/17] [pricelist] add more permissions --- flaschengeist/plugins/pricelist/__init__.py | 415 ++++++++++++++++-- .../plugins/pricelist/permissions.py | 14 + .../plugins/pricelist/pricelist_controller.py | 10 +- 3 files changed, 406 insertions(+), 33 deletions(-) diff --git a/flaschengeist/plugins/pricelist/__init__.py b/flaschengeist/plugins/pricelist/__init__.py index 0f56111..998e046 100644 --- a/flaschengeist/plugins/pricelist/__init__.py +++ b/flaschengeist/plugins/pricelist/__init__.py @@ -8,8 +8,6 @@ from flaschengeist import logger from flaschengeist.plugins import Plugin from flaschengeist.utils.decorators import login_required, extract_session from flaschengeist.utils.HTTP import no_content -from flaschengeist.models.session import Session -from flaschengeist.controller import userController from . import models from . import pricelist_controller, permissions @@ -31,6 +29,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: @@ -41,6 +50,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 @@ -51,6 +72,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 @@ -61,6 +95,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() @@ -68,6 +113,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: @@ -78,6 +134,18 @@ 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() drink_type = pricelist_controller.create_tag(data) return jsonify(drink_type) @@ -86,6 +154,19 @@ def new_tag(current_session): @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() tag = pricelist_controller.update_tag(identifier, data) return jsonify(tag) @@ -94,6 +175,17 @@ def update_tag(identifier, current_session): @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() @@ -101,6 +193,17 @@ 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() @@ -118,91 +221,335 @@ def get_drinks(identifier=None): @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! - try: - min_prices = PriceListPlugin.plugin.get_setting("min_prices") - except KeyError: - min_prices = [] - return jsonify(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("/drinks//picture", methods=["POST", "GET", "DELETE"]) -def set_picture(identifier): +@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() @@ -219,6 +566,14 @@ def set_picture(identifier): @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) 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 32d2c33..ca18aaf 100644 --- a/flaschengeist/plugins/pricelist/pricelist_controller.py +++ b/flaschengeist/plugins/pricelist/pricelist_controller.py @@ -6,8 +6,10 @@ from flaschengeist import logger from flaschengeist.config import config from flaschengeist.database import db 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(): @@ -159,6 +161,7 @@ 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 @@ -184,7 +187,7 @@ def update_drink(identifier, data): if drink_type: drink.type = drink_type - if volumes is not None: + if volumes is not None and session.user_.has_permission(EDIT_VOLUME): set_volumes(volumes, drink) if len(tags) > 0: drink.tags = tags @@ -218,6 +221,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 @@ -237,9 +241,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 From 32ad4471c6553387202765f48e8774a120fe5075 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Wed, 14 Apr 2021 22:43:28 +0200 Subject: [PATCH 13/17] [pricelist] black code --- flaschengeist/plugins/pricelist/__init__.py | 159 ++++++++++---------- 1 file changed, 80 insertions(+), 79 deletions(-) diff --git a/flaschengeist/plugins/pricelist/__init__.py b/flaschengeist/plugins/pricelist/__init__.py index 998e046..fce9659 100644 --- a/flaschengeist/plugins/pricelist/__init__.py +++ b/flaschengeist/plugins/pricelist/__init__.py @@ -245,48 +245,48 @@ def search_drinks(name): def create_drink(current_session): """Create Drink - Route: ``/pricelist/drinks`` | Method: ``POST`` + 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 - } - ] -}`` + 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 + Args: + current_session: Session sent with Authorization Header - Returns: - JSON encoded Drink or HTTP-error + Returns: + JSON encoded Drink or HTTP-error """ data = request.get_json() return jsonify(pricelist_controller.set_drink(data)) @@ -297,49 +297,49 @@ def create_drink(current_session): def update_drink(identifier, current_session): """Modify Drink - Route: ``/pricelist/drinks/`` | Method: ``PUT`` + 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 - } - ] -}`` + 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 + Args: + identifier: Identifier of Drink + current_session: Session sent with Authorization Header - Returns: - JSON encoded Drink or HTTP-error + Returns: + JSON encoded Drink or HTTP-error """ data = request.get_json() logger.debug(f"update drink {data}") @@ -511,6 +511,7 @@ def get_pricelist_settings_min_prices(current_session): 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): From 0630b5183d9a0a4908580049515628cd4cebfec7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Thu, 15 Apr 2021 15:23:37 +0200 Subject: [PATCH 14/17] [pricelist] fix bug set no volumes are set --- flaschengeist/plugins/pricelist/pricelist_controller.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/flaschengeist/plugins/pricelist/pricelist_controller.py b/flaschengeist/plugins/pricelist/pricelist_controller.py index ca18aaf..1c1491e 100644 --- a/flaschengeist/plugins/pricelist/pricelist_controller.py +++ b/flaschengeist/plugins/pricelist/pricelist_controller.py @@ -188,7 +188,8 @@ def update_drink(identifier, data): if drink_type: drink.type = drink_type if volumes is not None and session.user_.has_permission(EDIT_VOLUME): - set_volumes(volumes, drink) + drink.volumes = [] + drink.volumes = set_volumes(volumes) if len(tags) > 0: drink.tags = tags db.session.commit() @@ -197,11 +198,13 @@ def update_drink(identifier, data): 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): From 2d31cda66570dbcd33916a6b8d7e90bb6cc6f362 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Thu, 15 Apr 2021 22:05:34 +0200 Subject: [PATCH 15/17] Revert "[pricelist] delete visibleColums" This reverts commit 2d45c0dab9a46648744c3c590ea8bb6fe7e75e12. Conflicts: flaschengeist/plugins/pricelist/__init__.py --- flaschengeist/plugins/pricelist/__init__.py | 34 +++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/flaschengeist/plugins/pricelist/__init__.py b/flaschengeist/plugins/pricelist/__init__.py index fce9659..e7a25e3 100644 --- a/flaschengeist/plugins/pricelist/__init__.py +++ b/flaschengeist/plugins/pricelist/__init__.py @@ -5,6 +5,7 @@ from werkzeug.local import LocalProxy from werkzeug.exceptions import BadRequest, Forbidden, Unauthorized 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 @@ -535,6 +536,39 @@ def post_pricelist_settings_min_prices(current_session): return no_content() + +@PriceListPlugin.blueprint.route("/users//pricecalc_columns", methods=["GET", "PUT"]) +@login_required() +def get_columns(userid, current_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() + + @PriceListPlugin.blueprint.route("/drinks//picture", methods=["POST", "GET", "DELETE"]) @login_required(permission=permissions.EDIT) def set_picture(identifier, current_session): From f5624e9a7d4a5f81f5d5f13c85cb614d20a2841b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Thu, 15 Apr 2021 22:55:41 +0200 Subject: [PATCH 16/17] [pricelist] add user options pricelist_view --- flaschengeist/plugins/pricelist/__init__.py | 34 ++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/flaschengeist/plugins/pricelist/__init__.py b/flaschengeist/plugins/pricelist/__init__.py index e7a25e3..68205ba 100644 --- a/flaschengeist/plugins/pricelist/__init__.py +++ b/flaschengeist/plugins/pricelist/__init__.py @@ -536,7 +536,6 @@ def post_pricelist_settings_min_prices(current_session): return no_content() - @PriceListPlugin.blueprint.route("/users//pricecalc_columns", methods=["GET", "PUT"]) @login_required() def get_columns(userid, current_session): @@ -569,6 +568,39 @@ def get_columns(userid, current_session): 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): From 3da2ed53d5676dd6f4079c3a06ed5715480005e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Sat, 17 Apr 2021 18:27:14 +0200 Subject: [PATCH 17/17] [pricelist][#6] save order of pricelist columns for user --- flaschengeist/plugins/pricelist/__init__.py | 30 +++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/flaschengeist/plugins/pricelist/__init__.py b/flaschengeist/plugins/pricelist/__init__.py index 68205ba..ef19807 100644 --- a/flaschengeist/plugins/pricelist/__init__.py +++ b/flaschengeist/plugins/pricelist/__init__.py @@ -567,6 +567,36 @@ def get_columns(userid, current_session): 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()