Merge branch 'feature/pricelist' into develop

This commit is contained in:
Tim Gröger 2021-04-17 18:32:49 +02:00
commit 1e5304fe1e
6 changed files with 633 additions and 72 deletions

View File

@ -2,13 +2,13 @@
from flask import Blueprint, jsonify, request, current_app from flask import Blueprint, jsonify, request, current_app
from werkzeug.local import LocalProxy 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 import logger
from flaschengeist.utils.decorators import login_required
from flaschengeist.utils.HTTP import no_content
from flaschengeist.models.session import Session
from flaschengeist.controller import userController 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 models
from . import pricelist_controller, permissions from . import pricelist_controller, permissions
@ -16,6 +16,7 @@ from . import pricelist_controller, permissions
class PriceListPlugin(Plugin): class PriceListPlugin(Plugin):
name = "pricelist" name = "pricelist"
permissions = permissions.permissions
blueprint = Blueprint(name, __name__, url_prefix="/pricelist") blueprint = Blueprint(name, __name__, url_prefix="/pricelist")
plugin = LocalProxy(lambda: current_app.config["FG_PLUGINS"][PriceListPlugin.name]) plugin = LocalProxy(lambda: current_app.config["FG_PLUGINS"][PriceListPlugin.name])
models = models models = models
@ -29,6 +30,17 @@ class PriceListPlugin(Plugin):
@PriceListPlugin.blueprint.route("/drink-types", methods=["GET"]) @PriceListPlugin.blueprint.route("/drink-types", methods=["GET"])
@PriceListPlugin.blueprint.route("/drink-types/<int:identifier>", methods=["GET"]) @PriceListPlugin.blueprint.route("/drink-types/<int:identifier>", methods=["GET"])
def get_drink_types(identifier=None): def get_drink_types(identifier=None):
"""Get DrinkType(s)
Route: ``/pricelist/drink-types`` | Method: ``GET``
Route: ``/pricelist/drink-types/<identifier>`` | Method: ``GET``
Args:
identifier: If querying a spicific DrinkType
Returns:
JSON encoded (list of) DrinkType(s) or HTTP-error
"""
if identifier is None: if identifier is None:
result = pricelist_controller.get_drink_types() result = pricelist_controller.get_drink_types()
else: else:
@ -39,6 +51,18 @@ def get_drink_types(identifier=None):
@PriceListPlugin.blueprint.route("/drink-types", methods=["POST"]) @PriceListPlugin.blueprint.route("/drink-types", methods=["POST"])
@login_required(permission=permissions.CREATE_TYPE) @login_required(permission=permissions.CREATE_TYPE)
def new_drink_type(current_session): 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() data = request.get_json()
if "name" not in data: if "name" not in data:
raise BadRequest raise BadRequest
@ -49,6 +73,19 @@ def new_drink_type(current_session):
@PriceListPlugin.blueprint.route("/drink-types/<int:identifier>", methods=["PUT"]) @PriceListPlugin.blueprint.route("/drink-types/<int:identifier>", methods=["PUT"])
@login_required(permission=permissions.EDIT_TYPE) @login_required(permission=permissions.EDIT_TYPE)
def update_drink_type(identifier, current_session): def update_drink_type(identifier, current_session):
"""Modify DrinkType
Route ``/pricelist/drink-types/<identifier>`` | 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() data = request.get_json()
if "name" not in data: if "name" not in data:
raise BadRequest raise BadRequest
@ -59,6 +96,17 @@ def update_drink_type(identifier, current_session):
@PriceListPlugin.blueprint.route("/drink-types/<int:identifier>", methods=["DELETE"]) @PriceListPlugin.blueprint.route("/drink-types/<int:identifier>", methods=["DELETE"])
@login_required(permission=permissions.DELETE_TYPE) @login_required(permission=permissions.DELETE_TYPE)
def delete_drink_type(identifier, current_session): def delete_drink_type(identifier, current_session):
"""Delete DrinkType
Route: ``/pricelist/drink-types/<identifier>`` | 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) pricelist_controller.delete_drink_type(identifier)
return no_content() 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"])
@PriceListPlugin.blueprint.route("/tags/<int:identifier>", methods=["GET"]) @PriceListPlugin.blueprint.route("/tags/<int:identifier>", methods=["GET"])
def get_tags(identifier=None): def get_tags(identifier=None):
"""Get Tag(s)
Route: ``/pricelist/tags`` | Method: ``GET``
Route: ``/pricelist/tags/<identifier>`` | Method: ``GET``
Args:
identifier: Identifier of Tag
Returns:
JSON encoded (list of) Tag(s) or HTTP-error
"""
if identifier: if identifier:
result = pricelist_controller.get_tag(identifier) result = pricelist_controller.get_tag(identifier)
else: else:
@ -76,26 +135,58 @@ def get_tags(identifier=None):
@PriceListPlugin.blueprint.route("/tags", methods=["POST"]) @PriceListPlugin.blueprint.route("/tags", methods=["POST"])
@login_required(permission=permissions.CREATE_TAG) @login_required(permission=permissions.CREATE_TAG)
def new_tag(current_session): 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() data = request.get_json()
if "name" not in data: drink_type = pricelist_controller.create_tag(data)
raise BadRequest
drink_type = pricelist_controller.create_tag(data["name"])
return jsonify(drink_type) return jsonify(drink_type)
@PriceListPlugin.blueprint.route("/tags/<int:identifier>", methods=["PUT"]) @PriceListPlugin.blueprint.route("/tags/<int:identifier>", methods=["PUT"])
@login_required(permission=permissions.EDIT_TAG) @login_required(permission=permissions.EDIT_TAG)
def update_tag(identifier, current_session): def update_tag(identifier, current_session):
"""Modify Tag
Route: ``/pricelist/tags/<identifier>`` | 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() data = request.get_json()
if "name" not in data: tag = pricelist_controller.update_tag(identifier, data)
raise BadRequest
tag = pricelist_controller.rename_tag(identifier, data["name"])
return jsonify(tag) return jsonify(tag)
@PriceListPlugin.blueprint.route("/tags/<int:identifier>", methods=["DELETE"]) @PriceListPlugin.blueprint.route("/tags/<int:identifier>", methods=["DELETE"])
@login_required(permission=permissions.DELETE_TAG) @login_required(permission=permissions.DELETE_TAG)
def delete_tag(identifier, current_session): def delete_tag(identifier, current_session):
"""Delete Tag
Route: ``/pricelist/tags/<identifier>`` | 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) pricelist_controller.delete_tag(identifier)
return no_content() return no_content()
@ -103,84 +194,340 @@ def delete_tag(identifier, current_session):
@PriceListPlugin.blueprint.route("/drinks", methods=["GET"]) @PriceListPlugin.blueprint.route("/drinks", methods=["GET"])
@PriceListPlugin.blueprint.route("/drinks/<int:identifier>", methods=["GET"]) @PriceListPlugin.blueprint.route("/drinks/<int:identifier>", methods=["GET"])
def get_drinks(identifier=None): def get_drinks(identifier=None):
"""Get Drink(s)
Route: ``/pricelist/drinks`` | Method: ``GET``
Route: ``/pricelist/drinks/<identifier>`` | 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: if identifier:
result = pricelist_controller.get_drink(identifier) result = pricelist_controller.get_drink(identifier, public=public)
else: else:
result = pricelist_controller.get_drinks() result = pricelist_controller.get_drinks(public=public)
logger.debug(f"GET drink {result}")
return jsonify(result) return jsonify(result)
@PriceListPlugin.blueprint.route("/drinks/search/<string:name>", methods=["GET"]) @PriceListPlugin.blueprint.route("/drinks/search/<string:name>", methods=["GET"])
def search_drinks(name): def search_drinks(name):
return jsonify(pricelist_controller.get_drinks(name)) """Search Drink
Route: ``/pricelist/drinks/search/<name>`` | 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"]) @PriceListPlugin.blueprint.route("/drinks", methods=["POST"])
@login_required(permission=permissions.CREATE) @login_required(permission=permissions.CREATE)
def create_drink(current_session): 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() data = request.get_json()
return jsonify(pricelist_controller.set_drink(data)) return jsonify(pricelist_controller.set_drink(data))
@PriceListPlugin.blueprint.route("/drinks/<int:identifier>", methods=["PUT"]) @PriceListPlugin.blueprint.route("/drinks/<int:identifier>", methods=["PUT"])
def update_drink(identifier): @login_required(permission=permissions.EDIT)
def update_drink(identifier, current_session):
"""Modify Drink
Route: ``/pricelist/drinks/<identifier>`` | 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() data = request.get_json()
logger.debug(f"update drink {data}")
return jsonify(pricelist_controller.update_drink(identifier, data)) return jsonify(pricelist_controller.update_drink(identifier, data))
@PriceListPlugin.blueprint.route("/drinks/<int:identifier>", methods=["DELETE"]) @PriceListPlugin.blueprint.route("/drinks/<int:identifier>", methods=["DELETE"])
def delete_drink(identifier): @login_required(permission=permissions.DELETE)
def delete_drink(identifier, current_session):
"""Delete Drink
Route: ``/pricelist/drinks/<identifier>`` | 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) pricelist_controller.delete_drink(identifier)
return no_content() return no_content()
@PriceListPlugin.blueprint.route("/prices/<int:identifier>", methods=["DELETE"]) @PriceListPlugin.blueprint.route("/prices/<int:identifier>", methods=["DELETE"])
def delete_price(identifier): @login_required(permission=permissions.DELETE_PRICE)
def delete_price(identifier, current_session):
"""Delete Price
Route: ``/pricelist/prices/<identifier>`` | 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) pricelist_controller.delete_price(identifier)
return no_content() return no_content()
@PriceListPlugin.blueprint.route("/volumes/<int:identifier>", methods=["DELETE"]) @PriceListPlugin.blueprint.route("/volumes/<int:identifier>", methods=["DELETE"])
def delete_volume(identifier): @login_required(permission=permissions.DELETE_VOLUME)
def delete_volume(identifier, current_session):
"""Delete DrinkPriceVolume
Route: ``/pricelist/volumes/<identifier>`` | 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) pricelist_controller.delete_volume(identifier)
return no_content() return no_content()
@PriceListPlugin.blueprint.route("/ingredients/extraIngredients", methods=["GET"]) @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()) return jsonify(pricelist_controller.get_extra_ingredients())
@PriceListPlugin.blueprint.route("/ingredients/<int:identifier>", methods=["DELETE"]) @PriceListPlugin.blueprint.route("/ingredients/<int:identifier>", methods=["DELETE"])
def delete_ingredient(identifier): @login_required(permission=permissions.DELETE_INGREDIENTS_DRINK)
def delete_ingredient(identifier, current_session):
"""Delete Ingredient
Route: ``/pricelist/ingredients/<identifier>`` | 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) pricelist_controller.delete_ingredient(identifier)
return no_content() return no_content()
@PriceListPlugin.blueprint.route("/ingredients/extraIngredients", methods=["POST"]) @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() data = request.get_json()
return jsonify(pricelist_controller.set_extra_ingredient(data)) return jsonify(pricelist_controller.set_extra_ingredient(data))
@PriceListPlugin.blueprint.route("/ingredients/extraIngredients/<int:identifier>", methods=["PUT"]) @PriceListPlugin.blueprint.route("/ingredients/extraIngredients/<int:identifier>", 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() data = request.get_json()
return jsonify(pricelist_controller.update_extra_ingredient(identifier, data)) return jsonify(pricelist_controller.update_extra_ingredient(identifier, data))
@PriceListPlugin.blueprint.route("/ingredients/extraIngredients/<int:identifier>", methods=["DELETE"]) @PriceListPlugin.blueprint.route("/ingredients/extraIngredients/<int:identifier>", 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) pricelist_controller.delete_extra_ingredient(identifier)
return no_content() return no_content()
@PriceListPlugin.blueprint.route("/settings/min_prices", methods=["POST", "GET"]) @PriceListPlugin.blueprint.route("/settings/min_prices", methods=["GET"])
def pricelist_settings_min_prices(): @login_required()
if request.method == "GET": 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! # TODO: Handle if no prices are set!
return jsonify(PriceListPlugin.plugin.get_setting("min_prices")) try:
else: 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() data = request.get_json()
if not isinstance(data, list) or not all(isinstance(n, int) for n in data): if not isinstance(data, list) or not all(isinstance(n, int) for n in data):
raise BadRequest raise BadRequest
@ -191,7 +538,7 @@ def pricelist_settings_min_prices():
@PriceListPlugin.blueprint.route("/users/<userid>/pricecalc_columns", methods=["GET", "PUT"]) @PriceListPlugin.blueprint.route("/users/<userid>/pricecalc_columns", methods=["GET", "PUT"])
@login_required() @login_required()
def get_columns(userid, current_session: Session): def get_columns(userid, current_session):
"""Get pricecalc_columns of an user """Get pricecalc_columns of an user
Route: ``/users/<userid>/pricelist/pricecac_columns`` | Method: ``GET`` or ``PUT`` Route: ``/users/<userid>/pricelist/pricecac_columns`` | Method: ``GET`` or ``PUT``
@ -219,3 +566,112 @@ def get_columns(userid, current_session: Session):
user.set_attribute("pricecalc_columns", data) user.set_attribute("pricecalc_columns", data)
userController.persist() userController.persist()
return no_content() return no_content()
@PriceListPlugin.blueprint.route("/users/<userid>/pricecalc_columns_order", methods=["GET", "PUT"])
@login_required()
def get_columns_order(userid, current_session):
"""Get pricecalc_columns_order of an user
Route: ``/users/<userid>/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/<userid>/pricelist", methods=["GET", "PUT"])
@login_required()
def get_priclist_setting(userid, current_session):
"""Get pricelistsetting of an user
Route: ``/pricelist/user/<userid>/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/<int:identifier>/picture", methods=["POST", "GET", "DELETE"])
@login_required(permission=permissions.EDIT)
def set_picture(identifier, current_session):
"""Get, Create, Delete Drink Picture
Route: ``/pricelist/<identifier>/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/<identifier>", 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)

View File

@ -26,6 +26,7 @@ class Tag(db.Model, ModelSerializeMixin):
__tablename__ = "drink_tag" __tablename__ = "drink_tag"
id: int = db.Column("id", db.Integer, primary_key=True) id: int = db.Column("id", db.Integer, primary_key=True)
name: str = db.Column(db.String(30), nullable=False, unique=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): class DrinkType(db.Model, ModelSerializeMixin):
@ -51,6 +52,9 @@ class DrinkPrice(db.Model, ModelSerializeMixin):
public: bool = db.Column(db.Boolean, default=True) public: bool = db.Column(db.Boolean, default=True)
description: Optional[str] = db.Column(db.String(30)) description: Optional[str] = db.Column(db.String(30))
def __repr__(self):
return f"DrinkPric({self.id},{self.price},{self.public},{self.description})"
class ExtraIngredient(db.Model, ModelSerializeMixin): 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") 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) 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): 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_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)) 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")) _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]) type: Optional[DrinkType] = db.relationship("DrinkType", foreign_keys=[_type_id])
volumes: list[DrinkPriceVolume] = db.relationship(DrinkPriceVolume) volumes: list[DrinkPriceVolume] = db.relationship(DrinkPriceVolume)
def __repr__(self):
return f"Drink({self.id},{self.name},{self.volumes})"
class _Picture: class _Picture:
"""Wrapper class for pictures binaries""" """Wrapper class for pictures binaries"""

View File

@ -10,6 +10,18 @@ DELETE = "drink_delete"
CREATE_TAG = "drink_tag_create" CREATE_TAG = "drink_tag_create"
"""Can create and edit Tags""" """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" EDIT_TAG = "drink_tag_edit"
DELETE_TAG = "drink_tag_delete" DELETE_TAG = "drink_tag_delete"
@ -20,4 +32,6 @@ EDIT_TYPE = "drink_type_edit"
DELETE_TYPE = "drink_type_delete" DELETE_TYPE = "drink_type_delete"
EDIT_MIN_PRICES = "edit_min_prices"
permissions = [value for key, value in globals().items() if not key.startswith("_")] permissions = [value for key, value in globals().items() if not key.startswith("_")]

View File

@ -5,9 +5,11 @@ from uuid import uuid4
from flaschengeist import logger from flaschengeist import logger
from flaschengeist.config import config from flaschengeist.config import config
from flaschengeist.database import db 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 .models import Drink, DrinkPrice, Ingredient, Tag, DrinkType, DrinkPriceVolume, DrinkIngredient, ExtraIngredient
from .permissions import EDIT_VOLUME, EDIT_PRICE, EDIT_INGREDIENTS_DRINK
def update(): def update():
@ -31,9 +33,13 @@ def get_tag(identifier):
return ret return ret
def create_tag(name): def create_tag(data):
try: 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) db.session.add(tag)
update() update()
return tag return tag
@ -41,9 +47,12 @@ def create_tag(name):
raise BadRequest("Name already exists") raise BadRequest("Name already exists")
def rename_tag(identifier, new_name): def update_tag(identifier, data):
tag = get_tag(identifier) 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: try:
update() update()
except IntegrityError: except IntegrityError:
@ -105,13 +114,34 @@ def delete_drink_type(identifier):
raise BadRequest("DrinkType still in use") 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: if name:
return Drink.query.filter(Drink.name.contains(name)).all() drinks = Drink.query.filter(Drink.name.contains(name)).all()
return Drink.query.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): if isinstance(identifier, int):
drink = Drink.query.get(identifier) drink = Drink.query.get(identifier)
elif isinstance(identifier, str): elif isinstance(identifier, str):
@ -120,6 +150,8 @@ def get_drink(identifier):
raise BadRequest("Invalid identifier type for Drink") raise BadRequest("Invalid identifier type for Drink")
if drink is None: if drink is None:
raise NotFound raise NotFound
if public:
return _create_public_drink(drink)
return drink return drink
@ -129,11 +161,17 @@ def set_drink(data):
def update_drink(identifier, data): def update_drink(identifier, data):
try: try:
session = extract_session()
if "id" in data: if "id" in data:
data.pop("id") data.pop("id")
volumes = data.pop("volumes") if "volumes" in data else None volumes = data.pop("volumes") if "volumes" in data else None
tags = []
if "tags" in data: 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") drink_type = data.pop("type")
if isinstance(drink_type, dict) and "id" in drink_type: if isinstance(drink_type, dict) and "id" in drink_type:
drink_type = drink_type["id"] drink_type = drink_type["id"]
@ -149,19 +187,24 @@ def update_drink(identifier, data):
if drink_type: if drink_type:
drink.type = 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) drink.volumes = []
drink.volumes = set_volumes(volumes)
if len(tags) > 0:
drink.tags = tags
db.session.commit() db.session.commit()
return drink return drink
except (NotFound, KeyError): except (NotFound, KeyError):
raise BadRequest raise BadRequest
def set_volumes(volumes, drink): def set_volumes(volumes):
retVal = []
if not isinstance(volumes, list): if not isinstance(volumes, list):
raise BadRequest raise BadRequest
for volume in volumes: for volume in volumes:
drink.volumes.append(set_volume(volume)) retVal.append(set_volume(volume))
return retVal
def delete_drink(identifier): def delete_drink(identifier):
@ -181,6 +224,7 @@ def get_volumes(drink_id=None):
def set_volume(data): def set_volume(data):
session = extract_session()
allowed_keys = DrinkPriceVolume().serialize().keys() allowed_keys = DrinkPriceVolume().serialize().keys()
values = {key: value for key, value in data.items() if key in allowed_keys} values = {key: value for key, value in data.items() if key in allowed_keys}
prices = None prices = None
@ -200,9 +244,9 @@ def set_volume(data):
for key, value in values.items(): for key, value in values.items():
setattr(volume, key, value if value != "" else None) setattr(volume, key, value if value != "" else None)
if prices: if prices and session.user_.has_permission(EDIT_PRICE):
set_prices(prices, volume) set_prices(prices, volume)
if ingredients: if ingredients and session.user_.has_permission(EDIT_INGREDIENTS_DRINK):
set_ingredients(ingredients, volume) set_ingredients(ingredients, volume)
return volume return volume
@ -244,7 +288,9 @@ def get_prices(volume_id=None):
def set_price(data): 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} values = {key: value for key, value in data.items() if key in allowed_keys}
price_id = values.pop("id", -1) price_id = values.pop("id", -1)
if price_id < 0: if price_id < 0:
@ -357,16 +403,33 @@ def delete_extra_ingredient(identifier):
def save_drink_picture(identifier, file): def save_drink_picture(identifier, file):
drink = get_drink(identifier) drink = get_drink(identifier)
if not drink.uuid: old_uuid = None
if drink.uuid:
old_uuid = drink.uuid
drink.uuid = str(uuid4()) drink.uuid = str(uuid4())
db.session.commit() db.session.commit()
path = config["pricelist"]["path"] path = config["pricelist"]["path"]
save_picture(file, f"{path}/{drink.uuid}") 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): def get_drink_picture(identifier, size=None):
drink = get_drink(identifier)
if not drink.uuid:
raise BadRequest
path = config["pricelist"]["path"] 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()

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

@ -1,7 +1,8 @@
import os, sys import os, sys, shutil, io
from PIL import Image from PIL import Image
from flask import Response from flask import Response
from werkzeug.exceptions import BadRequest from werkzeug.exceptions import BadRequest
from ..utils.HTTP import no_content
thumbnail_sizes = ((32, 32), (64, 64), (128, 128), (256, 256), (512, 512)) 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": if file_type != "png":
image.save(f"{filename}.png", "PNG") image.save(f"{filename}.png", "PNG")
os.remove(f"{filename}.{file_type}") os.remove(f"{filename}.{file_type}")
image.show()
for thumbnail_size in thumbnail_sizes: for thumbnail_size in thumbnail_sizes:
work_image = image.copy() work_image = image.copy()
work_image.thumbnail(thumbnail_size) work_image.thumbnail(thumbnail_size)
@ -27,12 +27,29 @@ def save_picture(picture, path):
def get_picture(path, size=None): def get_picture(path, size=None):
try:
if size: if size:
if os.path.isfile(f"{path}/drink-{size}.png"):
with open(f"{path}/drink-{size}.png", "rb") as file: with open(f"{path}/drink-{size}.png", "rb") as file:
image = file.read() 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: else:
with open(f"{path}/drink.png", "rb") as file: with open(f"{path}/drink.png", "rb") as file:
image = file.read() image = file.read()
response = Response(image, mimetype="image/png") response = Response(image, mimetype="image/png")
response.add_etag() response.add_etag()
return response return response
except:
raise FileNotFoundError
def delete_picture(path):
try:
shutil.rmtree(path)
except FileNotFoundError:
pass