Merge remote-tracking branch 'origin/develop' into feature/pricelist

This commit is contained in:
Tim Gröger 2021-03-29 12:50:04 +02:00
commit 3a4e90f50e
26 changed files with 1103 additions and 965 deletions

View File

@ -45,9 +45,10 @@ def __load_plugins(app):
try: try:
logger.info(f"Load plugin {entry_point.name}") logger.info(f"Load plugin {entry_point.name}")
plugin = entry_point.load() plugin = entry_point.load()
setattr(plugin, "_plugin_name", entry_point.name) if not hasattr(plugin, "name"):
setattr(plugin, "name", entry_point.name)
plugin = plugin(config[entry_point.name]) plugin = plugin(config[entry_point.name])
if plugin.blueprint: if hasattr(plugin, "blueprint") and plugin.blueprint is not None:
app.register_blueprint(plugin.blueprint) app.register_blueprint(plugin.blueprint)
except: except:
logger.error( logger.error(

View File

@ -6,6 +6,7 @@ from werkzeug.exceptions import NotFound, BadRequest, Forbidden
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.models.notification import Notification
from flaschengeist.utils.hook import Hook from flaschengeist.utils.hook import Hook
from flaschengeist.utils.datetime import from_iso_format from flaschengeist.utils.datetime import from_iso_format
from flaschengeist.models.user import User, Role, _PasswordReset from flaschengeist.models.user import User, Role, _PasswordReset
@ -210,3 +211,15 @@ def persist(user=None):
if user: if user:
db.session.add(user) db.session.add(user)
db.session.commit() db.session.commit()
def get_notifications(user, start=None):
query = Notification.query.filter(Notification.user_id_ == user.id_)
if start is not None:
query = query.filter(Notification.time > start)
return query.order_by(Notification.time).all()
def delete_notification(nid, user):
Notification.query.filter(Notification.id == nid).filter(Notification.user_ == user).delete()
db.session.commit()

View File

@ -1,6 +1,8 @@
import sys import sys
import datetime import datetime
from sqlalchemy import BigInteger
from sqlalchemy.dialects import mysql
from sqlalchemy.types import DateTime, TypeDecorator from sqlalchemy.types import DateTime, TypeDecorator
@ -39,6 +41,12 @@ class ModelSerializeMixin:
return d return d
class Serial(TypeDecorator):
"""Same as MariaDB Serial used for IDs"""
impl = BigInteger().with_variant(mysql.BIGINT(unsigned=True), "mysql")
class UtcDateTime(TypeDecorator): class UtcDateTime(TypeDecorator):
"""Almost equivalent to `sqlalchemy.types.DateTime` with """Almost equivalent to `sqlalchemy.types.DateTime` with
``timezone=True`` option, but it differs from that by: ``timezone=True`` option, but it differs from that by:

View File

@ -0,0 +1,19 @@
from __future__ import annotations # TODO: Remove if python requirement is >= 3.10
from datetime import datetime
from typing import Any
from . import Serial, UtcDateTime, ModelSerializeMixin
from ..database import db
from .user import User
class Notification(db.Model, ModelSerializeMixin):
__tablename__ = "notification"
id: int = db.Column("id", Serial, primary_key=True)
plugin: str = db.Column(db.String(30), nullable=False)
text: str = db.Column(db.Text)
data: Any = db.Column(db.PickleType(protocol=4))
time: datetime = db.Column(UtcDateTime, nullable=False, default=UtcDateTime.current_utc)
user_id_: int = db.Column("user_id", Serial, db.ForeignKey("user.id"), nullable=False)
user_: User = db.relationship("User")

View File

@ -2,7 +2,7 @@ from __future__ import annotations # TODO: Remove if python requirement is >= 3
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from . import ModelSerializeMixin, UtcDateTime from . import ModelSerializeMixin, UtcDateTime, Serial
from .user import User from .user import User
from flaschengeist.database import db from flaschengeist.database import db
from secrets import compare_digest from secrets import compare_digest
@ -26,8 +26,8 @@ class Session(db.Model, ModelSerializeMixin):
platform: str = db.Column(db.String(30)) platform: str = db.Column(db.String(30))
userid: str = "" userid: str = ""
_id = db.Column("id", db.Integer, primary_key=True) _id = db.Column("id", Serial, primary_key=True)
_user_id = db.Column("user_id", db.Integer, db.ForeignKey("user.id")) _user_id = db.Column("user_id", Serial, db.ForeignKey("user.id"))
user_: User = db.relationship("User", back_populates="sessions_") user_: User = db.relationship("User", back_populates="sessions_")
@property @property

View File

@ -1,11 +1,13 @@
from __future__ import annotations # TODO: Remove if python requirement is >= 3.10 from __future__ import annotations # TODO: Remove if python requirement is >= 3.10
from typing import Any
from . import Serial
from ..database import db from ..database import db
class _PluginSetting(db.Model): class _PluginSetting(db.Model):
__tablename__ = "plugin_setting" __tablename__ = "plugin_setting"
id = db.Column("id", db.Integer, primary_key=True) id = db.Column("id", Serial, primary_key=True)
plugin: str = db.Column(db.String(30)) plugin: str = db.Column(db.String(30))
name: str = db.Column(db.String(30), nullable=False) name: str = db.Column(db.String(30), nullable=False)
value: any = db.Column(db.PickleType(protocol=4)) value: Any = db.Column(db.PickleType(protocol=4))

View File

@ -6,19 +6,19 @@ from datetime import date, datetime
from sqlalchemy.orm.collections import attribute_mapped_collection from sqlalchemy.orm.collections import attribute_mapped_collection
from ..database import db from ..database import db
from . import ModelSerializeMixin, UtcDateTime from . import ModelSerializeMixin, UtcDateTime, Serial
association_table = db.Table( association_table = db.Table(
"user_x_role", "user_x_role",
db.Column("user_id", db.Integer, db.ForeignKey("user.id")), db.Column("user_id", Serial, db.ForeignKey("user.id")),
db.Column("role_id", db.Integer, db.ForeignKey("role.id")), db.Column("role_id", Serial, db.ForeignKey("role.id")),
) )
role_permission_association_table = db.Table( role_permission_association_table = db.Table(
"role_x_permission", "role_x_permission",
db.Column("role_id", db.Integer, db.ForeignKey("role.id")), db.Column("role_id", Serial, db.ForeignKey("role.id")),
db.Column("permission_id", db.Integer, db.ForeignKey("permission.id")), db.Column("permission_id", Serial, db.ForeignKey("permission.id")),
) )
@ -26,12 +26,12 @@ class Permission(db.Model, ModelSerializeMixin):
__tablename__ = "permission" __tablename__ = "permission"
name: str = db.Column(db.String(30), unique=True) name: str = db.Column(db.String(30), unique=True)
_id = db.Column("id", db.Integer, primary_key=True) _id = db.Column("id", Serial, primary_key=True)
class Role(db.Model, ModelSerializeMixin): class Role(db.Model, ModelSerializeMixin):
__tablename__ = "role" __tablename__ = "role"
id: int = db.Column(db.Integer, primary_key=True) id: int = db.Column(Serial, primary_key=True)
name: str = db.Column(db.String(30), unique=True) name: str = db.Column(db.String(30), unique=True)
permissions: list[Permission] = db.relationship("Permission", secondary=role_permission_association_table) permissions: list[Permission] = db.relationship("Permission", secondary=role_permission_association_table)
@ -62,7 +62,7 @@ class User(db.Model, ModelSerializeMixin):
permissions: Optional[list[str]] = None permissions: Optional[list[str]] = None
avatar_url: Optional[str] = "" avatar_url: Optional[str] = ""
id_ = db.Column("id", db.Integer, primary_key=True) id_ = db.Column("id", Serial, primary_key=True)
roles_: list[Role] = db.relationship("Role", secondary=association_table, cascade="save-update, merge") roles_: list[Role] = db.relationship("Role", secondary=association_table, cascade="save-update, merge")
sessions_ = db.relationship("Session", back_populates="user_") sessions_ = db.relationship("Session", back_populates="user_")
@ -101,8 +101,8 @@ class User(db.Model, ModelSerializeMixin):
class _UserAttribute(db.Model, ModelSerializeMixin): class _UserAttribute(db.Model, ModelSerializeMixin):
__tablename__ = "user_attribute" __tablename__ = "user_attribute"
id = db.Column("id", db.Integer, primary_key=True) id = db.Column("id", Serial, primary_key=True)
user: User = db.Column("user", db.Integer, db.ForeignKey("user.id"), nullable=False) user: User = db.Column("user", Serial, db.ForeignKey("user.id"), nullable=False)
name: str = db.Column(db.String(30)) name: str = db.Column(db.String(30))
value: any = db.Column(db.PickleType(protocol=4)) value: any = db.Column(db.PickleType(protocol=4))
@ -111,7 +111,7 @@ class _PasswordReset(db.Model):
"""Table containing password reset requests""" """Table containing password reset requests"""
__tablename__ = "password_reset" __tablename__ = "password_reset"
_user_id: User = db.Column("user", db.Integer, db.ForeignKey("user.id"), primary_key=True) _user_id: User = db.Column("user", Serial, db.ForeignKey("user.id"), primary_key=True)
user: User = db.relationship("User", foreign_keys=[_user_id]) user: User = db.relationship("User", foreign_keys=[_user_id])
token: str = db.Column(db.String(32)) token: str = db.Column(db.String(32))
expires: datetime = db.Column(UtcDateTime) expires: datetime = db.Column(UtcDateTime)

View File

@ -3,6 +3,7 @@ import pkg_resources
from werkzeug.exceptions import MethodNotAllowed, NotFound from werkzeug.exceptions import MethodNotAllowed, NotFound
from flaschengeist.database import db from flaschengeist.database import db
from flaschengeist.models.notification import Notification
from flaschengeist.models.user import _Avatar from flaschengeist.models.user import _Avatar
from flaschengeist.models.setting import _PluginSetting from flaschengeist.models.setting import _PluginSetting
from flaschengeist.utils.hook import HookBefore, HookAfter from flaschengeist.utils.hook import HookBefore, HookAfter
@ -30,15 +31,16 @@ class Plugin:
"""Base class for all Plugins """Base class for all Plugins
If your class uses custom models add a static property called ``models``""" If your class uses custom models add a static property called ``models``"""
def __init__(self, config=None, blueprint=None, permissions=[]): blueprint = None # You have to override
permissions = [] # You have to override
name = "plugin" # You have to override
models = None # You have to override
def __init__(self, config=None):
"""Constructor called by create_app """Constructor called by create_app
Args: Args:
config: Dict configuration containing the plugin section config: Dict configuration containing the plugin section
blueprint: A flask blueprint containing all plugin routes
permissions: List of permissions of this Plugin
""" """
self.blueprint = blueprint
self.permissions = permissions
self.version = pkg_resources.get_distribution(self.__module__.split(".")[0]).version self.version = pkg_resources.get_distribution(self.__module__.split(".")[0]).version
def install(self): def install(self):
@ -63,7 +65,7 @@ class Plugin:
""" """
try: try:
setting = ( setting = (
_PluginSetting.query.filter(_PluginSetting.plugin == self._plugin_name) _PluginSetting.query.filter(_PluginSetting.plugin == self.name)
.filter(_PluginSetting.name == name) .filter(_PluginSetting.name == name)
.one() .one()
) )
@ -81,14 +83,19 @@ class Plugin:
value: Value to be stored value: Value to be stored
""" """
setting = ( setting = (
_PluginSetting.query.filter(_PluginSetting.plugin == self._plugin_name) _PluginSetting.query.filter(_PluginSetting.plugin == self.name)
.filter(_PluginSetting.name == name) .filter(_PluginSetting.name == name)
.one_or_none() .one_or_none()
) )
if setting is not None: if setting is not None:
setting.value = value setting.value = value
else: else:
db.session.add(_PluginSetting(plugin=self._plugin_name, name=name, value=value)) db.session.add(_PluginSetting(plugin=self.name, name=name, value=value))
db.session.commit()
def notify(self, user, text: str, data=None):
n = Notification(text=text, data=data, plugin=self.name, user_=user)
db.session.add(n)
db.session.commit() db.session.commit()
def serialize(self): def serialize(self):

View File

@ -11,15 +11,13 @@ from flaschengeist.utils.HTTP import no_content, created
from flaschengeist.utils.decorators import login_required from flaschengeist.utils.decorators import login_required
from flaschengeist.controller import sessionController, userController from flaschengeist.controller import sessionController, userController
auth_bp = Blueprint("auth", __name__)
class AuthRoutePlugin(Plugin): class AuthRoutePlugin(Plugin):
def __init__(self, conf): name = "auth"
super().__init__(blueprint=auth_bp) blueprint = Blueprint(name, __name__)
@auth_bp.route("/auth", methods=["POST"]) @AuthRoutePlugin.blueprint.route("/auth", methods=["POST"])
def login(): def login():
"""Login in an user and create a session """Login in an user and create a session
@ -52,7 +50,7 @@ def login():
return created(session) return created(session)
@auth_bp.route("/auth", methods=["GET"]) @AuthRoutePlugin.blueprint.route("/auth", methods=["GET"])
@login_required() @login_required()
def get_sessions(current_session, **kwargs): def get_sessions(current_session, **kwargs):
"""Get all valid sessions of current user """Get all valid sessions of current user
@ -66,7 +64,7 @@ def get_sessions(current_session, **kwargs):
return jsonify(sessions) return jsonify(sessions)
@auth_bp.route("/auth/<token>", methods=["DELETE"]) @AuthRoutePlugin.blueprint.route("/auth/<token>", methods=["DELETE"])
@login_required() @login_required()
def delete_session(token, current_session, **kwargs): def delete_session(token, current_session, **kwargs):
"""Delete a session aka "logout" """Delete a session aka "logout"
@ -88,7 +86,7 @@ def delete_session(token, current_session, **kwargs):
return "" return ""
@auth_bp.route("/auth/<token>", methods=["GET"]) @AuthRoutePlugin.blueprint.route("/auth/<token>", methods=["GET"])
@login_required() @login_required()
def get_session(token, current_session, **kwargs): def get_session(token, current_session, **kwargs):
"""Retrieve information about a session """Retrieve information about a session
@ -111,7 +109,7 @@ def get_session(token, current_session, **kwargs):
return jsonify(session) return jsonify(session)
@auth_bp.route("/auth/<token>", methods=["PUT"]) @AuthRoutePlugin.blueprint.route("/auth/<token>", methods=["PUT"])
@login_required() @login_required()
def set_lifetime(token, current_session, **kwargs): def set_lifetime(token, current_session, **kwargs):
"""Set lifetime of a session """Set lifetime of a session
@ -141,7 +139,7 @@ def set_lifetime(token, current_session, **kwargs):
raise BadRequest raise BadRequest
@auth_bp.route("/auth/<token>/user", methods=["GET"]) @AuthRoutePlugin.blueprint.route("/auth/<token>/user", methods=["GET"])
@login_required() @login_required()
def get_assocd_user(token, current_session, **kwargs): def get_assocd_user(token, current_session, **kwargs):
"""Retrieve user owning a session """Retrieve user owning a session
@ -164,7 +162,7 @@ def get_assocd_user(token, current_session, **kwargs):
return jsonify(session.user_) return jsonify(session.user_)
@auth_bp.route("/auth/reset", methods=["POST"]) @AuthRoutePlugin.blueprint.route("/auth/reset", methods=["POST"])
def reset_password(): def reset_password():
data = request.get_json() data = request.get_json()
if "userid" in data: if "userid" in data:

View File

@ -3,30 +3,25 @@
Extends users plugin with balance functions Extends users plugin with balance functions
""" """
from datetime import datetime, timezone from werkzeug.local import LocalProxy
from flask import Blueprint, current_app
from flaschengeist.utils.HTTP import no_content
from flask import Blueprint, request, jsonify
from werkzeug.exceptions import Forbidden, BadRequest
from flaschengeist import logger from flaschengeist import logger
from flaschengeist.utils import HTTP
from flaschengeist.models.session import Session
from flaschengeist.utils.datetime import from_iso_format
from flaschengeist.utils.decorators import login_required
from flaschengeist.controller import userController
from flaschengeist.plugins import Plugin, before_update_user from flaschengeist.plugins import Plugin, before_update_user
from . import balance_controller, permissions, models from . import permissions, models
balance_bp = Blueprint("balance", __name__)
class BalancePlugin(Plugin): class BalancePlugin(Plugin):
name = "balance"
blueprint = Blueprint(name, __name__)
permissions = permissions.permissions
plugin = LocalProxy(lambda: current_app.config["FG_PLUGINS"][BalancePlugin.name])
models = models models = models
def __init__(self, config): def __init__(self, config):
super().__init__(blueprint=balance_bp, permissions=permissions.permissions) super(BalancePlugin, self).__init__(config)
from . import routes, balance_controller
@before_update_user @before_update_user
def set_default_limit(user): def set_default_limit(user):
@ -36,277 +31,3 @@ class BalancePlugin(Plugin):
balance_controller.set_limit(user, limit, override=False) balance_controller.set_limit(user, limit, override=False)
except KeyError: except KeyError:
pass pass
def install(self):
from flaschengeist.database import db
db.create_all()
def str2bool(string: str):
if string.lower() in ["true", "yes", "1"]:
return True
elif string.lower() in ["false", "no", "0"]:
return False
raise ValueError
@balance_bp.route("/users/<userid>/balance/shortcuts", methods=["GET", "PUT"])
@login_required()
def get_shortcuts(userid, current_session: Session):
"""Get balance shortcuts of an user
Route: ``/users/<userid>/balance/shortcuts`` | 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("balance_shortcuts", []))
else:
data = request.get_json()
if not isinstance(data, list) or not all(isinstance(n, (int, float)) for n in data):
raise BadRequest
data.sort(reverse=True)
user.set_attribute("balance_shortcuts", data)
userController.persist()
return no_content()
@balance_bp.route("/users/<userid>/balance/limit", methods=["GET"])
@login_required()
def get_limit(userid, current_session: Session):
"""Get limit of an user
Route: ``/users/<userid>/balance/limit`` | Method: ``GET``
Args:
userid: Userid identifying the user
current_session: Session sent with Authorization Header
Returns:
JSON object containing the limit (or Null if no limit set) or HTTP error
"""
user = userController.get_user(userid)
if (user != current_session.user_ and not current_session.user_.has_permission(permissions.SET_LIMIT)) or (
user == current_session.user_ and not user.has_permission(permissions.SHOW)
):
raise Forbidden
return {"limit": balance_controller.get_limit(user)}
@balance_bp.route("/users/<userid>/balance/limit", methods=["PUT"])
@login_required(permissions.SET_LIMIT)
def set_limit(userid, current_session: Session):
"""Set the limit of an user
Route: ``/users/<userid>/balance/limit`` | Method: ``PUT``
POST-data: ``{limit: float}``
Args:
userid: Userid identifying the user
current_session: Session sent with Authorization Header
Returns:
HTTP-200 or HTTP error
"""
user = userController.get_user(userid)
data = request.get_json()
try:
limit = data["limit"]
except (TypeError, KeyError):
raise BadRequest
balance_controller.set_limit(user, limit)
return HTTP.no_content()
@balance_bp.route("/users/<userid>/balance", methods=["GET"])
@login_required(permission=permissions.SHOW)
def get_balance(userid, current_session: Session):
"""Get balance of user, optionally filtered
Route: ``/users/<userid>/balance`` | Method: ``GET``
GET-parameters: ```{from?: string, to?: string}```
Args:
userid: Userid of user to get balance from
current_session: Session sent with Authorization Header
Returns:
JSON object containing credit, debit and balance or HTTP error
"""
if userid != current_session.user_.userid and not current_session.user_.has_permission(permissions.SHOW_OTHER):
raise Forbidden
# Might raise NotFound
user = userController.get_user(userid)
start = request.args.get("from")
if start:
start = from_iso_format(start)
else:
start = datetime.fromtimestamp(0, tz=timezone.utc)
end = request.args.get("to")
if end:
end = from_iso_format(end)
else:
end = datetime.now(tz=timezone.utc)
balance = balance_controller.get_balance(user, start, end)
return {"credit": balance[0], "debit": balance[1], "balance": balance[2]}
@balance_bp.route("/users/<userid>/balance/transactions", methods=["GET"])
@login_required(permission=permissions.SHOW)
def get_transactions(userid, current_session: Session):
"""Get transactions of user, optionally filtered
Returns also count of transactions if limit is set (e.g. just count with limit = 0)
Route: ``/users/<userid>/balance/transactions`` | Method: ``GET``
GET-parameters: ```{from?: string, to?: string, limit?: int, offset?: int}```
Args:
userid: Userid of user to get transactions from
current_session: Session sent with Authorization Header
Returns:
JSON Object {transactions: Transaction[], count?: number} or HTTP error
"""
if userid != current_session.user_.userid and not current_session.user_.has_permission(permissions.SHOW_OTHER):
raise Forbidden
# Might raise NotFound
user = userController.get_user(userid)
start = request.args.get("from")
if start:
start = from_iso_format(start)
end = request.args.get("to")
if end:
end = from_iso_format(end)
show_reversals = request.args.get("showReversals", False)
show_cancelled = request.args.get("showCancelled", True)
limit = request.args.get("limit")
offset = request.args.get("offset")
try:
if limit is not None:
limit = int(limit)
if offset is not None:
offset = int(offset)
if not isinstance(show_reversals, bool):
show_reversals = str2bool(show_reversals)
if not isinstance(show_cancelled, bool):
show_cancelled = str2bool(show_cancelled)
except ValueError:
raise BadRequest
transactions, count = balance_controller.get_transactions(
user, start, end, limit, offset, show_reversal=show_reversals, show_cancelled=show_cancelled
)
return {"transactions": transactions, "count": count}
@balance_bp.route("/users/<userid>/balance", methods=["PUT"])
@login_required()
def change_balance(userid, current_session: Session):
"""Change balance of an user
If ``sender`` is preset in POST-data, the action is handled as a transfer from ``sender`` to user.
Route: ``/users/<userid>/balance`` | Method: ``PUT``
POST-data: ``{amount: float, sender: string}``
Args:
userid: userid identifying user to change balance
current_session: Session sent with Authorization Header
Returns:
JSON encoded transaction (201) or HTTP error
"""
data = request.get_json()
try:
amount = data["amount"]
except (TypeError, KeyError):
raise BadRequest
sender = data.get("sender", None)
user = userController.get_user(userid)
if sender:
sender = userController.get_user(sender)
if sender == user:
raise BadRequest
if (sender == current_session.user_ and sender.has_permission(permissions.SEND)) or (
sender != current_session.user_ and current_session.user_.has_permission(permissions.SEND_OTHER)
):
return HTTP.created(balance_controller.send(sender, user, amount, current_session.user_))
elif (
amount < 0
and (
(user == current_session.user_ and user.has_permission(permissions.DEBIT_OWN))
or current_session.user_.has_permission(permissions.DEBIT)
)
) or (amount > 0 and current_session.user_.has_permission(permissions.CREDIT)):
return HTTP.created(balance_controller.change_balance(user, data["amount"], current_session.user_))
raise Forbidden
@balance_bp.route("/balance/<int:transaction_id>", methods=["DELETE"])
@login_required()
def reverse_transaction(transaction_id, current_session: Session):
"""Reverse a transaction
Route: ``/balance/<int:transaction_id>`` | Method: ``DELETE``
Args:
transaction_id: Identifier of the transaction
current_session: Session sent with Authorization Header
Returns:
JSON encoded reversal (transaction) (201) or HTTP error
"""
transaction = balance_controller.get_transaction(transaction_id)
if current_session.user_.has_permission(permissions.REVERSAL) or (
transaction.sender_ == current_session.user_
and (datetime.now(tz=timezone.utc) - transaction.time).total_seconds() < 10
):
reversal = balance_controller.reverse_transaction(transaction, current_session.user_)
return HTTP.created(reversal)
raise Forbidden
@balance_bp.route("/balance", methods=["GET"])
@login_required(permission=permissions.SHOW_OTHER)
def get_balances(current_session: Session):
"""Get all balances
Route: ``/balance`` | Method: ``GET``
Args:
current_session: Session sent with Authorization Header
Returns:
JSON Array containing credit, debit and userid for each user or HTTP error
"""
balances = balance_controller.get_balances()
return jsonify([{"userid": u, "credit": v[0], "debit": v[1]} for u, v in balances.items()])

View File

@ -11,7 +11,7 @@ from flaschengeist.database import db
from flaschengeist.models.user import User from flaschengeist.models.user import User
from .models import Transaction from .models import Transaction
from . import permissions from . import permissions, BalancePlugin
__attribute_limit = "balance_limit" __attribute_limit = "balance_limit"
@ -86,6 +86,10 @@ def send(sender: User, receiver, amount: float, author: User):
transaction = Transaction(sender_=sender, receiver_=receiver, amount=amount, author_=author) transaction = Transaction(sender_=sender, receiver_=receiver, amount=amount, author_=author)
db.session.add(transaction) db.session.add(transaction)
db.session.commit() db.session.commit()
if sender is not None and sender.id_ != author.id_:
BalancePlugin.plugin.notify(sender, "Neue Transaktion")
if receiver is not None and receiver.id_ != author.id_:
BalancePlugin.plugin.notify(receiver, "Neue Transaktion")
return transaction return transaction

View File

@ -6,21 +6,21 @@ from sqlalchemy.ext.hybrid import hybrid_property
from flaschengeist.database import db from flaschengeist.database import db
from flaschengeist.models.user import User from flaschengeist.models.user import User
from flaschengeist.models import ModelSerializeMixin, UtcDateTime from flaschengeist.models import ModelSerializeMixin, UtcDateTime, Serial
class Transaction(db.Model, ModelSerializeMixin): class Transaction(db.Model, ModelSerializeMixin):
__tablename__ = "balance_transaction" __tablename__ = "balance_transaction"
# Protected foreign key properties # Protected foreign key properties
_receiver_id = db.Column("receiver_id", db.Integer, db.ForeignKey("user.id")) _receiver_id = db.Column("receiver_id", Serial, db.ForeignKey("user.id"))
_sender_id = db.Column("sender_id", db.Integer, db.ForeignKey("user.id")) _sender_id = db.Column("sender_id", Serial, db.ForeignKey("user.id"))
_author_id = db.Column("author_id", db.Integer, db.ForeignKey("user.id"), nullable=False) _author_id = db.Column("author_id", Serial, db.ForeignKey("user.id"), nullable=False)
# Public and exported member # Public and exported member
id: int = db.Column("id", db.Integer, primary_key=True) id: int = db.Column("id", Serial, primary_key=True)
time: datetime = db.Column(UtcDateTime, nullable=False, default=UtcDateTime.current_utc) time: datetime = db.Column(UtcDateTime, nullable=False, default=UtcDateTime.current_utc)
amount: float = db.Column(db.Numeric(precision=5, scale=2, asdecimal=False), nullable=False) amount: float = db.Column(db.Numeric(precision=5, scale=2, asdecimal=False), nullable=False)
reversal_id: Optional[int] = db.Column(db.Integer, db.ForeignKey("balance_transaction.id")) reversal_id: Optional[int] = db.Column(Serial, db.ForeignKey("balance_transaction.id"))
# Dummy properties used for JSON serialization (userid instead of full user) # Dummy properties used for JSON serialization (userid instead of full user)
author_id: Optional[str] = None author_id: Optional[str] = None

View File

@ -0,0 +1,279 @@
from datetime import datetime, timezone
from werkzeug.exceptions import Forbidden, BadRequest
from flask import request, jsonify
from flaschengeist.utils import HTTP
from flaschengeist.models.session import Session
from flaschengeist.utils.datetime import from_iso_format
from flaschengeist.utils.decorators import login_required
from flaschengeist.controller import userController
from . import BalancePlugin, balance_controller, permissions
def str2bool(string: str):
if string.lower() in ["true", "yes", "1"]:
return True
elif string.lower() in ["false", "no", "0"]:
return False
raise ValueError
@BalancePlugin.blueprint.route("/users/<userid>/balance/shortcuts", methods=["GET", "PUT"])
@login_required()
def get_shortcuts(userid, current_session: Session):
"""Get balance shortcuts of an user
Route: ``/users/<userid>/balance/shortcuts`` | 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("balance_shortcuts", []))
else:
data = request.get_json()
if not isinstance(data, list) or not all(isinstance(n, (int, float)) for n in data):
raise BadRequest
data.sort(reverse=True)
user.set_attribute("balance_shortcuts", data)
userController.persist()
return HTTP.no_content()
@BalancePlugin.blueprint.route("/users/<userid>/balance/limit", methods=["GET"])
@login_required()
def get_limit(userid, current_session: Session):
"""Get limit of an user
Route: ``/users/<userid>/balance/limit`` | Method: ``GET``
Args:
userid: Userid identifying the user
current_session: Session sent with Authorization Header
Returns:
JSON object containing the limit (or Null if no limit set) or HTTP error
"""
user = userController.get_user(userid)
if (user != current_session.user_ and not current_session.user_.has_permission(permissions.SET_LIMIT)) or (
user == current_session.user_ and not user.has_permission(permissions.SHOW)
):
raise Forbidden
return {"limit": balance_controller.get_limit(user)}
@BalancePlugin.blueprint.route("/users/<userid>/balance/limit", methods=["PUT"])
@login_required(permissions.SET_LIMIT)
def set_limit(userid, current_session: Session):
"""Set the limit of an user
Route: ``/users/<userid>/balance/limit`` | Method: ``PUT``
POST-data: ``{limit: float}``
Args:
userid: Userid identifying the user
current_session: Session sent with Authorization Header
Returns:
HTTP-200 or HTTP error
"""
user = userController.get_user(userid)
data = request.get_json()
try:
limit = data["limit"]
except (TypeError, KeyError):
raise BadRequest
balance_controller.set_limit(user, limit)
return HTTP.no_content()
@BalancePlugin.blueprint.route("/users/<userid>/balance", methods=["GET"])
@login_required(permission=permissions.SHOW)
def get_balance(userid, current_session: Session):
"""Get balance of user, optionally filtered
Route: ``/users/<userid>/balance`` | Method: ``GET``
GET-parameters: ```{from?: string, to?: string}```
Args:
userid: Userid of user to get balance from
current_session: Session sent with Authorization Header
Returns:
JSON object containing credit, debit and balance or HTTP error
"""
if userid != current_session.user_.userid and not current_session.user_.has_permission(permissions.SHOW_OTHER):
raise Forbidden
# Might raise NotFound
user = userController.get_user(userid)
start = request.args.get("from")
if start:
start = from_iso_format(start)
else:
start = datetime.fromtimestamp(0, tz=timezone.utc)
end = request.args.get("to")
if end:
end = from_iso_format(end)
else:
end = datetime.now(tz=timezone.utc)
balance = balance_controller.get_balance(user, start, end)
return {"credit": balance[0], "debit": balance[1], "balance": balance[2]}
@BalancePlugin.blueprint.route("/users/<userid>/balance/transactions", methods=["GET"])
@login_required(permission=permissions.SHOW)
def get_transactions(userid, current_session: Session):
"""Get transactions of user, optionally filtered
Returns also count of transactions if limit is set (e.g. just count with limit = 0)
Route: ``/users/<userid>/balance/transactions`` | Method: ``GET``
GET-parameters: ```{from?: string, to?: string, limit?: int, offset?: int}```
Args:
userid: Userid of user to get transactions from
current_session: Session sent with Authorization Header
Returns:
JSON Object {transactions: Transaction[], count?: number} or HTTP error
"""
if userid != current_session.user_.userid and not current_session.user_.has_permission(permissions.SHOW_OTHER):
raise Forbidden
# Might raise NotFound
user = userController.get_user(userid)
start = request.args.get("from")
if start:
start = from_iso_format(start)
end = request.args.get("to")
if end:
end = from_iso_format(end)
show_reversals = request.args.get("showReversals", False)
show_cancelled = request.args.get("showCancelled", True)
limit = request.args.get("limit")
offset = request.args.get("offset")
try:
if limit is not None:
limit = int(limit)
if offset is not None:
offset = int(offset)
if not isinstance(show_reversals, bool):
show_reversals = str2bool(show_reversals)
if not isinstance(show_cancelled, bool):
show_cancelled = str2bool(show_cancelled)
except ValueError:
raise BadRequest
transactions, count = balance_controller.get_transactions(
user, start, end, limit, offset, show_reversal=show_reversals, show_cancelled=show_cancelled
)
return {"transactions": transactions, "count": count}
@BalancePlugin.blueprint.route("/users/<userid>/balance", methods=["PUT"])
@login_required()
def change_balance(userid, current_session: Session):
"""Change balance of an user
If ``sender`` is preset in POST-data, the action is handled as a transfer from ``sender`` to user.
Route: ``/users/<userid>/balance`` | Method: ``PUT``
POST-data: ``{amount: float, sender: string}``
Args:
userid: userid identifying user to change balance
current_session: Session sent with Authorization Header
Returns:
JSON encoded transaction (201) or HTTP error
"""
data = request.get_json()
try:
amount = data["amount"]
except (TypeError, KeyError):
raise BadRequest
sender = data.get("sender", None)
user = userController.get_user(userid)
if sender:
sender = userController.get_user(sender)
if sender == user:
raise BadRequest
if (sender == current_session.user_ and sender.has_permission(permissions.SEND)) or (
sender != current_session.user_ and current_session.user_.has_permission(permissions.SEND_OTHER)
):
return HTTP.created(balance_controller.send(sender, user, amount, current_session.user_))
elif (
amount < 0
and (
(user == current_session.user_ and user.has_permission(permissions.DEBIT_OWN))
or current_session.user_.has_permission(permissions.DEBIT)
)
) or (amount > 0 and current_session.user_.has_permission(permissions.CREDIT)):
return HTTP.created(balance_controller.change_balance(user, data["amount"], current_session.user_))
raise Forbidden
@BalancePlugin.blueprint.route("/balance/<int:transaction_id>", methods=["DELETE"])
@login_required()
def reverse_transaction(transaction_id, current_session: Session):
"""Reverse a transaction
Route: ``/balance/<int:transaction_id>`` | Method: ``DELETE``
Args:
transaction_id: Identifier of the transaction
current_session: Session sent with Authorization Header
Returns:
JSON encoded reversal (transaction) (201) or HTTP error
"""
transaction = balance_controller.get_transaction(transaction_id)
if current_session.user_.has_permission(permissions.REVERSAL) or (
transaction.sender_ == current_session.user_
and (datetime.now(tz=timezone.utc) - transaction.time).total_seconds() < 10
):
reversal = balance_controller.reverse_transaction(transaction, current_session.user_)
return HTTP.created(reversal)
raise Forbidden
@BalancePlugin.blueprint.route("/balance", methods=["GET"])
@login_required(permission=permissions.SHOW_OTHER)
def get_balances(current_session: Session):
"""Get all balances
Route: ``/balance`` | Method: ``GET``
Args:
current_session: Session sent with Authorization Header
Returns:
JSON Array containing credit, debit and userid for each user or HTTP error
"""
balances = balance_controller.get_balances()
return jsonify([{"userid": u, "credit": v[0], "debit": v[1]} for u, v in balances.items()])

View File

@ -1,441 +1,21 @@
"""Schedule plugin """Events plugin
Provides duty schedule / duty roster functions Provides duty schedule / duty roster functions
""" """
from datetime import datetime, timedelta, timezone from flask import Blueprint, current_app
from http.client import NO_CONTENT from werkzeug.local import LocalProxy
from flask import Blueprint, request, jsonify
from werkzeug.exceptions import BadRequest, NotFound, Forbidden
from flaschengeist.plugins import Plugin from flaschengeist.plugins import Plugin
from flaschengeist.models.session import Session from . import permissions, models
from flaschengeist.utils.decorators import login_required
from flaschengeist.utils.datetime import from_iso_format
from flaschengeist.controller import userController
from . import event_controller, permissions
from . import models
from ...utils.HTTP import no_content
events_bp = Blueprint("events", __name__)
class EventPlugin(Plugin): class EventPlugin(Plugin):
name = "events"
plugin = LocalProxy(lambda: current_app.config["FG_PLUGINS"][EventPlugin.name])
permissions = permissions.permissions
blueprint = Blueprint(name, __name__)
models = models models = models
def __init__(self, config): def __init__(self, cfg):
super().__init__( super(EventPlugin, self).__init__(cfg)
blueprint=events_bp, from . import routes
permissions=permissions.permissions,
)
@events_bp.route("/events/templates", methods=["GET"])
@login_required()
def get_templates(current_session):
return jsonify(event_controller.get_templates())
@events_bp.route("/events/event-types", methods=["GET"])
@events_bp.route("/events/event-types/<int:identifier>", methods=["GET"])
@login_required()
def get_event_types(current_session, identifier=None):
"""Get EventType(s)
Route: ``/events/event-types`` | Method: ``GET``
Route: ``/events/event-types/<identifier>`` | Method: ``GET``
Args:
current_session: Session sent with Authorization Header
identifier: If querying a specific EventType
Returns:
JSON encoded (list of) EventType(s) or HTTP-error
"""
if identifier:
result = event_controller.get_event_type(identifier)
else:
result = event_controller.get_event_types()
return jsonify(result)
@events_bp.route("/events/event-types", methods=["POST"])
@login_required(permission=permissions.EVENT_TYPE)
def new_event_type(current_session):
"""Create a new EventType
Route: ``/events/event-types`` | Method: ``POST``
POST-data: ``{name: string}``
Args:
current_session: Session sent with Authorization Header
Returns:
HTTP-Created or HTTP-error
"""
data = request.get_json()
if "name" not in data:
raise BadRequest
event_type = event_controller.create_event_type(data["name"])
return jsonify(event_type)
@events_bp.route("/events/event-types/<int:identifier>", methods=["PUT", "DELETE"])
@login_required(permission=permissions.EVENT_TYPE)
def modify_event_type(identifier, current_session):
"""Rename or delete an event type
Route: ``/events/event-types/<id>`` | Method: ``PUT`` or ``DELETE``
POST-data: (if renaming) ``{name: string}``
Args:
identifier: Identifier of the EventType
current_session: Session sent with Authorization Header
Returns:
HTTP-NoContent or HTTP-error
"""
if request.method == "DELETE":
event_controller.delete_event_type(identifier)
else:
data = request.get_json()
if "name" not in data:
raise BadRequest("Parameter missing in data")
event_controller.rename_event_type(identifier, data["name"])
return "", NO_CONTENT
@events_bp.route("/events/job-types", methods=["GET"])
@login_required()
def get_job_types(current_session):
"""Get all JobTypes
Route: ``/events/job-types`` | Method: ``GET``
Args:
current_session: Session sent with Authorization Header
Returns:
JSON encoded list of JobType HTTP-error
"""
types = event_controller.get_job_types()
return jsonify(types)
@events_bp.route("/events/job-types", methods=["POST"])
@login_required(permission=permissions.JOB_TYPE)
def new_job_type(current_session):
"""Create a new JobType
Route: ``/events/job-types`` | Method: ``POST``
POST-data: ``{name: string}``
Args:
current_session: Session sent with Authorization Header
Returns:
JSON encoded JobType or HTTP-error
"""
data = request.get_json()
if "name" not in data:
raise BadRequest
jt = event_controller.create_job_type(data["name"])
return jsonify(jt)
@events_bp.route("/events/job-types/<int:type_id>", methods=["PUT", "DELETE"])
@login_required(permission=permissions.JOB_TYPE)
def modify_job_type(type_id, current_session):
"""Rename or delete a JobType
Route: ``/events/job-types/<name>`` | Method: ``PUT`` or ``DELETE``
POST-data: (if renaming) ``{name: string}``
Args:
type_id: Identifier of the JobType
current_session: Session sent with Authorization Header
Returns:
HTTP-NoContent or HTTP-error
"""
if request.method == "DELETE":
event_controller.delete_job_type(type_id)
else:
data = request.get_json()
if "name" not in data:
raise BadRequest("Parameter missing in data")
event_controller.rename_job_type(type_id, data["name"])
return "", NO_CONTENT
@events_bp.route("/events/<int:event_id>", methods=["GET"])
@login_required()
def get_event(event_id, current_session):
"""Get event by id
Route: ``/events/<event_id>`` | Method: ``GET``
Args:
event_id: ID identifying the event
current_session: Session sent with Authorization Header
Returns:
JSON encoded event object
"""
event = event_controller.get_event(event_id)
return jsonify(event)
@events_bp.route("/events", methods=["GET"])
@login_required()
def get_filtered_events(current_session):
begin = request.args.get("from")
if begin is not None:
begin = from_iso_format(begin)
end = request.args.get("to")
if end is not None:
end = from_iso_format(end)
if begin is None and end is None:
begin = datetime.now()
return jsonify(event_controller.get_events(begin, end))
@events_bp.route("/events/<int:year>/<int:month>", methods=["GET"])
@events_bp.route("/events/<int:year>/<int:month>/<int:day>", methods=["GET"])
@login_required()
def get_events(current_session, year=datetime.now().year, month=datetime.now().month, day=None):
"""Get Event objects for specified date (or month or year),
if nothing set then events for current month are returned
Route: ``/events[/<year>/<month>[/<int:day>]]`` | Method: ``GET``
Args:
year (int, optional): year to query, defaults to current year
month (int, optional): month to query (if set), defaults to current month
day (int, optional): day to query events for (if set)
current_session: Session sent with Authorization Header
Returns:
JSON encoded list containing events found or HTTP-error
"""
try:
begin = datetime(year=year, month=month, day=1, tzinfo=timezone.utc)
if day:
begin += timedelta(days=day - 1)
end = begin + timedelta(days=1)
else:
if month == 12:
end = datetime(year=year + 1, month=1, day=1, tzinfo=timezone.utc)
else:
end = datetime(year=year, month=month + 1, day=1, tzinfo=timezone.utc)
events = event_controller.get_events(begin, end)
return jsonify(events)
except ValueError:
raise BadRequest("Invalid date given")
def _add_job(event, data):
try:
start = from_iso_format(data["start"])
end = None
if "end" in data:
end = from_iso_format(data["end"])
required_services = data["required_services"]
job_type = data["type"]
if isinstance(job_type, dict):
job_type = data["type"]["id"]
except (KeyError, ValueError):
raise BadRequest("Missing or invalid POST parameter")
job_type = event_controller.get_job_type(job_type)
event_controller.add_job(event, job_type, required_services, start, end, comment=data.get("comment", None))
@events_bp.route("/events", methods=["POST"])
@login_required(permission=permissions.CREATE)
def create_event(current_session):
"""Create an new event
Route: ``/events`` | Method: ``POST``
POST-data: See interfaces for Event, can already contain jobs
Args:
current_session: Session sent with Authorization Header
Returns:
JSON encoded Event object or HTTP-error
"""
data = request.get_json()
end = data.get("end", None)
try:
start = from_iso_format(data["start"])
if end is not None:
end = from_iso_format(end)
data_type = data["type"]
if isinstance(data_type, dict):
data_type = data["type"]["id"]
event_type = event_controller.get_event_type(data_type)
except KeyError:
raise BadRequest("Missing POST parameter")
except (NotFound, ValueError):
raise BadRequest("Invalid parameter")
event = event_controller.create_event(
start=start,
end=end,
name=data.get("name", None),
is_template=data.get("is_template", None),
event_type=event_type,
description=data.get("description", None),
)
if "jobs" in data:
for job in data["jobs"]:
_add_job(event, job)
return jsonify(event)
@events_bp.route("/events/<int:event_id>", methods=["PUT"])
@login_required(permission=permissions.EDIT)
def modify_event(event_id, current_session):
"""Modify an event
Route: ``/events/<event_id>`` | Method: ``PUT``
POST-data: See interfaces for Event, can already contain slots
Args:
event_id: Identifier of the event
current_session: Session sent with Authorization Header
Returns:
JSON encoded Event object or HTTP-error
"""
event = event_controller.get_event(event_id)
data = request.get_json()
if "start" in data:
event.start = from_iso_format(data["start"])
if "end" in data:
event.end = from_iso_format(data["end"])
if "description" in data:
event.description = data["description"]
if "type" in data:
event_type = event_controller.get_event_type(data["type"])
event.type = event_type
event_controller.update()
return jsonify(event)
@events_bp.route("/events/<int:event_id>", methods=["DELETE"])
@login_required(permission=permissions.DELETE)
def delete_event(event_id, current_session):
"""Delete an event
Route: ``/events/<event_id>`` | Method: ``DELETE``
Args:
event_id: Identifier of the event
current_session: Session sent with Authorization Header
Returns:
HTTP-NoContent or HTTP-error
"""
event_controller.delete_event(event_id)
return "", NO_CONTENT
@events_bp.route("/events/<int:event_id>/jobs", methods=["POST"])
@login_required(permission=permissions.EDIT)
def add_job(event_id, current_session):
"""Add an new Job to an Event / EventSlot
Route: ``/events/<event_id>/jobs`` | Method: ``POST``
POST-data: See Job
Args:
event_id: Identifier of the event
current_session: Session sent with Authorization Header
Returns:
JSON encoded Event object or HTTP-error
"""
event = event_controller.get_event(event_id)
_add_job(event, request.get_json())
return jsonify(event)
@events_bp.route("/events/<int:event_id>/jobs/<int:job_id>", methods=["DELETE"])
@login_required(permission=permissions.DELETE)
def delete_job(event_id, job_id, current_session):
"""Delete a Job
Route: ``/events/<event_id>/jobs/<job_id>`` | Method: ``DELETE``
Args:
event_id: Identifier of the event
job_id: Identifier of the Job
current_session: Session sent with Authorization Header
Returns:
HTTP-no-content or HTTP error
"""
job_slot = event_controller.get_job(job_id, event_id)
event_controller.delete_job(job_slot)
return no_content()
@events_bp.route("/events/<int:event_id>/jobs/<int:job_id>", methods=["PUT"])
@login_required()
def update_job(event_id, job_id, current_session: Session):
"""Edit Job or assign user to the Job
Route: ``/events/<event_id>/jobs/<job_id>`` | Method: ``PUT``
POST-data: See TS interface for Job or ``{user: {userid: string, value: number}}``
Args:
event_id: Identifier of the event
job_id: Identifier of the Job
current_session: Session sent with Authorization Header
Returns:
JSON encoded Job object or HTTP-error
"""
job = event_controller.get_job(job_id, event_id)
data = request.get_json()
if not data:
raise BadRequest
if ("user" not in data or len(data) > 1) and not current_session.user_.has_permission(permissions.EDIT):
raise Forbidden
if "user" in data:
try:
user = userController.get_user(data["user"]["userid"])
value = data["user"]["value"]
if (user == current_session.user_ and not user.has_permission(permissions.ASSIGN)) or (
user != current_session.user_ and not current_session.user_.has_permission(permissions.ASSIGN_OTHER)
):
raise Forbidden
event_controller.assign_to_job(job, user, value)
except (KeyError, ValueError):
raise BadRequest
if "required_services" in data:
job.required_services = data["required_services"]
if "type" in data:
job.type = event_controller.get_job_type(data["type"])
event_controller.update()
return jsonify(job)
# TODO: JobTransfer

View File

@ -1,14 +1,14 @@
from datetime import datetime from datetime import datetime, timedelta, timezone
from typing import Optional from typing import Optional
from sqlalchemy import or_, and_
from werkzeug.exceptions import BadRequest, NotFound from werkzeug.exceptions import BadRequest, NotFound
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from flaschengeist import logger from flaschengeist import logger
from flaschengeist.database import db from flaschengeist.database import db
from flaschengeist.plugins.events import EventPlugin
from flaschengeist.plugins.events.models import EventType, Event, Job, JobType, Service from flaschengeist.plugins.events.models import EventType, Event, Job, JobType, Service
from flaschengeist.utils.datetime import from_iso_format from flaschengeist.utils.scheduler import scheduled
def update(): def update():
@ -102,10 +102,21 @@ def delete_job_type(name):
raise BadRequest("Type still in use") raise BadRequest("Type still in use")
def get_event(event_id) -> Event: def clear_backup(event: Event):
for job in event.jobs:
services = []
for service in job.services:
if not service.is_backup:
services.append(service)
job.services = services
def get_event(event_id, with_backup=False) -> Event:
event = Event.query.get(event_id) event = Event.query.get(event_id)
if event is None: if event is None:
raise NotFound raise NotFound
if not with_backup:
return clear_backup(event)
return event return event
@ -113,11 +124,12 @@ def get_templates():
return Event.query.filter(Event.is_template == True).all() return Event.query.filter(Event.is_template == True).all()
def get_events(start: Optional[datetime] = None, end=None): def get_events(start: Optional[datetime] = None, end=None, with_backup=False):
"""Query events which start from begin until end """Query events which start from begin until end
Args: Args:
start (datetime): Earliest start start (datetime): Earliest start
end (datetime): Latest start end (datetime): Latest start
with_backup (bool): Export also backup services
Returns: collection of Event objects Returns: collection of Event objects
""" """
@ -126,7 +138,11 @@ def get_events(start: Optional[datetime] = None, end=None):
query = query.filter(start <= Event.start) query = query.filter(start <= Event.start)
if end is not None: if end is not None:
query = query.filter(Event.start < end) query = query.filter(Event.start < end)
return query.all() events = query.all()
if not with_backup:
for event in events:
clear_backup(event)
return events
def delete_event(event_id): def delete_event(event_id):
@ -202,3 +218,26 @@ def assign_to_job(job: Job, user, value):
service = Service(user_=user, value=value, job_=job) service = Service(user_=user, value=value, job_=job)
db.session.add(service) db.session.add(service)
db.session.commit() db.session.commit()
@scheduled
def assign_backups():
logger.debug("Notifications")
now = datetime.now(tz=timezone.utc)
# now + backup_time + next cron tick
start = now + timedelta(hours=16) + timedelta(minutes=30)
services = Service.query.filter(Service.is_backup == True).join(Job).filter(Job.start <= start).all()
for service in services:
if service.job_.start <= now or service.job_.is_full():
EventPlugin.plugin.notify(
service.user_, "Your backup assignment was cancelled.", {"event_id": service.job_.event_id_}
)
logger.debug(f"Service is outdated or full, removing. {service.serialize()}")
db.session.delete(service)
else:
service.is_backup = False
logger.debug(f"Service not full, assigning backup. {service.serialize()}")
EventPlugin.plugin.notify(
service.user_, "Your backup assignment was accepted.", {"event_id": service.job_.event_id_}
)
db.session.commit()

View File

@ -1,12 +1,11 @@
from __future__ import annotations # TODO: Remove if python requirement is >= 3.10 from __future__ import annotations # TODO: Remove if python requirement is >= 3.10
import enum
from datetime import datetime from datetime import datetime
from typing import Optional, Union from typing import Optional, Union
from sqlalchemy import UniqueConstraint from sqlalchemy import UniqueConstraint
from flaschengeist.models import ModelSerializeMixin, UtcDateTime from flaschengeist.models import ModelSerializeMixin, UtcDateTime, Serial
from flaschengeist.models.user import User from flaschengeist.models.user import User
from flaschengeist.database import db from flaschengeist.database import db
@ -19,13 +18,13 @@ _table_prefix_ = "events_"
class EventType(db.Model, ModelSerializeMixin): class EventType(db.Model, ModelSerializeMixin):
__tablename__ = _table_prefix_ + "event_type" __tablename__ = _table_prefix_ + "event_type"
id: int = db.Column(db.Integer, primary_key=True) id: int = db.Column(Serial, 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)
class JobType(db.Model, ModelSerializeMixin): class JobType(db.Model, ModelSerializeMixin):
__tablename__ = _table_prefix_ + "job_type" __tablename__ = _table_prefix_ + "job_type"
id: int = db.Column(db.Integer, primary_key=True) id: int = db.Column(Serial, 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)
@ -37,12 +36,11 @@ class JobType(db.Model, ModelSerializeMixin):
class Service(db.Model, ModelSerializeMixin): class Service(db.Model, ModelSerializeMixin):
__tablename__ = _table_prefix_ + "service" __tablename__ = _table_prefix_ + "service"
userid: str = "" userid: str = ""
is_backup: bool = db.Column(db.Boolean, default=False)
value: float = db.Column(db.Numeric(precision=3, scale=2, asdecimal=False), nullable=False) value: float = db.Column(db.Numeric(precision=3, scale=2, asdecimal=False), nullable=False)
_job_id = db.Column( _job_id = db.Column("job_id", Serial, db.ForeignKey(f"{_table_prefix_}job.id"), nullable=False, primary_key=True)
"job_id", db.Integer, db.ForeignKey(f"{_table_prefix_}job.id"), nullable=False, primary_key=True _user_id = db.Column("user_id", Serial, db.ForeignKey("user.id"), nullable=False, primary_key=True)
)
_user_id = db.Column("user_id", db.Integer, db.ForeignKey("user.id"), nullable=False, primary_key=True)
user_: User = db.relationship("User") user_: User = db.relationship("User")
job_: Job = db.relationship("Job") job_: Job = db.relationship("Job")
@ -54,9 +52,9 @@ class Service(db.Model, ModelSerializeMixin):
class Job(db.Model, ModelSerializeMixin): class Job(db.Model, ModelSerializeMixin):
__tablename__ = _table_prefix_ + "job" __tablename__ = _table_prefix_ + "job"
_type_id = db.Column("type_id", db.Integer, db.ForeignKey(f"{_table_prefix_}job_type.id"), nullable=False) _type_id = db.Column("type_id", Serial, db.ForeignKey(f"{_table_prefix_}job_type.id"), nullable=False)
id: int = db.Column(db.Integer, primary_key=True) id: int = db.Column(Serial, primary_key=True)
start: datetime = db.Column(UtcDateTime, nullable=False) start: datetime = db.Column(UtcDateTime, nullable=False)
end: Optional[datetime] = db.Column(UtcDateTime) end: Optional[datetime] = db.Column(UtcDateTime)
type: Union[JobType, int] = db.relationship("JobType") type: Union[JobType, int] = db.relationship("JobType")
@ -65,7 +63,7 @@ class Job(db.Model, ModelSerializeMixin):
required_services: float = db.Column(db.Numeric(precision=4, scale=2, asdecimal=False), nullable=False) required_services: float = db.Column(db.Numeric(precision=4, scale=2, asdecimal=False), nullable=False)
event_ = db.relationship("Event", back_populates="jobs") event_ = db.relationship("Event", back_populates="jobs")
event_id_ = db.Column("event_id", db.Integer, db.ForeignKey(f"{_table_prefix_}event.id"), nullable=False) event_id_ = db.Column("event_id", Serial, db.ForeignKey(f"{_table_prefix_}event.id"), nullable=False)
__table_args__ = (UniqueConstraint("type_id", "start", "event_id", name="_type_start_uc"),) __table_args__ = (UniqueConstraint("type_id", "start", "event_id", name="_type_start_uc"),)
@ -77,7 +75,7 @@ class Event(db.Model, ModelSerializeMixin):
"""Model for an Event""" """Model for an Event"""
__tablename__ = _table_prefix_ + "event" __tablename__ = _table_prefix_ + "event"
id: int = db.Column(db.Integer, primary_key=True) id: int = db.Column(Serial, primary_key=True)
start: datetime = db.Column(UtcDateTime, nullable=False) start: datetime = db.Column(UtcDateTime, nullable=False)
end: Optional[datetime] = db.Column(UtcDateTime) end: Optional[datetime] = db.Column(UtcDateTime)
name: Optional[str] = db.Column(db.String(255)) name: Optional[str] = db.Column(db.String(255))
@ -89,15 +87,15 @@ class Event(db.Model, ModelSerializeMixin):
) )
# Protected for internal use # Protected for internal use
_type_id = db.Column( _type_id = db.Column(
"type_id", db.Integer, db.ForeignKey(f"{_table_prefix_}event_type.id", ondelete="CASCADE"), nullable=False "type_id", Serial, db.ForeignKey(f"{_table_prefix_}event_type.id", ondelete="CASCADE"), nullable=False
) )
class Invite(db.Model, ModelSerializeMixin): class Invite(db.Model, ModelSerializeMixin):
__tablename__ = _table_prefix_ + "invite" __tablename__ = _table_prefix_ + "invite"
id: int = db.Column(db.Integer, primary_key=True) id: int = db.Column(Serial, primary_key=True)
job_id: int = db.Column(db.Integer, db.ForeignKey(_table_prefix_ + "job.id"), nullable=False) job_id: int = db.Column(Serial, db.ForeignKey(_table_prefix_ + "job.id"), nullable=False)
# Dummy properties for API export # Dummy properties for API export
invitee_id: str = None invitee_id: str = None
sender_id: str = None sender_id: str = None
@ -105,8 +103,8 @@ class Invite(db.Model, ModelSerializeMixin):
invitee_: User = db.relationship("User", foreign_keys="Invite._invitee_id") invitee_: User = db.relationship("User", foreign_keys="Invite._invitee_id")
sender_: User = db.relationship("User", foreign_keys="Invite._sender_id") sender_: User = db.relationship("User", foreign_keys="Invite._sender_id")
# Protected properties needed for internal use # Protected properties needed for internal use
_invitee_id = db.Column("invitee_id", db.Integer, db.ForeignKey("user.id"), nullable=False) _invitee_id = db.Column("invitee_id", Serial, db.ForeignKey("user.id"), nullable=False)
_sender_id = db.Column("sender_id", db.Integer, db.ForeignKey("user.id"), nullable=False) _sender_id = db.Column("sender_id", Serial, db.ForeignKey("user.id"), nullable=False)
@property @property
def invitee_id(self): def invitee_id(self):

View File

@ -1,22 +1,25 @@
CREATE = "schedule_create" CREATE = "events_create"
"""Can create events""" """Can create events"""
EDIT = "schedule_edit" EDIT = "events_edit"
"""Can edit events""" """Can edit events"""
DELETE = "schedule_delete" DELETE = "events_delete"
"""Can delete events""" """Can delete events"""
EVENT_TYPE = "schedule_event_type" EVENT_TYPE = "events_event_type"
"""Can create and edit EventTypes""" """Can create and edit EventTypes"""
JOB_TYPE = "schedule_job_type" JOB_TYPE = "events_job_type"
"""Can create and edit JobTypes""" """Can create and edit JobTypes"""
ASSIGN = "schedule_assign" ASSIGN = "events_assign"
"""Can self assign to jobs""" """Can self assign to jobs"""
ASSIGN_OTHER = "schedule_assign_other" ASSIGN_OTHER = "events_assign_other"
"""Can assign other users to jobs""" """Can assign other users to jobs"""
SEE_BACKUP = "events_see_backup"
"""Can see users assigned as backup"""
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

@ -0,0 +1,431 @@
from datetime import datetime, timedelta, timezone
from http.client import NO_CONTENT
from flask import request, jsonify
from werkzeug.exceptions import BadRequest, NotFound, Forbidden
from flaschengeist.models.session import Session
from flaschengeist.utils.decorators import login_required
from flaschengeist.utils.datetime import from_iso_format
from flaschengeist.controller import userController
from . import event_controller, permissions, EventPlugin
from ...utils.HTTP import no_content
@EventPlugin.blueprint.route("/events/templates", methods=["GET"])
@login_required()
def get_templates(current_session):
return jsonify(event_controller.get_templates())
@EventPlugin.blueprint.route("/events/event-types", methods=["GET"])
@EventPlugin.blueprint.route("/events/event-types/<int:identifier>", methods=["GET"])
@login_required()
def get_event_types(current_session, identifier=None):
"""Get EventType(s)
Route: ``/events/event-types`` | Method: ``GET``
Route: ``/events/event-types/<identifier>`` | Method: ``GET``
Args:
current_session: Session sent with Authorization Header
identifier: If querying a specific EventType
Returns:
JSON encoded (list of) EventType(s) or HTTP-error
"""
if identifier:
result = event_controller.get_event_type(identifier)
else:
result = event_controller.get_event_types()
return jsonify(result)
@EventPlugin.blueprint.route("/events/event-types", methods=["POST"])
@login_required(permission=permissions.EVENT_TYPE)
def new_event_type(current_session):
"""Create a new EventType
Route: ``/events/event-types`` | Method: ``POST``
POST-data: ``{name: string}``
Args:
current_session: Session sent with Authorization Header
Returns:
HTTP-Created or HTTP-error
"""
data = request.get_json()
if "name" not in data:
raise BadRequest
event_type = event_controller.create_event_type(data["name"])
return jsonify(event_type)
@EventPlugin.blueprint.route("/events/event-types/<int:identifier>", methods=["PUT", "DELETE"])
@login_required(permission=permissions.EVENT_TYPE)
def modify_event_type(identifier, current_session):
"""Rename or delete an event type
Route: ``/events/event-types/<id>`` | Method: ``PUT`` or ``DELETE``
POST-data: (if renaming) ``{name: string}``
Args:
identifier: Identifier of the EventType
current_session: Session sent with Authorization Header
Returns:
HTTP-NoContent or HTTP-error
"""
if request.method == "DELETE":
event_controller.delete_event_type(identifier)
else:
data = request.get_json()
if "name" not in data:
raise BadRequest("Parameter missing in data")
event_controller.rename_event_type(identifier, data["name"])
return "", NO_CONTENT
@EventPlugin.blueprint.route("/events/job-types", methods=["GET"])
@login_required()
def get_job_types(current_session):
"""Get all JobTypes
Route: ``/events/job-types`` | Method: ``GET``
Args:
current_session: Session sent with Authorization Header
Returns:
JSON encoded list of JobType HTTP-error
"""
types = event_controller.get_job_types()
return jsonify(types)
@EventPlugin.blueprint.route("/events/job-types", methods=["POST"])
@login_required(permission=permissions.JOB_TYPE)
def new_job_type(current_session):
"""Create a new JobType
Route: ``/events/job-types`` | Method: ``POST``
POST-data: ``{name: string}``
Args:
current_session: Session sent with Authorization Header
Returns:
JSON encoded JobType or HTTP-error
"""
data = request.get_json()
if "name" not in data:
raise BadRequest
jt = event_controller.create_job_type(data["name"])
return jsonify(jt)
@EventPlugin.blueprint.route("/events/job-types/<int:type_id>", methods=["PUT", "DELETE"])
@login_required(permission=permissions.JOB_TYPE)
def modify_job_type(type_id, current_session):
"""Rename or delete a JobType
Route: ``/events/job-types/<name>`` | Method: ``PUT`` or ``DELETE``
POST-data: (if renaming) ``{name: string}``
Args:
type_id: Identifier of the JobType
current_session: Session sent with Authorization Header
Returns:
HTTP-NoContent or HTTP-error
"""
if request.method == "DELETE":
event_controller.delete_job_type(type_id)
else:
data = request.get_json()
if "name" not in data:
raise BadRequest("Parameter missing in data")
event_controller.rename_job_type(type_id, data["name"])
return "", NO_CONTENT
@EventPlugin.blueprint.route("/events/<int:event_id>", methods=["GET"])
@login_required()
def get_event(event_id, current_session):
"""Get event by id
Route: ``/events/<event_id>`` | Method: ``GET``
Args:
event_id: ID identifying the event
current_session: Session sent with Authorization Header
Returns:
JSON encoded event object
"""
event = event_controller.get_event(
event_id, with_backup=current_session.user_.has_permission(permissions.SEE_BACKUP)
)
return jsonify(event)
@EventPlugin.blueprint.route("/events", methods=["GET"])
@login_required()
def get_filtered_events(current_session):
begin = request.args.get("from")
if begin is not None:
begin = from_iso_format(begin)
end = request.args.get("to")
if end is not None:
end = from_iso_format(end)
if begin is None and end is None:
begin = datetime.now()
return jsonify(
event_controller.get_events(
begin, end, with_backup=current_session.user_.has_permission(permissions.SEE_BACKUP)
)
)
@EventPlugin.blueprint.route("/events/<int:year>/<int:month>", methods=["GET"])
@EventPlugin.blueprint.route("/events/<int:year>/<int:month>/<int:day>", methods=["GET"])
@login_required()
def get_events(current_session, year=datetime.now().year, month=datetime.now().month, day=None):
"""Get Event objects for specified date (or month or year),
if nothing set then events for current month are returned
Route: ``/events[/<year>/<month>[/<int:day>]]`` | Method: ``GET``
Args:
year (int, optional): year to query, defaults to current year
month (int, optional): month to query (if set), defaults to current month
day (int, optional): day to query events for (if set)
current_session: Session sent with Authorization Header
Returns:
JSON encoded list containing events found or HTTP-error
"""
try:
begin = datetime(year=year, month=month, day=1, tzinfo=timezone.utc)
if day:
begin += timedelta(days=day - 1)
end = begin + timedelta(days=1)
else:
if month == 12:
end = datetime(year=year + 1, month=1, day=1, tzinfo=timezone.utc)
else:
end = datetime(year=year, month=month + 1, day=1, tzinfo=timezone.utc)
events = event_controller.get_events(
begin, end, with_backup=current_session.user_.has_permission(permissions.SEE_BACKUP)
)
return jsonify(events)
except ValueError:
raise BadRequest("Invalid date given")
def _add_job(event, data):
try:
start = from_iso_format(data["start"])
end = None
if "end" in data:
end = from_iso_format(data["end"])
required_services = data["required_services"]
job_type = data["type"]
if isinstance(job_type, dict):
job_type = data["type"]["id"]
except (KeyError, ValueError):
raise BadRequest("Missing or invalid POST parameter")
job_type = event_controller.get_job_type(job_type)
event_controller.add_job(event, job_type, required_services, start, end, comment=data.get("comment", None))
@EventPlugin.blueprint.route("/events", methods=["POST"])
@login_required(permission=permissions.CREATE)
def create_event(current_session):
"""Create an new event
Route: ``/events`` | Method: ``POST``
POST-data: See interfaces for Event, can already contain jobs
Args:
current_session: Session sent with Authorization Header
Returns:
JSON encoded Event object or HTTP-error
"""
data = request.get_json()
end = data.get("end", None)
try:
start = from_iso_format(data["start"])
if end is not None:
end = from_iso_format(end)
data_type = data["type"]
if isinstance(data_type, dict):
data_type = data["type"]["id"]
event_type = event_controller.get_event_type(data_type)
except KeyError:
raise BadRequest("Missing POST parameter")
except (NotFound, ValueError):
raise BadRequest("Invalid parameter")
event = event_controller.create_event(
start=start,
end=end,
name=data.get("name", None),
is_template=data.get("is_template", None),
event_type=event_type,
description=data.get("description", None),
)
if "jobs" in data:
for job in data["jobs"]:
_add_job(event, job)
return jsonify(event)
@EventPlugin.blueprint.route("/events/<int:event_id>", methods=["PUT"])
@login_required(permission=permissions.EDIT)
def modify_event(event_id, current_session):
"""Modify an event
Route: ``/events/<event_id>`` | Method: ``PUT``
POST-data: See interfaces for Event, can already contain slots
Args:
event_id: Identifier of the event
current_session: Session sent with Authorization Header
Returns:
JSON encoded Event object or HTTP-error
"""
event = event_controller.get_event(event_id)
data = request.get_json()
if "start" in data:
event.start = from_iso_format(data["start"])
if "end" in data:
event.end = from_iso_format(data["end"])
if "description" in data:
event.description = data["description"]
if "type" in data:
event_type = event_controller.get_event_type(data["type"])
event.type = event_type
event_controller.update()
return jsonify(event)
@EventPlugin.blueprint.route("/events/<int:event_id>", methods=["DELETE"])
@login_required(permission=permissions.DELETE)
def delete_event(event_id, current_session):
"""Delete an event
Route: ``/events/<event_id>`` | Method: ``DELETE``
Args:
event_id: Identifier of the event
current_session: Session sent with Authorization Header
Returns:
HTTP-NoContent or HTTP-error
"""
event_controller.delete_event(event_id)
return "", NO_CONTENT
@EventPlugin.blueprint.route("/events/<int:event_id>/jobs", methods=["POST"])
@login_required(permission=permissions.EDIT)
def add_job(event_id, current_session):
"""Add an new Job to an Event / EventSlot
Route: ``/events/<event_id>/jobs`` | Method: ``POST``
POST-data: See Job
Args:
event_id: Identifier of the event
current_session: Session sent with Authorization Header
Returns:
JSON encoded Event object or HTTP-error
"""
event = event_controller.get_event(event_id)
_add_job(event, request.get_json())
return jsonify(event)
@EventPlugin.blueprint.route("/events/<int:event_id>/jobs/<int:job_id>", methods=["DELETE"])
@login_required(permission=permissions.DELETE)
def delete_job(event_id, job_id, current_session):
"""Delete a Job
Route: ``/events/<event_id>/jobs/<job_id>`` | Method: ``DELETE``
Args:
event_id: Identifier of the event
job_id: Identifier of the Job
current_session: Session sent with Authorization Header
Returns:
HTTP-no-content or HTTP error
"""
job_slot = event_controller.get_job(job_id, event_id)
event_controller.delete_job(job_slot)
return no_content()
@EventPlugin.blueprint.route("/events/<int:event_id>/jobs/<int:job_id>", methods=["PUT"])
@login_required()
def update_job(event_id, job_id, current_session: Session):
"""Edit Job or assign user to the Job
Route: ``/events/<event_id>/jobs/<job_id>`` | Method: ``PUT``
POST-data: See TS interface for Job or ``{user: {userid: string, value: number}}``
Args:
event_id: Identifier of the event
job_id: Identifier of the Job
current_session: Session sent with Authorization Header
Returns:
JSON encoded Job object or HTTP-error
"""
job = event_controller.get_job(job_id, event_id)
data = request.get_json()
if not data:
raise BadRequest
if ("user" not in data or len(data) > 1) and not current_session.user_.has_permission(permissions.EDIT):
raise Forbidden
if "user" in data:
try:
user = userController.get_user(data["user"]["userid"])
value = data["user"]["value"]
if (user == current_session.user_ and not user.has_permission(permissions.ASSIGN)) or (
user != current_session.user_ and not current_session.user_.has_permission(permissions.ASSIGN_OTHER)
):
raise Forbidden
event_controller.assign_to_job(job, user, value)
except (KeyError, ValueError):
raise BadRequest
if "required_services" in data:
job.required_services = data["required_services"]
if "type" in data:
job.type = event_controller.get_job_type(data["type"])
event_controller.update()
return jsonify(job)
# TODO: JobTransfer

View File

@ -1,42 +1,42 @@
"""Pricelist plugin""" """Pricelist plugin"""
from flask import Blueprint, jsonify, request from flask import Blueprint, jsonify, request, current_app
from http.client import NO_CONTENT from werkzeug.local import LocalProxy
from werkzeug.exceptions import BadRequest, Forbidden, Unauthorized
from flaschengeist.plugins import Plugin from flaschengeist.plugins import Plugin
from flaschengeist.utils.decorators import login_required,extract_session from flaschengeist.utils.decorators import login_required, extract_session
from werkzeug.exceptions import BadRequest, Forbidden, Unauthorized from flaschengeist.utils.HTTP import no_content
from flaschengeist.config import config from flaschengeist.models.session import Session
from flaschengeist.controller import userController
from . import models from . import models
from . import pricelist_controller, permissions from . import pricelist_controller, permissions
from ...controller import userController
from ...models.session import Session
from ...utils.HTTP import no_content
pricelist_bp = Blueprint("pricelist", __name__, url_prefix="/pricelist")
class PriceListPlugin(Plugin): class PriceListPlugin(Plugin):
name = "pricelist"
blueprint = Blueprint(name, __name__, url_prefix="/pricelist")
plugin = LocalProxy(lambda: current_app.config["FG_PLUGINS"][PriceListPlugin.name])
models = models models = models
def __init__(self, cfg): def __init__(self, cfg):
super().__init__(blueprint=pricelist_bp, permissions=permissions.permissions) super().__init__(cfg)
config = {"discount": 0} config = {"discount": 0}
config.update(cfg) config.update(cfg)
@pricelist_bp.route("/drink-types", methods=["GET"]) @PriceListPlugin.blueprint.route("/drink-types", methods=["GET"])
@pricelist_bp.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):
if identifier: if identifier is None:
result = pricelist_controller.get_drink_type(identifier)
else:
result = pricelist_controller.get_drink_types() result = pricelist_controller.get_drink_types()
else:
result = pricelist_controller.get_drink_type(identifier)
return jsonify(result) return jsonify(result)
@pricelist_bp.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):
data = request.get_json() data = request.get_json()
@ -46,25 +46,25 @@ def new_drink_type(current_session):
return jsonify(drink_type) return jsonify(drink_type)
@pricelist_bp.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):
data = request.get_json() data = request.get_json()
if "name" not in data: if "name" not in data:
raise BadRequest raise BadRequest
drink_type = pricelist_controller.rename_drink_type(data["id"], data["name"]) drink_type = pricelist_controller.rename_drink_type(identifier, data["name"])
return jsonify(drink_type) return jsonify(drink_type)
@pricelist_bp.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):
pricelist_controller.delete_drink_type(identifier) pricelist_controller.delete_drink_type(identifier)
return "", NO_CONTENT return no_content()
@pricelist_bp.route("/tags", methods=["GET"]) @PriceListPlugin.blueprint.route("/tags", methods=["GET"])
@pricelist_bp.route("/tags/<int:identifier>", methods=["GET"]) @PriceListPlugin.blueprint.route("/tags/<int:identifier>", methods=["GET"])
def get_tags(identifier=None): def get_tags(identifier=None):
if identifier: if identifier:
result = pricelist_controller.get_tag(identifier) result = pricelist_controller.get_tag(identifier)
@ -73,7 +73,7 @@ def get_tags(identifier=None):
return jsonify(result) return jsonify(result)
@pricelist_bp.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):
data = request.get_json() data = request.get_json()
@ -83,25 +83,25 @@ def new_tag(current_session):
return jsonify(drink_type) return jsonify(drink_type)
@pricelist_bp.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):
data = request.get_json() data = request.get_json()
if "name" not in data: if "name" not in data:
raise BadRequest raise BadRequest
drink_type = pricelist_controller.rename_tag(data["name"]) tag = pricelist_controller.rename_tag(identifier, data["name"])
return jsonify(drink_type) return jsonify(tag)
@pricelist_bp.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):
pricelist_controller.delete_tag(identifier) pricelist_controller.delete_tag(identifier)
return "", NO_CONTENT return no_content()
@pricelist_bp.route("/drinks", methods=["GET"]) @PriceListPlugin.blueprint.route("/drinks", methods=["GET"])
@pricelist_bp.route("/drinks/<int:identifier>", methods=["GET"]) @PriceListPlugin.blueprint.route("/drinks/<int:identifier>", methods=["GET"])
def get_drinks(identifier=None): def get_drinks(identifier=None):
public = True public = True
try: try:
@ -116,85 +116,87 @@ def get_drinks(identifier=None):
result = pricelist_controller.get_drinks(public=public) result = pricelist_controller.get_drinks(public=public)
return jsonify(result) return jsonify(result)
@pricelist_bp.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)) return jsonify(pricelist_controller.get_drinks(name))
@pricelist_bp.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):
data = request.get_json() data = request.get_json()
return jsonify(pricelist_controller.set_drink(data)) return jsonify(pricelist_controller.set_drink(data))
@pricelist_bp.route("/drinks/<int:identifier>", methods=["PUT"]) @PriceListPlugin.blueprint.route("/drinks/<int:identifier>", methods=["PUT"])
def update_drink(identifier): def update_drink(identifier):
data = request.get_json() data = request.get_json()
return jsonify(pricelist_controller.update_drink(identifier, data)) return jsonify(pricelist_controller.update_drink(identifier, data))
@pricelist_bp.route("/drinks/<int:identifier>", methods=["DELETE"]) @PriceListPlugin.blueprint.route("/drinks/<int:identifier>", methods=["DELETE"])
def delete_drink(identifier): def delete_drink(identifier):
pricelist_controller.delete_drink(identifier) pricelist_controller.delete_drink(identifier)
return "", NO_CONTENT return no_content()
@pricelist_bp.route("/prices/<int:identifier>", methods=["DELETE"]) @PriceListPlugin.blueprint.route("/prices/<int:identifier>", methods=["DELETE"])
def delete_price(identifier): def delete_price(identifier):
pricelist_controller.delete_price(identifier) pricelist_controller.delete_price(identifier)
return "", NO_CONTENT return no_content()
@pricelist_bp.route("/volumes/<int:identifier>", methods=["DELETE"]) @PriceListPlugin.blueprint.route("/volumes/<int:identifier>", methods=["DELETE"])
def delete_volume(identifier): def delete_volume(identifier):
pricelist_controller.delete_volume(identifier) pricelist_controller.delete_volume(identifier)
return "", NO_CONTENT return no_content()
@pricelist_bp.route("/ingredients/extraIngredients", methods=["GET"]) @PriceListPlugin.blueprint.route("/ingredients/extraIngredients", methods=["GET"])
def get_extraIngredients(): def get_extra_ingredients():
return jsonify(pricelist_controller.get_extra_ingredients()) return jsonify(pricelist_controller.get_extra_ingredients())
@pricelist_bp.route("/ingredients/<int:identifier>", methods=["DELETE"]) @PriceListPlugin.blueprint.route("/ingredients/<int:identifier>", methods=["DELETE"])
def delete_ingredient(identifier): def delete_ingredient(identifier):
pricelist_controller.delete_ingredient(identifier) pricelist_controller.delete_ingredient(identifier)
return "", NO_CONTENT return no_content()
@pricelist_bp.route("/ingredients/extraIngredients", methods=["POST"]) @PriceListPlugin.blueprint.route("/ingredients/extraIngredients", methods=["POST"])
def set_extra_ingredient(): def set_extra_ingredient():
data = request.get_json() data = request.get_json()
return jsonify(pricelist_controller.set_extra_ingredient(data)) return jsonify(pricelist_controller.set_extra_ingredient(data))
@pricelist_bp.route("/ingredients/extraIngredients/<int:identifier>", methods=["PUT"]) @PriceListPlugin.blueprint.route("/ingredients/extraIngredients/<int:identifier>", methods=["PUT"])
def update_extra_ingredient(identifier): def update_extra_ingredient(identifier):
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))
@pricelist_bp.route("/ingredients/extraIngredients/<int:identifier>", methods=["DELETE"]) @PriceListPlugin.blueprint.route("/ingredients/extraIngredients/<int:identifier>", methods=["DELETE"])
def delete_extra_ingredient(identifier): def delete_extra_ingredient(identifier):
pricelist_controller.delete_extra_ingredient(identifier) pricelist_controller.delete_extra_ingredient(identifier)
return "", NO_CONTENT return no_content()
@pricelist_bp.route("/settings/min_prices", methods=["POST", "GET"]) @PriceListPlugin.blueprint.route("/settings/min_prices", methods=["POST", "GET"])
def pricelist_settings_min_prices(): def pricelist_settings_min_prices():
if request.method == "GET": if request.method == "GET":
return jsonify(PriceListPlugin.get_setting(PriceListPlugin, "min_prices")) # TODO: Handle if no prices are set!
return jsonify(PriceListPlugin.plugin.get_setting("min_prices"))
else: else:
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
data.sort() data.sort()
PriceListPlugin.set_setting(PriceListPlugin, "min_prices", data) PriceListPlugin.plugin.set_setting("min_prices", data)
return no_content() return no_content()
@pricelist_bp.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: Session):
"""Get pricecalc_columns of an user """Get pricecalc_columns of an user
@ -225,7 +227,7 @@ def get_columns(userid, current_session: Session):
userController.persist() userController.persist()
return no_content() return no_content()
@pricelist_bp.route("/drinks/<int:identifier>/picture", methods=["POST", "GET", "DELETE"]) @PriceListPlugin.route("/drinks/<int:identifier>/picture", methods=["POST", "GET", "DELETE"])
def set_picture(identifier): def set_picture(identifier):
if request.method == "DELETE": if request.method == "DELETE":
@ -241,10 +243,10 @@ def set_picture(identifier):
else: else:
raise BadRequest raise BadRequest
@pricelist_bp.route("/picture/<identifier>", methods=["GET"]) @PriceListPlugin.route("/picture/<identifier>", methods=["GET"])
def _get_picture(identifier): def _get_picture(identifier):
if request.method == "GET": if request.method == "GET":
size = request.args.get("size") size = request.args.get("size")
path = config["pricelist"]["path"] path = PriceListPlugin.plugin["path"]
response = pricelist_controller.get_drink_picture(identifier, size) response = pricelist_controller.get_drink_picture(identifier, size)
return response.make_conditional(request) return response.make_conditional(request)

View File

@ -3,7 +3,7 @@ from __future__ import annotations # TODO: Remove if python requirement is >= 3
from flaschengeist.database import db from flaschengeist.database import db
from flaschengeist.models import ModelSerializeMixin from flaschengeist.models import ModelSerializeMixin
from typing import Optional, Union from typing import Optional
drink_tag_association = db.Table( drink_tag_association = db.Table(
"drink_x_tag", "drink_x_tag",
@ -71,7 +71,7 @@ class DrinkIngredient(db.Model, ModelSerializeMixin):
__tablename__ = "drink_ingredient" __tablename__ = "drink_ingredient"
id: int = db.Column("id", db.Integer, primary_key=True) id: int = db.Column("id", db.Integer, primary_key=True)
volume: float = db.Column(db.Numeric(precision=5, scale=2, asdecimal=False), nullable=False) volume: float = db.Column(db.Numeric(precision=5, scale=2, asdecimal=False), nullable=False)
drink_ingredient_id: int = db.Column("drink_ingredient_id", db.Integer, db.ForeignKey("drink.id")) ingredient_id: int = db.Column(db.Integer, db.ForeignKey("drink.id"))
# drink_ingredient: Drink = db.relationship("Drink") # drink_ingredient: Drink = db.relationship("Drink")
# price: float = 0 # price: float = 0
@ -92,11 +92,12 @@ class Ingredient(db.Model, ModelSerializeMixin):
__tablename__ = "ingredient_association" __tablename__ = "ingredient_association"
id: int = db.Column("id", db.Integer, primary_key=True) id: int = db.Column("id", db.Integer, primary_key=True)
volume_id = db.Column(db.Integer, db.ForeignKey("drink_price_volume.id")) volume_id = db.Column(db.Integer, db.ForeignKey("drink_price_volume.id"))
drink_ingredient_id = db.Column(db.Integer, db.ForeignKey("drink_ingredient.id"))
drink_ingredient: Optional[DrinkIngredient] = db.relationship(DrinkIngredient) drink_ingredient: Optional[DrinkIngredient] = db.relationship(DrinkIngredient)
extra_ingredient_id = db.Column(db.Integer, db.ForeignKey("extra_ingredient.id"))
extra_ingredient: Optional[ExtraIngredient] = db.relationship(ExtraIngredient) extra_ingredient: Optional[ExtraIngredient] = db.relationship(ExtraIngredient)
_drink_ingredient_id = db.Column(db.Integer, db.ForeignKey("drink_ingredient.id"))
_extra_ingredient_id = db.Column(db.Integer, db.ForeignKey("extra_ingredient.id"))
class MinPrices(ModelSerializeMixin): class MinPrices(ModelSerializeMixin):
""" """
@ -134,8 +135,8 @@ class Drink(db.Model, ModelSerializeMixin):
package_size: Optional[int] = db.Column(db.Integer) package_size: Optional[int] = db.Column(db.Integer)
name: str = db.Column(db.String(60), nullable=False) name: str = db.Column(db.String(60), nullable=False)
volume: Optional[float] = db.Column(db.Numeric(precision=5, scale=2, asdecimal=False)) volume: Optional[float] = db.Column(db.Numeric(precision=5, scale=2, asdecimal=False))
cost_price_pro_volume: Optional[float] = db.Column(db.Numeric(precision=5, scale=3, asdecimal=False)) cost_per_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)) cost_per_package: Optional[float] = db.Column(db.Numeric(precision=5, scale=3, asdecimal=False))
uuid: str = db.Column(db.String(36)) uuid: str = db.Column(db.String(36))
receipt: Optional[str] = db.Column(db.String) receipt: Optional[str] = db.Column(db.String)

View File

@ -1,15 +1,14 @@
from werkzeug.exceptions import BadRequest, NotFound from werkzeug.exceptions import BadRequest, NotFound
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
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 .models import Drink, DrinkPrice, Ingredient, Tag, DrinkType, DrinkPriceVolume, DrinkIngredient, ExtraIngredient from .models import Drink, DrinkPrice, Ingredient, Tag, DrinkType, DrinkPriceVolume, DrinkIngredient, ExtraIngredient
from flaschengeist.utils.picture import save_picture, get_picture, delete_picture
from uuid import uuid4
def update(): def update():
db.session.commit() db.session.commit()
@ -21,15 +20,15 @@ def get_tags():
def get_tag(identifier): def get_tag(identifier):
if isinstance(identifier, int): if isinstance(identifier, int):
retVal = Tag.query.get(identifier) ret = Tag.query.get(identifier)
elif isinstance(identifier, str): elif isinstance(identifier, str):
retVal = Tag.query.filter(Tag.name == identifier).one_or_none() ret = Tag.query.filter(Tag.name == identifier).one_or_none()
else: else:
logger.debug("Invalid identifier type for Tag") logger.debug("Invalid identifier type for Tag")
raise BadRequest raise BadRequest
if not retVal: if ret is None:
raise NotFound raise NotFound
return retVal return ret
def create_tag(name): def create_tag(name):
@ -66,23 +65,23 @@ def get_drink_types():
def get_drink_type(identifier): def get_drink_type(identifier):
if isinstance(identifier, int): if isinstance(identifier, int):
retVal = DrinkType.query.get(identifier) ret = DrinkType.query.get(identifier)
elif isinstance(identifier, str): elif isinstance(identifier, str):
retVal = DrinkType.query.filter(Tag.name == identifier).one_or_none() ret = DrinkType.query.filter(Tag.name == identifier).one_or_none()
else: else:
logger.debug("Invalid identifier type for DrinkType") logger.debug("Invalid identifier type for DrinkType")
raise BadRequest raise BadRequest
if not retVal: if ret is None:
raise NotFound raise NotFound
return retVal return ret
def create_drink_type(name): def create_drink_type(name):
try: try:
drinkType = DrinkType(name=name) drink_type = DrinkType(name=name)
db.session.add(drinkType) db.session.add(drink_type)
update() update()
return drinkType return drink_type
except IntegrityError: except IntegrityError:
raise BadRequest("Name already exists") raise BadRequest("Name already exists")
@ -98,8 +97,8 @@ def rename_drink_type(identifier, new_name):
def delete_drink_type(identifier): def delete_drink_type(identifier):
drinkType = get_drink_type(identifier) drink_type = get_drink_type(identifier)
db.session.delete(drinkType) db.session.delete(drink_type)
try: try:
update() update()
except IntegrityError: except IntegrityError:
@ -137,13 +136,12 @@ def get_drink(identifier, public=False):
elif isinstance(identifier, str): elif isinstance(identifier, str):
drink = Drink.query.filter(Tag.name == identifier).one_or_none() drink = Drink.query.filter(Tag.name == identifier).one_or_none()
else: else:
logger.debug("Invalid identifier type for Drink") raise BadRequest("Invalid identifier type for Drink")
raise BadRequest if drink is None:
if drink: raise NotFound
if public: if public:
return _create_public_drink(drink) return _create_public_drink(drink)
return drink return drink
raise NotFound
def set_drink(data): def set_drink(data):
@ -151,44 +149,40 @@ def set_drink(data):
def update_drink(identifier, data): def update_drink(identifier, data):
allowedKeys = Drink().serialize().keys() try:
if "id" in data: if "id" in data:
data.pop("id") data.pop("id")
if "volumes" in data: volumes = data.pop("volumes") if "volumes" in data else None
volumes = data.pop("volumes") if "tags" in data:
if "tags" in data: data.pop("tags")
data.pop("tags") drink_type = data.pop("type")
type = None if isinstance(drink_type, dict) and "id" in drink_type:
if "type" in data: drink_type = drink_type["id"]
_type = data.pop("type") drink_type = get_drink_type(drink_type)
if isinstance(_type, dict) and "id" in _type: if identifier == -1:
type = get_drink_type(_type.get("id")) drink = Drink()
if identifier == -1: db.session.add(drink)
drink = Drink() else:
db.session.add(drink) drink = get_drink(identifier)
else: for key, value in data.items():
drink = get_drink(identifier) if hasattr(drink, key):
if not drink: setattr(drink, key, value if value != "" else None)
raise NotFound
for key, value in data.items():
if hasattr(drink, key):
setattr(drink, key, value if value != "" else None)
if type: if drink_type:
drink.type = type drink.type = drink_type
if volumes: if volumes is not None:
set_volumes(volumes, drink) set_volumes(volumes, drink)
db.session.commit() db.session.commit()
return drink return drink
except (NotFound, KeyError):
raise BadRequest
def set_volumes(volumes, drink): def set_volumes(volumes, drink):
if isinstance(volumes, list): if not isinstance(volumes, list):
_volumes = [] raise BadRequest
for _volume in volumes: for volume in volumes:
volume = set_volume(_volume) drink.volumes.append(set_volume(volume))
_volumes.append(volume)
drink.volumes = _volumes
def delete_drink(identifier): def delete_drink(identifier):
@ -216,15 +210,12 @@ def set_volume(data):
prices = values.pop("prices") prices = values.pop("prices")
if "ingredients" in values: if "ingredients" in values:
ingredients = values.pop("ingredients") ingredients = values.pop("ingredients")
id = None vol_id = values.pop("id", None)
if "id" in values: if vol_id < 0:
id = values.pop("id")
volume = None
if id < 0:
volume = DrinkPriceVolume(**values) volume = DrinkPriceVolume(**values)
db.session.add(volume) db.session.add(volume)
else: else:
volume = get_volume(id) volume = get_volume(vol_id)
if not volume: if not volume:
raise NotFound raise NotFound
for key, value in values.items(): for key, value in values.items():
@ -276,15 +267,12 @@ def get_prices(volume_id=None):
def set_price(data): def set_price(data):
allowed_keys = DrinkPrice().serialize().keys() allowed_keys = DrinkPrice().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}
id = None price_id = values.pop("id", -1)
if "id" in values: if price_id < 0:
id = values.pop("id")
price = None
if id < 0:
price = DrinkPrice(**values) price = DrinkPrice(**values)
db.session.add(price) db.session.add(price)
else: else:
price = get_price(id) price = get_price(price_id)
if not price: if not price:
raise NotFound raise NotFound
for key, value in values.items(): for key, value in values.items():
@ -300,17 +288,14 @@ def delete_price(identifier):
def set_drink_ingredient(data): def set_drink_ingredient(data):
allowedKeys = DrinkIngredient().serialize().keys() allowed_keys = DrinkIngredient().serialize().keys()
drink = None 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 allowedKeys} ingredient_id = values.pop("id", -1)
id = None if ingredient_id < 0:
if "id" in values:
id = values.pop("id")
if id < 0:
drink_ingredient = DrinkIngredient(**values) drink_ingredient = DrinkIngredient(**values)
db.session.add(drink_ingredient) db.session.add(drink_ingredient)
else: else:
drink_ingredient = DrinkIngredient.query.get(id) drink_ingredient = DrinkIngredient.query.get(ingredient_id)
if not drink_ingredient: if not drink_ingredient:
raise NotFound raise NotFound
for key, value in values.items(): for key, value in values.items():
@ -329,15 +314,12 @@ def set_ingredient(data):
drink_ingredient_value = data.pop("drink_ingredient") drink_ingredient_value = data.pop("drink_ingredient")
if "extra_ingredient" in data: if "extra_ingredient" in data:
extra_ingredient_value = data.pop("extra_ingredient") extra_ingredient_value = data.pop("extra_ingredient")
id = None ingredient_id = data.pop("id", -1)
if "id" in data: if ingredient_id < 0:
id = data.pop("id")
ingredient = None
if id < 0:
ingredient = Ingredient(**data) ingredient = Ingredient(**data)
db.session.add(ingredient) db.session.add(ingredient)
else: else:
ingredient = get_ingredient(id) ingredient = get_ingredient(ingredient_id)
if not ingredient: if not ingredient:
raise NotFound raise NotFound
if drink_ingredient_value: if drink_ingredient_value:
@ -365,10 +347,10 @@ def get_extra_ingredient(identifier):
def set_extra_ingredient(data): def set_extra_ingredient(data):
allowedKeys = ExtraIngredient().serialize().keys() allowed_keys = ExtraIngredient().serialize().keys()
if "id" in data: if "id" in data:
data.pop("id") data.pop("id")
values = {key: value for key, value in data.items() if key in allowedKeys} values = {key: value for key, value in data.items() if key in allowed_keys}
extra_ingredient = ExtraIngredient(**values) extra_ingredient = ExtraIngredient(**values)
db.session.add(extra_ingredient) db.session.add(extra_ingredient)
db.session.commit() db.session.commit()
@ -376,10 +358,10 @@ def set_extra_ingredient(data):
def update_extra_ingredient(identifier, data): def update_extra_ingredient(identifier, data):
allowedKeys = ExtraIngredient().serialize().keys() allowed_keys = ExtraIngredient().serialize().keys()
if "id" in data: if "id" in data:
data.pop("id") data.pop("id")
values = {key: value for key, value in data.items() if key in allowedKeys} values = {key: value for key, value in data.items() if key in allowed_keys}
extra_ingredient = get_extra_ingredient(identifier) extra_ingredient = get_extra_ingredient(identifier)
if extra_ingredient: if extra_ingredient:
for key, value in values.items(): for key, value in values.items():

View File

@ -12,17 +12,17 @@ from flaschengeist.utils.decorators import login_required
from flaschengeist.controller import roleController from flaschengeist.controller import roleController
from flaschengeist.utils.HTTP import created from flaschengeist.utils.HTTP import created
roles_bp = Blueprint("roles", __name__)
_permission_edit = "roles_edit" _permission_edit = "roles_edit"
_permission_delete = "roles_delete" _permission_delete = "roles_delete"
class RolesPlugin(Plugin): class RolesPlugin(Plugin):
def __init__(self, config): name = "roles"
super().__init__(config, roles_bp, permissions=[_permission_edit, _permission_delete]) blueprint = Blueprint(name, __name__)
permissions = [_permission_edit, _permission_delete]
@roles_bp.route("/roles", methods=["GET"]) @RolesPlugin.blueprint.route("/roles", methods=["GET"])
@login_required() @login_required()
def list_roles(current_session): def list_roles(current_session):
"""List all existing roles """List all existing roles
@ -39,7 +39,7 @@ def list_roles(current_session):
return jsonify(roles) return jsonify(roles)
@roles_bp.route("/roles", methods=["POST"]) @RolesPlugin.blueprint.route("/roles", methods=["POST"])
@login_required(permission=_permission_edit) @login_required(permission=_permission_edit)
def create_role(current_session): def create_role(current_session):
"""Create new role """Create new role
@ -62,7 +62,7 @@ def create_role(current_session):
return created(roleController.create_role(data["name"], permissions)) return created(roleController.create_role(data["name"], permissions))
@roles_bp.route("/roles/permissions", methods=["GET"]) @RolesPlugin.blueprint.route("/roles/permissions", methods=["GET"])
@login_required() @login_required()
def list_permissions(current_session): def list_permissions(current_session):
"""List all existing permissions """List all existing permissions
@ -79,7 +79,7 @@ def list_permissions(current_session):
return jsonify(permissions) return jsonify(permissions)
@roles_bp.route("/roles/<role_name>", methods=["GET"]) @RolesPlugin.blueprint.route("/roles/<role_name>", methods=["GET"])
@login_required() @login_required()
def get_role(role_name, current_session): def get_role(role_name, current_session):
"""Get role by name """Get role by name
@ -97,7 +97,7 @@ def get_role(role_name, current_session):
return jsonify(role) return jsonify(role)
@roles_bp.route("/roles/<int:role_id>", methods=["PUT"]) @RolesPlugin.blueprint.route("/roles/<int:role_id>", methods=["PUT"])
@login_required(permission=_permission_edit) @login_required(permission=_permission_edit)
def edit_role(role_id, current_session): def edit_role(role_id, current_session):
"""Edit role, rename and / or set permissions """Edit role, rename and / or set permissions
@ -123,7 +123,7 @@ def edit_role(role_id, current_session):
return "", NO_CONTENT return "", NO_CONTENT
@roles_bp.route("/roles/<int:role_id>", methods=["DELETE"]) @RolesPlugin.blueprint.route("/roles/<int:role_id>", methods=["DELETE"])
@login_required(permission=_permission_delete) @login_required(permission=_permission_delete)
def delete_role(role_id, current_session): def delete_role(role_id, current_session):
"""Delete role """Delete role

View File

@ -13,18 +13,17 @@ from flaschengeist.plugins import Plugin
from flaschengeist.models.user import User, _Avatar from flaschengeist.models.user import User, _Avatar
from flaschengeist.utils.decorators import login_required, extract_session, headers from flaschengeist.utils.decorators import login_required, extract_session, headers
from flaschengeist.controller import userController from flaschengeist.controller import userController
from flaschengeist.utils.HTTP import created from flaschengeist.utils.HTTP import created, no_content
from flaschengeist.utils.datetime import from_iso_format from flaschengeist.utils.datetime import from_iso_format
users_bp = Blueprint("users", __name__)
class UsersPlugin(Plugin): class UsersPlugin(Plugin):
def __init__(self, cfg): name = "users"
super().__init__(blueprint=users_bp, permissions=permissions.permissions) blueprint = Blueprint(name, __name__)
permissions = permissions.permissions
@users_bp.route("/users", methods=["POST"]) @UsersPlugin.blueprint.route("/users", methods=["POST"])
def register(): def register():
"""Register a new user """Register a new user
The password will be set to a random string of at lease 16byte entropy. The password will be set to a random string of at lease 16byte entropy.
@ -55,7 +54,7 @@ def register():
return make_response(jsonify(userController.register(data)), CREATED) return make_response(jsonify(userController.register(data)), CREATED)
@users_bp.route("/users", methods=["GET"]) @UsersPlugin.blueprint.route("/users", methods=["GET"])
@login_required() @login_required()
@headers({"Cache-Control": "private, must-revalidate, max-age=3600"}) @headers({"Cache-Control": "private, must-revalidate, max-age=3600"})
def list_users(current_session): def list_users(current_session):
@ -74,7 +73,7 @@ def list_users(current_session):
return jsonify(users) return jsonify(users)
@users_bp.route("/users/<userid>", methods=["GET"]) @UsersPlugin.blueprint.route("/users/<userid>", methods=["GET"])
@login_required() @login_required()
@headers({"Cache-Control": "private, must-revalidate, max-age=3600"}) @headers({"Cache-Control": "private, must-revalidate, max-age=3600"})
def get_user(userid, current_session): def get_user(userid, current_session):
@ -97,7 +96,25 @@ def get_user(userid, current_session):
return jsonify(serial) return jsonify(serial)
@users_bp.route("/users/<userid>/avatar", methods=["GET"]) @UsersPlugin.blueprint.route("/users/<userid>/frontend", methods=["POST", "GET"])
@login_required()
def frontend(userid, current_session):
if current_session.user_.userid != userid:
raise Forbidden
if request.method == "POST":
if request.content_length > 1024 ** 2:
raise BadRequest
current_session.user_.set_attribute("frontend", request.get_json())
return no_content()
else:
content = current_session.user_.get_attribute("frontend", None)
if content is None:
return no_content()
return jsonify(content)
@UsersPlugin.blueprint.route("/users/<userid>/avatar", methods=["GET"])
@headers({"Cache-Control": "public, max-age=604800"}) @headers({"Cache-Control": "public, max-age=604800"})
def get_avatar(userid): def get_avatar(userid):
user = userController.get_user(userid) user = userController.get_user(userid)
@ -109,7 +126,7 @@ def get_avatar(userid):
raise NotFound raise NotFound
@users_bp.route("/users/<userid>/avatar", methods=["POST"]) @UsersPlugin.blueprint.route("/users/<userid>/avatar", methods=["POST"])
@login_required() @login_required()
def set_avatar(userid, current_session): def set_avatar(userid, current_session):
user = userController.get_user(userid) user = userController.get_user(userid)
@ -127,7 +144,7 @@ def set_avatar(userid, current_session):
raise BadRequest raise BadRequest
@users_bp.route("/users/<userid>", methods=["DELETE"]) @UsersPlugin.blueprint.route("/users/<userid>", methods=["DELETE"])
@login_required(permission=permissions.DELETE) @login_required(permission=permissions.DELETE)
def delete_user(userid, current_session): def delete_user(userid, current_session):
"""Delete user by userid """Delete user by userid
@ -147,7 +164,7 @@ def delete_user(userid, current_session):
return "", NO_CONTENT return "", NO_CONTENT
@users_bp.route("/users/<userid>", methods=["PUT"]) @UsersPlugin.blueprint.route("/users/<userid>", methods=["PUT"])
@login_required() @login_required()
def edit_user(userid, current_session): def edit_user(userid, current_session):
"""Modify user by userid """Modify user by userid
@ -198,3 +215,19 @@ def edit_user(userid, current_session):
userController.modify_user(user, password, new_password) userController.modify_user(user, password, new_password)
userController.update_user(user) userController.update_user(user)
return "", NO_CONTENT return "", NO_CONTENT
@UsersPlugin.blueprint.route("/notifications", methods=["GET"])
@login_required()
def notifications(current_session):
f = request.args.get("from", None)
if f is not None:
f = from_iso_format(f)
return jsonify(userController.get_notifications(current_session.user_, f))
@UsersPlugin.blueprint.route("/notifications/<nid>", methods=["DELETE"])
@login_required()
def remove_notifications(nid, current_session):
userController.delete_notification(nid, current_session.user_)
return no_content()

View File

@ -0,0 +1,17 @@
from flask import current_app
from flaschengeist.utils.HTTP import no_content
_scheduled = set()
def scheduled(func):
_scheduled.add(func)
return func
@current_app.route("/cron")
def __run_scheduled():
for function in _scheduled:
function()
return no_content()

View File

@ -160,7 +160,7 @@ def export(arguments):
if arguments.plugins: if arguments.plugins:
for entry_point in pkg_resources.iter_entry_points("flaschengeist.plugin"): for entry_point in pkg_resources.iter_entry_points("flaschengeist.plugin"):
plg = entry_point.load() plg = entry_point.load()
if hasattr(plg, "models"): if hasattr(plg, "models") and plg.models is not None:
gen.run(plg.models) gen.run(plg.models)
gen.write() gen.write()

View File

@ -6,7 +6,7 @@ mysql_driver = "PyMySQL" if os.name == "nt" else "mysqlclient"
class DocsCommand(Command): class DocsCommand(Command):
description = "Generate and export API documentation" description = "Generate and export API documentation using pdoc3"
user_options = [ user_options = [
# The format is (long option, short option, description). # The format is (long option, short option, description).
("output=", "o", "Documentation output path"), ("output=", "o", "Documentation output path"),