Compare commits

..

No commits in common. "db7c2c818b8a3e09281e8ce2e33b6e16f8d36789" and "607b29027b2dadfe95005ed9096e8a498c618836" have entirely different histories.

8 changed files with 1656 additions and 11 deletions

View File

@ -1,7 +1,6 @@
from __future__ import annotations # TODO: Remove if python requirement is >= 3.12 (? PEP 563 is defered) from __future__ import annotations # TODO: Remove if python requirement is >= 3.12 (? PEP 563 is defered)
from typing import Any, Dict, List from typing import Any, List, Dict
from sqlalchemy.orm.collections import attribute_mapped_collection from sqlalchemy.orm.collections import attribute_mapped_collection
from ..database import db from ..database import db
@ -69,10 +68,7 @@ class BasePlugin(db.Model):
value: Value to be stored value: Value to be stored
""" """
if value is None and name in self.__settings.keys(): if value is None and name in self.__settings.keys():
pl = self.__settings[name] del self.__settings[name]
db.session.delete(pl)
else: else:
setting = self.__settings.setdefault(name, PluginSetting(plugin_id=self.id, name=name, value=None)) setting = self.__settings.setdefault(name, PluginSetting(plugin_id=self.id, name=name, value=None))
setting.value = value setting.value = value
db.session.add(setting)
db.session.commit()

View File

@ -0,0 +1,753 @@
"""Pricelist plugin"""
from flask import Blueprint, jsonify, request
from werkzeug.exceptions import BadRequest, Forbidden, NotFound, Unauthorized
from flaschengeist import logger
from flaschengeist.controller import userController
from flaschengeist.controller.imageController import send_image, send_thumbnail
from flaschengeist.plugins import Plugin
from flaschengeist.utils.decorators import login_required, extract_session
from flaschengeist.utils.HTTP import no_content
from . import models
from . import pricelist_controller, permissions
class PriceListPlugin(Plugin):
models = models
blueprint = Blueprint("pricelist", __name__, url_prefix="/pricelist")
def install(self):
self.install_permissions(permissions.permissions)
def load(self):
config = {"discount": 0}
config.update(config)
@PriceListPlugin.blueprint.route("/drink-types", methods=["GET"])
@PriceListPlugin.blueprint.route("/drink-types/<int:identifier>", methods=["GET"])
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:
result = pricelist_controller.get_drink_types()
else:
result = pricelist_controller.get_drink_type(identifier)
return jsonify(result)
@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
drink_type = pricelist_controller.create_drink_type(data["name"])
return jsonify(drink_type)
@PriceListPlugin.blueprint.route("/drink-types/<int:identifier>", methods=["PUT"])
@login_required(permission=permissions.EDIT_TYPE)
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()
if "name" not in data:
raise BadRequest
drink_type = pricelist_controller.rename_drink_type(identifier, data["name"])
return jsonify(drink_type)
@PriceListPlugin.blueprint.route("/drink-types/<int:identifier>", methods=["DELETE"])
@login_required(permission=permissions.DELETE_TYPE)
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)
return no_content()
@PriceListPlugin.blueprint.route("/tags", methods=["GET"])
@PriceListPlugin.blueprint.route("/tags/<int:identifier>", methods=["GET"])
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:
result = pricelist_controller.get_tag(identifier)
else:
result = pricelist_controller.get_tags()
return jsonify(result)
@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)
@PriceListPlugin.blueprint.route("/tags/<int:identifier>", methods=["PUT"])
@login_required(permission=permissions.EDIT_TAG)
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()
tag = pricelist_controller.update_tag(identifier, data)
return jsonify(tag)
@PriceListPlugin.blueprint.route("/tags/<int:identifier>", methods=["DELETE"])
@login_required(permission=permissions.DELETE_TAG)
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)
return no_content()
@PriceListPlugin.blueprint.route("/drinks", methods=["GET"])
@PriceListPlugin.blueprint.route("/drinks/<int:identifier>", methods=["GET"])
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:
result = pricelist_controller.get_drink(identifier, public=public)
return jsonify(result)
else:
limit = request.args.get("limit")
offset = request.args.get("offset")
search_name = request.args.get("search_name")
search_key = request.args.get("search_key")
ingredient = request.args.get("ingredient", type=bool)
receipt = request.args.get("receipt", type=bool)
try:
if limit is not None:
limit = int(limit)
if offset is not None:
offset = int(offset)
if ingredient is not None:
ingredient = bool(ingredient)
if receipt is not None:
receipt = bool(receipt)
except ValueError:
raise BadRequest
drinks, count = pricelist_controller.get_drinks(
public=public,
limit=limit,
offset=offset,
search_name=search_name,
search_key=search_key,
ingredient=ingredient,
receipt=receipt,
)
mop = drinks.copy()
logger.debug(f"GET drink {drinks}, {count}")
# return jsonify({"drinks": drinks, "count": count})
return jsonify({"drinks": drinks, "count": count})
@PriceListPlugin.blueprint.route("/list", methods=["GET"])
def get_pricelist():
"""Get Priclist
Route: ``/pricelist/list`` | Method: ``GET``
Returns:
JSON encoded list of DrinkPrices or HTTP-KeyError
"""
public = True
try:
extract_session()
public = False
except Unauthorized:
public = True
limit = request.args.get("limit")
offset = request.args.get("offset")
search_name = request.args.get("search_name")
search_key = request.args.get("search_key")
ingredient = request.args.get("ingredient", type=bool)
receipt = request.args.get("receipt", type=bool)
descending = request.args.get("descending", type=bool)
sortBy = request.args.get("sortBy")
try:
if limit is not None:
limit = int(limit)
if offset is not None:
offset = int(offset)
if ingredient is not None:
ingredient = bool(ingredient)
if receipt is not None:
receipt = bool(receipt)
if descending is not None:
descending = bool(descending)
except ValueError:
raise BadRequest
pricelist, count = pricelist_controller.get_pricelist(
public=public,
limit=limit,
offset=offset,
search_name=search_name,
search_key=search_key,
descending=descending,
sortBy=sortBy,
)
logger.debug(f"GET pricelist {pricelist}, {count}")
return jsonify({"pricelist": pricelist, "count": count})
@PriceListPlugin.blueprint.route("/drinks/search/<string:name>", methods=["GET"])
def search_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"])
@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/<int:identifier>", methods=["PUT"])
@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()
logger.debug(f"update drink {data}")
return jsonify(pricelist_controller.update_drink(identifier, data))
@PriceListPlugin.blueprint.route("/drinks/<int:identifier>", methods=["DELETE"])
@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)
return no_content()
@PriceListPlugin.blueprint.route("/prices/<int:identifier>", methods=["DELETE"])
@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)
return no_content()
@PriceListPlugin.blueprint.route("/volumes/<int:identifier>", methods=["DELETE"])
@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)
return no_content()
@PriceListPlugin.blueprint.route("/ingredients/extraIngredients", methods=["GET"])
@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/<int:identifier>", methods=["DELETE"])
@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)
return no_content()
@PriceListPlugin.blueprint.route("/ingredients/extraIngredients", methods=["POST"])
@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/<int:identifier>", methods=["PUT"])
@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/<int:identifier>", methods=["DELETE"])
@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=["GET"])
@login_required()
def get_pricelist_settings_min_prices(current_session):
"""Get MinPrices
Route: ``/pricelist/settings/min_prices`` | Method: ``GET``
Args:
current_session: Session sent with Authorization Header
Returns:
JSON encoded list of MinPrices
"""
# TODO: Handle if no prices are set!
try:
min_prices = PriceListPlugin.plugin.get_setting("min_prices")
except KeyError:
min_prices = []
return jsonify(min_prices)
@PriceListPlugin.blueprint.route("/settings/min_prices", methods=["POST"])
@login_required(permission=permissions.EDIT_MIN_PRICES)
def post_pricelist_settings_min_prices(current_session):
"""Create MinPrices
Route: ``/pricelist/settings/min_prices`` | Method: ``POST``
POST-data: ``list[int]``
Args:
current_session: Session sent with Authorization Header
Returns:
HTTP-NoContent or HTTP-error
"""
data = request.get_json()
if not isinstance(data, list) or not all(isinstance(n, int) for n in data):
raise BadRequest
data.sort()
PriceListPlugin.plugin.set_setting("min_prices", data)
return no_content()
@PriceListPlugin.blueprint.route("/users/<userid>/pricecalc_columns", methods=["GET", "PUT"])
@login_required()
def get_columns(userid, current_session):
"""Get pricecalc_columns of an user
Route: ``/users/<userid>/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("/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", "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:
return jsonify(pricelist_controller.save_drink_picture(identifier, file))
else:
raise BadRequest
@PriceListPlugin.blueprint.route("/drinks/<int:identifier>/picture", methods=["GET"])
# @headers({"Cache-Control": "private, must-revalidate"})
def _get_picture(identifier):
"""Get Picture
Args:
identifier: Identifier of Drink
Returns:
Picture or HTTP-error
"""
drink = pricelist_controller.get_drink(identifier)
if drink.has_image:
if request.args.get("thumbnail"):
return send_thumbnail(image=drink.image_)
return send_image(image=drink.image_)
raise NotFound

View File

@ -0,0 +1,141 @@
"""pricelist: initial
Revision ID: 58ab9b6a8839
Revises:
Create Date: 2022-02-23 14:45:30.563647
"""
from alembic import op
import sqlalchemy as sa
import flaschengeist
# revision identifiers, used by Alembic.
revision = "58ab9b6a8839"
down_revision = None
branch_labels = ("pricelist",)
depends_on = "flaschengeist"
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"drink_extra_ingredient",
sa.Column("id", flaschengeist.models.Serial(), nullable=False),
sa.Column("name", sa.String(length=30), nullable=False),
sa.Column("price", sa.Numeric(precision=5, scale=2, asdecimal=False), nullable=True),
sa.PrimaryKeyConstraint("id", name=op.f("pk_drink_extra_ingredient")),
sa.UniqueConstraint("name", name=op.f("uq_drink_extra_ingredient_name")),
)
op.create_table(
"drink_tag",
sa.Column("id", flaschengeist.models.Serial(), nullable=False),
sa.Column("name", sa.String(length=30), nullable=False),
sa.Column("color", sa.String(length=7), nullable=False),
sa.PrimaryKeyConstraint("id", name=op.f("pk_drink_tag")),
sa.UniqueConstraint("name", name=op.f("uq_drink_tag_name")),
)
op.create_table(
"drink_type",
sa.Column("id", flaschengeist.models.Serial(), nullable=False),
sa.Column("name", sa.String(length=30), nullable=False),
sa.PrimaryKeyConstraint("id", name=op.f("pk_drink_type")),
sa.UniqueConstraint("name", name=op.f("uq_drink_type_name")),
)
op.create_table(
"drink",
sa.Column("id", flaschengeist.models.Serial(), nullable=False),
sa.Column("article_id", sa.String(length=64), nullable=True),
sa.Column("package_size", sa.Integer(), nullable=True),
sa.Column("name", sa.String(length=60), nullable=False),
sa.Column("volume", sa.Numeric(precision=5, scale=2, asdecimal=False), nullable=True),
sa.Column("cost_per_volume", sa.Numeric(precision=5, scale=3, asdecimal=False), nullable=True),
sa.Column("cost_per_package", sa.Numeric(precision=5, scale=3, asdecimal=False), nullable=True),
sa.Column("receipt", sa.PickleType(), nullable=True),
sa.Column("type_id", flaschengeist.models.Serial(), nullable=True),
sa.Column("image_id", flaschengeist.models.Serial(), nullable=True),
sa.ForeignKeyConstraint(["image_id"], ["image.id"], name=op.f("fk_drink_image_id_image")),
sa.ForeignKeyConstraint(["type_id"], ["drink_type.id"], name=op.f("fk_drink_type_id_drink_type")),
sa.PrimaryKeyConstraint("id", name=op.f("pk_drink")),
)
op.create_table(
"drink_ingredient",
sa.Column("id", flaschengeist.models.Serial(), nullable=False),
sa.Column("volume", sa.Numeric(precision=5, scale=2, asdecimal=False), nullable=False),
sa.Column("ingredient_id", flaschengeist.models.Serial(), nullable=True),
sa.ForeignKeyConstraint(["ingredient_id"], ["drink.id"], name=op.f("fk_drink_ingredient_ingredient_id_drink")),
sa.PrimaryKeyConstraint("id", name=op.f("pk_drink_ingredient")),
)
op.create_table(
"drink_price_volume",
sa.Column("id", flaschengeist.models.Serial(), nullable=False),
sa.Column("drink_id", flaschengeist.models.Serial(), nullable=True),
sa.Column("volume", sa.Numeric(precision=5, scale=2, asdecimal=False), nullable=True),
sa.ForeignKeyConstraint(["drink_id"], ["drink.id"], name=op.f("fk_drink_price_volume_drink_id_drink")),
sa.PrimaryKeyConstraint("id", name=op.f("pk_drink_price_volume")),
)
op.create_table(
"drink_x_tag",
sa.Column("drink_id", flaschengeist.models.Serial(), nullable=True),
sa.Column("tag_id", flaschengeist.models.Serial(), nullable=True),
sa.ForeignKeyConstraint(["drink_id"], ["drink.id"], name=op.f("fk_drink_x_tag_drink_id_drink")),
sa.ForeignKeyConstraint(["tag_id"], ["drink_tag.id"], name=op.f("fk_drink_x_tag_tag_id_drink_tag")),
)
op.create_table(
"drink_x_type",
sa.Column("drink_id", flaschengeist.models.Serial(), nullable=True),
sa.Column("type_id", flaschengeist.models.Serial(), nullable=True),
sa.ForeignKeyConstraint(["drink_id"], ["drink.id"], name=op.f("fk_drink_x_type_drink_id_drink")),
sa.ForeignKeyConstraint(["type_id"], ["drink_type.id"], name=op.f("fk_drink_x_type_type_id_drink_type")),
)
op.create_table(
"drink_ingredient_association",
sa.Column("id", flaschengeist.models.Serial(), nullable=False),
sa.Column("volume_id", flaschengeist.models.Serial(), nullable=True),
sa.Column("_drink_ingredient_id", flaschengeist.models.Serial(), nullable=True),
sa.Column("_extra_ingredient_id", flaschengeist.models.Serial(), nullable=True),
sa.ForeignKeyConstraint(
["_drink_ingredient_id"],
["drink_ingredient.id"],
name=op.f("fk_drink_ingredient_association__drink_ingredient_id_drink_ingredient"),
),
sa.ForeignKeyConstraint(
["_extra_ingredient_id"],
["drink_extra_ingredient.id"],
name=op.f("fk_drink_ingredient_association__extra_ingredient_id_drink_extra_ingredient"),
),
sa.ForeignKeyConstraint(
["volume_id"],
["drink_price_volume.id"],
name=op.f("fk_drink_ingredient_association_volume_id_drink_price_volume"),
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_drink_ingredient_association")),
)
op.create_table(
"drink_price",
sa.Column("id", flaschengeist.models.Serial(), nullable=False),
sa.Column("price", sa.Numeric(precision=5, scale=2, asdecimal=False), nullable=True),
sa.Column("volume_id", flaschengeist.models.Serial(), nullable=True),
sa.Column("public", sa.Boolean(), nullable=True),
sa.Column("description", sa.String(length=30), nullable=True),
sa.ForeignKeyConstraint(
["volume_id"], ["drink_price_volume.id"], name=op.f("fk_drink_price_volume_id_drink_price_volume")
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_drink_price")),
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("drink_price")
op.drop_table("drink_ingredient_association")
op.drop_table("drink_x_type")
op.drop_table("drink_x_tag")
op.drop_table("drink_price_volume")
op.drop_table("drink_ingredient")
op.drop_table("drink")
op.drop_table("drink_type")
op.drop_table("drink_tag")
op.drop_table("drink_extra_ingredient")
# ### end Alembic commands ###

View File

@ -0,0 +1,180 @@
from __future__ import annotations # TODO: Remove if python requirement is >= 3.12 (? PEP 563 is defered)
from typing import Optional
from flaschengeist.database import db
from flaschengeist.database.types import ModelSerializeMixin, Serial
from flaschengeist.models import Image
drink_tag_association = db.Table(
"drink_x_tag",
db.Column("drink_id", Serial, db.ForeignKey("drink.id")),
db.Column("tag_id", Serial, db.ForeignKey("drink_tag.id")),
)
drink_type_association = db.Table(
"drink_x_type",
db.Column("drink_id", Serial, db.ForeignKey("drink.id")),
db.Column("type_id", Serial, db.ForeignKey("drink_type.id")),
)
class Tag(db.Model, ModelSerializeMixin):
"""
Tag
"""
__tablename__ = "drink_tag"
id: int = db.Column("id", Serial, 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):
"""
DrinkType
"""
__tablename__ = "drink_type"
id: int = db.Column("id", Serial, primary_key=True)
name: str = db.Column(db.String(30), nullable=False, unique=True)
class DrinkPrice(db.Model, ModelSerializeMixin):
"""
PriceFromVolume
"""
__tablename__ = "drink_price"
id: int = db.Column("id", Serial, primary_key=True)
price: float = db.Column(db.Numeric(precision=5, scale=2, asdecimal=False))
volume_id_ = db.Column("volume_id", Serial, db.ForeignKey("drink_price_volume.id"))
volume: "DrinkPriceVolume" = None
_volume: "DrinkPriceVolume" = db.relationship("DrinkPriceVolume", back_populates="_prices", join_depth=1)
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):
"""
ExtraIngredient
"""
__tablename__ = "drink_extra_ingredient"
id: int = db.Column("id", Serial, primary_key=True)
name: str = db.Column(db.String(30), unique=True, nullable=False)
price: float = db.Column(db.Numeric(precision=5, scale=2, asdecimal=False))
class DrinkIngredient(db.Model, ModelSerializeMixin):
"""
Drink Ingredient
"""
__tablename__ = "drink_ingredient"
id: int = db.Column("id", Serial, primary_key=True)
volume: float = db.Column(db.Numeric(precision=5, scale=2, asdecimal=False), nullable=False)
ingredient_id: int = db.Column(Serial, db.ForeignKey("drink.id"))
cost_per_volume: float
name: str
_drink_ingredient: Drink = db.relationship("Drink")
@property
def cost_per_volume(self):
return self._drink_ingredient.cost_per_volume if self._drink_ingredient else None
@property
def name(self):
return self._drink_ingredient.name if self._drink_ingredient else None
class Ingredient(db.Model, ModelSerializeMixin):
"""
Ingredient Associationtable
"""
__tablename__ = "drink_ingredient_association"
id: int = db.Column("id", Serial, primary_key=True)
volume_id = db.Column(Serial, db.ForeignKey("drink_price_volume.id"))
drink_ingredient: Optional[DrinkIngredient] = db.relationship(DrinkIngredient, cascade="all,delete")
extra_ingredient: Optional[ExtraIngredient] = db.relationship(ExtraIngredient)
_drink_ingredient_id = db.Column(Serial, db.ForeignKey("drink_ingredient.id"))
_extra_ingredient_id = db.Column(Serial, db.ForeignKey("drink_extra_ingredient.id"))
class MinPrices(ModelSerializeMixin):
"""
MinPrices
"""
percentage: float
price: float
class DrinkPriceVolume(db.Model, ModelSerializeMixin):
"""
Drink Volumes and Prices
"""
__tablename__ = "drink_price_volume"
id: int = db.Column("id", Serial, primary_key=True)
drink_id = db.Column(Serial, db.ForeignKey("drink.id"))
drink: "Drink" = None
_drink: "Drink" = db.relationship("Drink", back_populates="_volumes")
volume: float = db.Column(db.Numeric(precision=5, scale=2, asdecimal=False))
min_prices: list[MinPrices] = []
# ingredients: list[Ingredient] = []
prices: list[DrinkPrice] = []
_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,
cascade="all,delete,delete-orphan",
)
def __repr__(self):
return f"DrinkPriceVolume({self.id},{self.drink_id},{self.volume},{self.prices})"
class Drink(db.Model, ModelSerializeMixin):
"""
DrinkPrice
"""
__tablename__ = "drink"
id: int = db.Column("id", Serial, primary_key=True)
article_id: Optional[str] = db.Column(db.String(64))
package_size: Optional[int] = db.Column(db.Integer)
name: str = db.Column(db.String(60), nullable=False)
volume: Optional[float] = db.Column(db.Numeric(precision=5, scale=2, 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))
has_image: bool = False
receipt: Optional[list[str]] = db.Column(db.PickleType(protocol=4))
_type_id = db.Column("type_id", Serial, db.ForeignKey("drink_type.id"))
_image_id = db.Column("image_id", Serial, db.ForeignKey("image.id"))
image_: Image = db.relationship("Image", cascade="all, delete", foreign_keys=[_image_id])
tags: Optional[list[Tag]] = db.relationship("Tag", secondary=drink_tag_association, cascade="save-update, merge")
type: Optional[DrinkType] = db.relationship("DrinkType", foreign_keys=[_type_id])
volumes: list[DrinkPriceVolume] = []
_volumes: list[DrinkPriceVolume] = db.relationship(
DrinkPriceVolume, back_populates="_drink", cascade="all,delete,delete-orphan"
)
def __repr__(self):
return f"Drink({self.id},{self.name},{self.volumes})"
@property
def has_image(self):
return self.image_ is not None

View File

@ -0,0 +1,37 @@
CREATE = "drink_create"
"""Can create drinks"""
EDIT = "drink_edit"
"""Can edit drinks"""
DELETE = "drink_delete"
"""Can delete drinks"""
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"
CREATE_TYPE = "drink_type_create"
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("_")]

View File

@ -0,0 +1,540 @@
from werkzeug.exceptions import BadRequest, NotFound
from sqlalchemy.exc import IntegrityError
from flaschengeist import logger
from flaschengeist.database import db
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
import flaschengeist.controller.imageController as image_controller
def update():
db.session.commit()
def get_tags():
return Tag.query.all()
def get_tag(identifier):
if isinstance(identifier, int):
ret = Tag.query.get(identifier)
elif isinstance(identifier, str):
ret = Tag.query.filter(Tag.name == identifier).one_or_none()
else:
logger.debug("Invalid identifier type for Tag")
raise BadRequest
if ret is None:
raise NotFound
return ret
def create_tag(data):
try:
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
except IntegrityError:
raise BadRequest("Name already exists")
def update_tag(identifier, data):
tag = get_tag(identifier)
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:
raise BadRequest("Name already exists")
def delete_tag(identifier):
tag = get_tag(identifier)
db.session.delete(tag)
try:
update()
except IntegrityError:
raise BadRequest("Tag still in use")
def get_drink_types():
return DrinkType.query.all()
def get_drink_type(identifier):
if isinstance(identifier, int):
ret = DrinkType.query.get(identifier)
elif isinstance(identifier, str):
ret = DrinkType.query.filter(Tag.name == identifier).one_or_none()
else:
logger.debug("Invalid identifier type for DrinkType")
raise BadRequest
if ret is None:
raise NotFound
return ret
def create_drink_type(name):
try:
drink_type = DrinkType(name=name)
db.session.add(drink_type)
update()
return drink_type
except IntegrityError:
raise BadRequest("Name already exists")
def rename_drink_type(identifier, new_name):
drink_type = get_drink_type(identifier)
drink_type.name = new_name
try:
update()
except IntegrityError:
raise BadRequest("Name already exists")
return drink_type
def delete_drink_type(identifier):
drink_type = get_drink_type(identifier)
db.session.delete(drink_type)
try:
update()
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,
public=False,
limit=None,
offset=None,
search_name=None,
search_key=None,
ingredient=False,
receipt=None,
):
count = None
if name:
query = Drink.query.filter(Drink.name.contains(name))
else:
query = Drink.query
if ingredient:
query = query.filter(Drink.cost_per_volume >= 0)
if receipt:
query = query.filter(Drink._volumes.any(DrinkPriceVolume.ingredients != None))
if public:
query = query.filter(Drink._volumes.any(DrinkPriceVolume._prices.any(DrinkPrice.public)))
if search_name:
if search_key == "name":
query = query.filter(Drink.name.contains(search_name))
elif search_key == "article_id":
query = query.filter(Drink.article_id.contains(search_name))
elif search_key == "drink_type":
query = query.filter(Drink.type.has(DrinkType.name.contains(search_name)))
elif search_key == "tags":
query = query.filter(Drink.tags.any(Tag.name.contains(search_name)))
else:
query = query.filter(
(Drink.name.contains(search_name))
| (Drink.article_id.contains(search_name))
| (Drink.type.has(DrinkType.name.contains(search_name)))
| (Drink.tags.any(Tag.name.contains(search_name)))
)
query = query.order_by(Drink.name.asc())
if limit is not None:
count = query.count()
query = query.limit(limit)
if offset is not None:
query = query.offset(offset)
drinks = query.all()
for drink in drinks:
for volume in drink._volumes:
volume.prices = volume._prices
drink.volumes = drink._volumes
return drinks, count
def get_pricelist(
public=False,
limit=None,
offset=None,
search_name=None,
search_key=None,
sortBy=None,
descending=False,
):
count = None
query = DrinkPrice.query
if public:
query = query.filter(DrinkPrice.public)
if search_name:
if search_key == "name":
query = query.filter(DrinkPrice._volume.has(DrinkPriceVolume._drink.has(Drink.name.contains(search_name))))
if search_key == "type":
query = query.filter(
DrinkPrice._volume.has(
DrinkPriceVolume._drink.has(Drink.type.has(DrinkType.name.contains(search_name)))
)
)
if search_key == "tags":
query = query.filter(
DrinkPrice._volume.has(DrinkPriceVolume._drink.has(Drink.tags.any(Tag.name.conaitns(search_name))))
)
if search_key == "volume":
query = query.filter(DrinkPrice._volume.has(DrinkPriceVolume.volume == float(search_name)))
if search_key == "price":
query = query.filter(DrinkPrice.price == float(search_name))
if search_key == "description":
query = query.filter(DrinkPrice.description.contains(search_name))
else:
try:
search_name = float(search_name)
query = query.filter(
(DrinkPrice._volume.has(DrinkPriceVolume.volume == float(search_name)))
| (DrinkPrice.price == float(search_name))
)
except:
query = query.filter(
(DrinkPrice._volume.has(DrinkPriceVolume._drink.has(Drink.name.contains(search_name))))
| (
DrinkPrice._volume.has(
DrinkPriceVolume._drink.has(Drink.type.has(DrinkType.name.contains(search_name)))
)
)
| (
DrinkPrice._volume.has(
DrinkPriceVolume._drink.has(Drink.tags.any(Tag.name.contains(search_name)))
)
)
| (DrinkPrice.description.contains(search_name))
)
if sortBy == "type":
query = (
query.join(DrinkPrice._volume)
.join(DrinkPriceVolume._drink)
.join(Drink.type)
.order_by(DrinkType.name.desc() if descending else DrinkType.name.asc())
)
elif sortBy == "volume":
query = query.join(DrinkPrice._volume).order_by(
DrinkPriceVolume.volume.desc() if descending else DrinkPriceVolume.volume.asc()
)
elif sortBy == "price":
query = query.order_by(DrinkPrice.price.desc() if descending else DrinkPrice.price.asc())
else:
query = (
query.join(DrinkPrice._volume)
.join(DrinkPriceVolume._drink)
.order_by(Drink.name.desc() if descending else Drink.name.asc())
)
if limit is not None:
count = query.count()
query = query.limit(limit)
if offset is not None:
query = query.offset(offset)
prices = query.all()
for price in prices:
price._volume.drink = price._volume._drink
price.volume = price._volume
return prices, count
def get_drink(identifier, public=False):
drink = None
if isinstance(identifier, int):
drink = Drink.query.get(identifier)
elif isinstance(identifier, str):
drink = Drink.query.filter(Tag.name == identifier).one_or_none()
else:
raise BadRequest("Invalid identifier type for Drink")
if drink is None:
raise NotFound
if public:
return _create_public_drink(drink)
for volume in drink._volumes:
volume.prices = volume._prices
drink.volumes = drink._volumes
return drink
def set_drink(data):
return update_drink(-1, data)
def update_drink(identifier, data):
try:
session = extract_session()
if "id" in data:
data.pop("id")
volumes = data.pop("volumes") if "volumes" in data else None
tags = []
if "tags" in data:
_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"]
drink_type = get_drink_type(drink_type)
if identifier == -1:
drink = Drink()
db.session.add(drink)
else:
drink = get_drink(identifier)
for key, value in data.items():
if hasattr(drink, key) and key != "has_image":
setattr(drink, key, value if value != "" else None)
if drink_type:
drink.type = drink_type
if volumes is not None and session.user_.has_permission(EDIT_VOLUME):
drink._volumes = []
drink._volumes = set_volumes(volumes)
if len(tags) > 0:
drink.tags = tags
db.session.commit()
for volume in drink._volumes:
volume.prices = volume._prices
drink.volumes = drink._volumes
return drink
except (NotFound, KeyError):
raise BadRequest
def set_volumes(volumes):
retVal = []
if not isinstance(volumes, list):
raise BadRequest
for volume in volumes:
retVal.append(set_volume(volume))
return retVal
def delete_drink(identifier):
drink = get_drink(identifier)
db.session.delete(drink)
db.session.commit()
def get_volume(identifier):
return DrinkPriceVolume.query.get(identifier)
def get_volumes(drink_id=None):
if drink_id:
return DrinkPriceVolume.query.filter(DrinkPriceVolume.drink_id == drink_id).all()
return DrinkPriceVolume.query.all()
def set_volume(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
ingredients = None
if "prices" in values:
prices = values.pop("prices")
if "ingredients" in values:
ingredients = values.pop("ingredients")
values.pop("id", None)
volume = DrinkPriceVolume(**values)
db.session.add(volume)
if prices and session.user_.has_permission(EDIT_PRICE):
set_prices(prices, volume)
if ingredients and session.user_.has_permission(EDIT_INGREDIENTS_DRINK):
set_ingredients(ingredients, volume)
return volume
def set_prices(prices, volume):
if isinstance(prices, list):
_prices = []
for _price in prices:
price = set_price(_price)
_prices.append(price)
volume._prices = _prices
def set_ingredients(ingredients, volume):
if isinstance(ingredients, list):
_ingredietns = []
for _ingredient in ingredients:
ingredient = set_ingredient(_ingredient)
_ingredietns.append(ingredient)
volume.ingredients = _ingredietns
def delete_volume(identifier):
volume = get_volume(identifier)
db.session.delete(volume)
db.session.commit()
def get_price(identifier):
if isinstance(identifier, int):
return DrinkPrice.query.get(identifier)
raise NotFound
def get_prices(volume_id=None):
if volume_id:
return DrinkPrice.query.filter(DrinkPrice.volume_id_ == volume_id).all()
return DrinkPrice.query.all()
def set_price(data):
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.pop("id", -1)
price = DrinkPrice(**values)
db.session.add(price)
return price
def delete_price(identifier):
price = get_price(identifier)
db.session.delete(price)
db.session.commit()
def set_drink_ingredient(data):
allowed_keys = DrinkIngredient().serialize().keys()
values = {key: value for key, value in data.items() if key in allowed_keys}
if "cost_per_volume" in values:
values.pop("cost_per_volume")
if "name" in values:
values.pop("name")
values.pop("id", -1)
drink_ingredient = DrinkIngredient(**values)
db.session.add(drink_ingredient)
return drink_ingredient
def get_ingredient(identifier):
return Ingredient.query.get(identifier)
def set_ingredient(data):
drink_ingredient_value = None
extra_ingredient_value = None
if "drink_ingredient" in data:
drink_ingredient_value = data.pop("drink_ingredient")
if "extra_ingredient" in data:
extra_ingredient_value = data.pop("extra_ingredient")
data.pop("id", -1)
ingredient = Ingredient(**data)
db.session.add(ingredient)
if drink_ingredient_value:
ingredient.drink_ingredient = set_drink_ingredient(drink_ingredient_value)
if extra_ingredient_value:
if "id" in extra_ingredient_value:
ingredient.extra_ingredient = get_extra_ingredient(extra_ingredient_value.get("id"))
return ingredient
def delete_ingredient(identifier):
ingredient = get_ingredient(identifier)
if ingredient.drink_ingredient:
db.session.delete(ingredient.drink_ingredient)
db.session.delete(ingredient)
db.session.commit()
def get_extra_ingredients():
return ExtraIngredient.query.all()
def get_extra_ingredient(identifier):
return ExtraIngredient.query.get(identifier)
def set_extra_ingredient(data):
allowed_keys = ExtraIngredient().serialize().keys()
if "id" in data:
data.pop("id")
values = {key: value for key, value in data.items() if key in allowed_keys}
extra_ingredient = ExtraIngredient(**values)
db.session.add(extra_ingredient)
db.session.commit()
return extra_ingredient
def update_extra_ingredient(identifier, data):
allowed_keys = ExtraIngredient().serialize().keys()
if "id" in data:
data.pop("id")
values = {key: value for key, value in data.items() if key in allowed_keys}
extra_ingredient = get_extra_ingredient(identifier)
if extra_ingredient:
for key, value in values.items():
setattr(extra_ingredient, key, value)
db.session.commit()
return extra_ingredient
def delete_extra_ingredient(identifier):
extra_ingredient = get_extra_ingredient(identifier)
db.session.delete(extra_ingredient)
db.session.commit()
def save_drink_picture(identifier, file):
drink = delete_drink_picture(identifier)
drink.image_ = image_controller.upload_image(file)
db.session.commit()
return drink
def delete_drink_picture(identifier):
drink = get_drink(identifier)
if drink.image_:
db.session.delete(drink.image_)
drink.image_ = None
db.session.commit()
return drink

View File

@ -1,6 +1,5 @@
from datetime import datetime, timedelta
from flask import Blueprint from flask import Blueprint
from datetime import datetime, timedelta
from flaschengeist import logger from flaschengeist import logger
from flaschengeist.config import config from flaschengeist.config import config
@ -67,17 +66,15 @@ class SchedulerPlugin(Plugin):
changed = False changed = False
now = datetime.now() now = datetime.now()
status: dict = self.get_setting("status", default=dict()) status = self.get_setting("status", default=dict())
for id, task in _scheduled_tasks.items(): for id, task in _scheduled_tasks.items():
last_run = status.setdefault(id, now) last_run = status.setdefault(id, now)
status[id] = last_run
if last_run + task.interval <= now: if last_run + task.interval <= now:
logger.debug( logger.debug(
f"Run task {id}, was scheduled for {last_run + task.interval}, next iteration: {now + task.interval}" f"Run task {id}, was scheduled for {last_run + task.interval}, next iteration: {now + task.interval}"
) )
task.function() task.function()
status[id] = now
changed = True changed = True
else: else:
logger.debug(f"Skip task {id}, is scheduled for {last_run + task.interval}") logger.debug(f"Skip task {id}, is scheduled for {last_run + task.interval}")

View File

@ -65,6 +65,7 @@ flaschengeist.plugins =
roles = flaschengeist.plugins.roles:RolesPlugin roles = flaschengeist.plugins.roles:RolesPlugin
balance = flaschengeist.plugins.balance:BalancePlugin balance = flaschengeist.plugins.balance:BalancePlugin
mail = flaschengeist.plugins.message_mail:MailMessagePlugin mail = flaschengeist.plugins.message_mail:MailMessagePlugin
pricelist = flaschengeist.plugins.pricelist:PriceListPlugin
scheduler = flaschengeist.plugins.scheduler:SchedulerPlugin scheduler = flaschengeist.plugins.scheduler:SchedulerPlugin
[bdist_wheel] [bdist_wheel]