[core][plugin] Added Notifications, restructure plugins
This commit is contained in:
		
							parent
							
								
									544ae6a3fe
								
							
						
					
					
						commit
						775e775e89
					
				|  | @ -45,9 +45,10 @@ def __load_plugins(app): | |||
|             try: | ||||
|                 logger.info(f"Load plugin {entry_point.name}") | ||||
|                 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]) | ||||
|                 if plugin.blueprint: | ||||
|                 if hasattr(plugin, "blueprint") and plugin.blueprint is not None: | ||||
|                     app.register_blueprint(plugin.blueprint) | ||||
|             except: | ||||
|                 logger.error( | ||||
|  |  | |||
|  | @ -6,6 +6,7 @@ from werkzeug.exceptions import NotFound, BadRequest, Forbidden | |||
| from flaschengeist import logger | ||||
| from flaschengeist.config import config | ||||
| from flaschengeist.database import db | ||||
| from flaschengeist.models.notification import Notification | ||||
| from flaschengeist.utils.hook import Hook | ||||
| from flaschengeist.utils.datetime import from_iso_format | ||||
| from flaschengeist.models.user import User, Role, _PasswordReset | ||||
|  | @ -210,3 +211,15 @@ def persist(user=None): | |||
|     if user: | ||||
|         db.session.add(user) | ||||
|     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() | ||||
|  |  | |||
|  | @ -1,6 +1,8 @@ | |||
| import sys | ||||
| import datetime | ||||
| 
 | ||||
| from sqlalchemy import BigInteger | ||||
| from sqlalchemy.dialects import mysql | ||||
| from sqlalchemy.types import DateTime, TypeDecorator | ||||
| 
 | ||||
| 
 | ||||
|  | @ -39,6 +41,12 @@ class ModelSerializeMixin: | |||
|         return d | ||||
| 
 | ||||
| 
 | ||||
| class Serial(TypeDecorator): | ||||
|     """Same as MariaDB Serial used for IDs""" | ||||
| 
 | ||||
|     impl = BigInteger().with_variant(mysql.BIGINT(unsigned=True), "mysql") | ||||
| 
 | ||||
| 
 | ||||
| class UtcDateTime(TypeDecorator): | ||||
|     """Almost equivalent to `sqlalchemy.types.DateTime` with | ||||
|     ``timezone=True`` option, but it differs from that by: | ||||
|  |  | |||
|  | @ -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") | ||||
|  | @ -2,7 +2,7 @@ from __future__ import annotations  # TODO: Remove if python requirement is >= 3 | |||
| 
 | ||||
| from datetime import datetime, timedelta, timezone | ||||
| 
 | ||||
| from . import ModelSerializeMixin, UtcDateTime | ||||
| from . import ModelSerializeMixin, UtcDateTime, Serial | ||||
| from .user import User | ||||
| from flaschengeist.database import db | ||||
| from secrets import compare_digest | ||||
|  | @ -26,8 +26,8 @@ class Session(db.Model, ModelSerializeMixin): | |||
|     platform: str = db.Column(db.String(30)) | ||||
|     userid: str = "" | ||||
| 
 | ||||
|     _id = db.Column("id", db.Integer, primary_key=True) | ||||
|     _user_id = db.Column("user_id", db.Integer, db.ForeignKey("user.id")) | ||||
|     _id = db.Column("id", Serial, primary_key=True) | ||||
|     _user_id = db.Column("user_id", Serial, db.ForeignKey("user.id")) | ||||
|     user_: User = db.relationship("User", back_populates="sessions_") | ||||
| 
 | ||||
|     @property | ||||
|  |  | |||
|  | @ -1,11 +1,13 @@ | |||
| from __future__ import annotations  # TODO: Remove if python requirement is >= 3.10 | ||||
| from typing import Any | ||||
| 
 | ||||
| from . import Serial | ||||
| from ..database import db | ||||
| 
 | ||||
| 
 | ||||
| class _PluginSetting(db.Model): | ||||
|     __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)) | ||||
|     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)) | ||||
|  |  | |||
|  | @ -6,19 +6,19 @@ from datetime import date, datetime | |||
| from sqlalchemy.orm.collections import attribute_mapped_collection | ||||
| 
 | ||||
| from ..database import db | ||||
| from . import ModelSerializeMixin, UtcDateTime | ||||
| from . import ModelSerializeMixin, UtcDateTime, Serial | ||||
| 
 | ||||
| 
 | ||||
| association_table = db.Table( | ||||
|     "user_x_role", | ||||
|     db.Column("user_id", db.Integer, db.ForeignKey("user.id")), | ||||
|     db.Column("role_id", db.Integer, db.ForeignKey("role.id")), | ||||
|     db.Column("user_id", Serial, db.ForeignKey("user.id")), | ||||
|     db.Column("role_id", Serial, db.ForeignKey("role.id")), | ||||
| ) | ||||
| 
 | ||||
| role_permission_association_table = db.Table( | ||||
|     "role_x_permission", | ||||
|     db.Column("role_id", db.Integer, db.ForeignKey("role.id")), | ||||
|     db.Column("permission_id", db.Integer, db.ForeignKey("permission.id")), | ||||
|     db.Column("role_id", Serial, db.ForeignKey("role.id")), | ||||
|     db.Column("permission_id", Serial, db.ForeignKey("permission.id")), | ||||
| ) | ||||
| 
 | ||||
| 
 | ||||
|  | @ -26,12 +26,12 @@ class Permission(db.Model, ModelSerializeMixin): | |||
|     __tablename__ = "permission" | ||||
|     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): | ||||
|     __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) | ||||
|     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 | ||||
|     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") | ||||
|     sessions_ = db.relationship("Session", back_populates="user_") | ||||
| 
 | ||||
|  | @ -101,8 +101,8 @@ class User(db.Model, ModelSerializeMixin): | |||
| 
 | ||||
| class _UserAttribute(db.Model, ModelSerializeMixin): | ||||
|     __tablename__ = "user_attribute" | ||||
|     id = db.Column("id", db.Integer, primary_key=True) | ||||
|     user: User = db.Column("user", db.Integer, db.ForeignKey("user.id"), nullable=False) | ||||
|     id = db.Column("id", Serial, primary_key=True) | ||||
|     user: User = db.Column("user", Serial, db.ForeignKey("user.id"), nullable=False) | ||||
|     name: str = db.Column(db.String(30)) | ||||
|     value: any = db.Column(db.PickleType(protocol=4)) | ||||
| 
 | ||||
|  | @ -111,7 +111,7 @@ class _PasswordReset(db.Model): | |||
|     """Table containing password reset requests""" | ||||
| 
 | ||||
|     __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]) | ||||
|     token: str = db.Column(db.String(32)) | ||||
|     expires: datetime = db.Column(UtcDateTime) | ||||
|  |  | |||
|  | @ -3,6 +3,7 @@ import pkg_resources | |||
| from werkzeug.exceptions import MethodNotAllowed, NotFound | ||||
| 
 | ||||
| from flaschengeist.database import db | ||||
| from flaschengeist.models.notification import Notification | ||||
| from flaschengeist.models.user import _Avatar | ||||
| from flaschengeist.models.setting import _PluginSetting | ||||
| from flaschengeist.utils.hook import HookBefore, HookAfter | ||||
|  | @ -30,15 +31,16 @@ class Plugin: | |||
|     """Base class for all Plugins | ||||
|     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 | ||||
|         Args: | ||||
|             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 | ||||
| 
 | ||||
|     def install(self): | ||||
|  | @ -63,7 +65,7 @@ class Plugin: | |||
|         """ | ||||
|         try: | ||||
|             setting = ( | ||||
|                 _PluginSetting.query.filter(_PluginSetting.plugin == self._plugin_name) | ||||
|                 _PluginSetting.query.filter(_PluginSetting.plugin == self.name) | ||||
|                 .filter(_PluginSetting.name == name) | ||||
|                 .one() | ||||
|             ) | ||||
|  | @ -81,14 +83,19 @@ class Plugin: | |||
|             value: Value to be stored | ||||
|         """ | ||||
|         setting = ( | ||||
|             _PluginSetting.query.filter(_PluginSetting.plugin == self._plugin_name) | ||||
|             _PluginSetting.query.filter(_PluginSetting.plugin == self.name) | ||||
|             .filter(_PluginSetting.name == name) | ||||
|             .one_or_none() | ||||
|         ) | ||||
|         if setting is not None: | ||||
|             setting.value = value | ||||
|         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() | ||||
| 
 | ||||
|     def serialize(self): | ||||
|  |  | |||
|  | @ -11,15 +11,13 @@ from flaschengeist.utils.HTTP import no_content, created | |||
| from flaschengeist.utils.decorators import login_required | ||||
| from flaschengeist.controller import sessionController, userController | ||||
| 
 | ||||
| auth_bp = Blueprint("auth", __name__) | ||||
| 
 | ||||
| 
 | ||||
| class AuthRoutePlugin(Plugin): | ||||
|     def __init__(self, conf): | ||||
|         super().__init__(blueprint=auth_bp) | ||||
|     name = "auth" | ||||
|     blueprint = Blueprint(name, __name__) | ||||
| 
 | ||||
| 
 | ||||
| @auth_bp.route("/auth", methods=["POST"]) | ||||
| @AuthRoutePlugin.blueprint.route("/auth", methods=["POST"]) | ||||
| def login(): | ||||
|     """Login in an user and create a session | ||||
| 
 | ||||
|  | @ -52,7 +50,7 @@ def login(): | |||
|     return created(session) | ||||
| 
 | ||||
| 
 | ||||
| @auth_bp.route("/auth", methods=["GET"]) | ||||
| @AuthRoutePlugin.blueprint.route("/auth", methods=["GET"]) | ||||
| @login_required() | ||||
| def get_sessions(current_session, **kwargs): | ||||
|     """Get all valid sessions of current user | ||||
|  | @ -66,7 +64,7 @@ def get_sessions(current_session, **kwargs): | |||
|     return jsonify(sessions) | ||||
| 
 | ||||
| 
 | ||||
| @auth_bp.route("/auth/<token>", methods=["DELETE"]) | ||||
| @AuthRoutePlugin.blueprint.route("/auth/<token>", methods=["DELETE"]) | ||||
| @login_required() | ||||
| def delete_session(token, current_session, **kwargs): | ||||
|     """Delete a session aka "logout" | ||||
|  | @ -88,7 +86,7 @@ def delete_session(token, current_session, **kwargs): | |||
|     return "" | ||||
| 
 | ||||
| 
 | ||||
| @auth_bp.route("/auth/<token>", methods=["GET"]) | ||||
| @AuthRoutePlugin.blueprint.route("/auth/<token>", methods=["GET"]) | ||||
| @login_required() | ||||
| def get_session(token, current_session, **kwargs): | ||||
|     """Retrieve information about a session | ||||
|  | @ -111,7 +109,7 @@ def get_session(token, current_session, **kwargs): | |||
|     return jsonify(session) | ||||
| 
 | ||||
| 
 | ||||
| @auth_bp.route("/auth/<token>", methods=["PUT"]) | ||||
| @AuthRoutePlugin.blueprint.route("/auth/<token>", methods=["PUT"]) | ||||
| @login_required() | ||||
| def set_lifetime(token, current_session, **kwargs): | ||||
|     """Set lifetime of a session | ||||
|  | @ -141,7 +139,7 @@ def set_lifetime(token, current_session, **kwargs): | |||
|         raise BadRequest | ||||
| 
 | ||||
| 
 | ||||
| @auth_bp.route("/auth/<token>/user", methods=["GET"]) | ||||
| @AuthRoutePlugin.blueprint.route("/auth/<token>/user", methods=["GET"]) | ||||
| @login_required() | ||||
| def get_assocd_user(token, current_session, **kwargs): | ||||
|     """Retrieve user owning a session | ||||
|  | @ -164,7 +162,7 @@ def get_assocd_user(token, current_session, **kwargs): | |||
|     return jsonify(session.user_) | ||||
| 
 | ||||
| 
 | ||||
| @auth_bp.route("/auth/reset", methods=["POST"]) | ||||
| @AuthRoutePlugin.blueprint.route("/auth/reset", methods=["POST"]) | ||||
| def reset_password(): | ||||
|     data = request.get_json() | ||||
|     if "userid" in data: | ||||
|  |  | |||
|  | @ -3,30 +3,25 @@ | |||
| Extends users plugin with balance functions | ||||
| """ | ||||
| 
 | ||||
| from datetime import datetime, timezone | ||||
| 
 | ||||
| from flaschengeist.utils.HTTP import no_content | ||||
| from flask import Blueprint, request, jsonify | ||||
| from werkzeug.exceptions import Forbidden, BadRequest | ||||
| from werkzeug.local import LocalProxy | ||||
| from flask import Blueprint, current_app | ||||
| 
 | ||||
| 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 . import balance_controller, permissions, models | ||||
| 
 | ||||
| balance_bp = Blueprint("balance", __name__) | ||||
| from . import permissions, models | ||||
| 
 | ||||
| 
 | ||||
| class BalancePlugin(Plugin): | ||||
|     name = "balance" | ||||
|     blueprint = Blueprint(name, __name__) | ||||
|     permissions = permissions.permissions | ||||
|     plugin = LocalProxy(lambda: current_app.config["FG_PLUGINS"][BalancePlugin.name]) | ||||
|     models = models | ||||
| 
 | ||||
|     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 | ||||
|         def set_default_limit(user): | ||||
|  | @ -36,277 +31,3 @@ class BalancePlugin(Plugin): | |||
|                 balance_controller.set_limit(user, limit, override=False) | ||||
|             except KeyError: | ||||
|                 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()]) | ||||
|  |  | |||
|  | @ -11,7 +11,7 @@ from flaschengeist.database import db | |||
| from flaschengeist.models.user import User | ||||
| 
 | ||||
| from .models import Transaction | ||||
| from . import permissions | ||||
| from . import permissions, BalancePlugin | ||||
| 
 | ||||
| __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) | ||||
|     db.session.add(transaction) | ||||
|     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 | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -6,21 +6,21 @@ from sqlalchemy.ext.hybrid import hybrid_property | |||
| 
 | ||||
| from flaschengeist.database import db | ||||
| from flaschengeist.models.user import User | ||||
| from flaschengeist.models import ModelSerializeMixin, UtcDateTime | ||||
| from flaschengeist.models import ModelSerializeMixin, UtcDateTime, Serial | ||||
| 
 | ||||
| 
 | ||||
| class Transaction(db.Model, ModelSerializeMixin): | ||||
|     __tablename__ = "balance_transaction" | ||||
|     # Protected foreign key properties | ||||
|     _receiver_id = db.Column("receiver_id", db.Integer, db.ForeignKey("user.id")) | ||||
|     _sender_id = db.Column("sender_id", db.Integer, db.ForeignKey("user.id")) | ||||
|     _author_id = db.Column("author_id", db.Integer, db.ForeignKey("user.id"), nullable=False) | ||||
|     _receiver_id = db.Column("receiver_id", Serial, db.ForeignKey("user.id")) | ||||
|     _sender_id = db.Column("sender_id", Serial, db.ForeignKey("user.id")) | ||||
|     _author_id = db.Column("author_id", Serial, db.ForeignKey("user.id"), nullable=False) | ||||
| 
 | ||||
|     # 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) | ||||
|     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) | ||||
|     author_id: Optional[str] = None | ||||
|  |  | |||
|  | @ -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()]) | ||||
|  | @ -1,441 +1,21 @@ | |||
| """Schedule plugin | ||||
| """Events plugin | ||||
| 
 | ||||
| Provides duty schedule / duty roster functions | ||||
| """ | ||||
| from datetime import datetime, timedelta, timezone | ||||
| from http.client import NO_CONTENT | ||||
| from flask import Blueprint, request, jsonify | ||||
| from werkzeug.exceptions import BadRequest, NotFound, Forbidden | ||||
| from flask import Blueprint, current_app | ||||
| from werkzeug.local import LocalProxy | ||||
| 
 | ||||
| from flaschengeist.plugins import Plugin | ||||
| 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 | ||||
| from . import models | ||||
| from ...utils.HTTP import no_content | ||||
| 
 | ||||
| events_bp = Blueprint("events", __name__) | ||||
| from . import permissions, models | ||||
| 
 | ||||
| 
 | ||||
| class EventPlugin(Plugin): | ||||
|     name = "events" | ||||
|     plugin = LocalProxy(lambda: current_app.config["FG_PLUGINS"][EventPlugin.name]) | ||||
|     permissions = permissions.permissions | ||||
|     blueprint = Blueprint(name, __name__) | ||||
|     models = models | ||||
| 
 | ||||
|     def __init__(self, config): | ||||
|         super().__init__( | ||||
|             blueprint=events_bp, | ||||
|             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 | ||||
|     def __init__(self, cfg): | ||||
|         super(EventPlugin, self).__init__(cfg) | ||||
|         from . import routes | ||||
|  |  | |||
|  | @ -1,14 +1,14 @@ | |||
| from datetime import datetime | ||||
| from datetime import datetime, timedelta, timezone | ||||
| from typing import Optional | ||||
| 
 | ||||
| from sqlalchemy import or_, and_ | ||||
| from werkzeug.exceptions import BadRequest, NotFound | ||||
| from sqlalchemy.exc import IntegrityError | ||||
| 
 | ||||
| from flaschengeist import logger | ||||
| from flaschengeist.database import db | ||||
| from flaschengeist.plugins.events import EventPlugin | ||||
| 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(): | ||||
|  | @ -102,10 +102,21 @@ def delete_job_type(name): | |||
|         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) | ||||
|     if event is None: | ||||
|         raise NotFound | ||||
|     if not with_backup: | ||||
|         return clear_backup(event) | ||||
|     return event | ||||
| 
 | ||||
| 
 | ||||
|  | @ -113,11 +124,12 @@ def get_templates(): | |||
|     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 | ||||
|     Args: | ||||
|         start (datetime): Earliest start | ||||
|         end (datetime): Latest start | ||||
|         with_backup (bool): Export also backup services | ||||
| 
 | ||||
|     Returns: collection of Event objects | ||||
|     """ | ||||
|  | @ -126,7 +138,11 @@ def get_events(start: Optional[datetime] = None, end=None): | |||
|         query = query.filter(start <= Event.start) | ||||
|     if end is not None: | ||||
|         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): | ||||
|  | @ -202,3 +218,26 @@ def assign_to_job(job: Job, user, value): | |||
|             service = Service(user_=user, value=value, job_=job) | ||||
|             db.session.add(service) | ||||
|     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() | ||||
|  |  | |||
|  | @ -1,12 +1,11 @@ | |||
| from __future__ import annotations  # TODO: Remove if python requirement is >= 3.10 | ||||
| 
 | ||||
| import enum | ||||
| from datetime import datetime | ||||
| from typing import Optional, Union | ||||
| 
 | ||||
| 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.database import db | ||||
| 
 | ||||
|  | @ -19,13 +18,13 @@ _table_prefix_ = "events_" | |||
| 
 | ||||
| class EventType(db.Model, ModelSerializeMixin): | ||||
|     __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) | ||||
| 
 | ||||
| 
 | ||||
| class JobType(db.Model, ModelSerializeMixin): | ||||
|     __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) | ||||
| 
 | ||||
| 
 | ||||
|  | @ -37,12 +36,11 @@ class JobType(db.Model, ModelSerializeMixin): | |||
| class Service(db.Model, ModelSerializeMixin): | ||||
|     __tablename__ = _table_prefix_ + "service" | ||||
|     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) | ||||
| 
 | ||||
|     _job_id = db.Column( | ||||
|         "job_id", db.Integer, db.ForeignKey(f"{_table_prefix_}job.id"), nullable=False, primary_key=True | ||||
|     ) | ||||
|     _user_id = db.Column("user_id", db.Integer, db.ForeignKey("user.id"), nullable=False, primary_key=True) | ||||
|     _job_id = db.Column("job_id", Serial, 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_: User = db.relationship("User") | ||||
|     job_: Job = db.relationship("Job") | ||||
|  | @ -54,9 +52,9 @@ class Service(db.Model, ModelSerializeMixin): | |||
| 
 | ||||
| class Job(db.Model, ModelSerializeMixin): | ||||
|     __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) | ||||
|     end: Optional[datetime] = db.Column(UtcDateTime) | ||||
|     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) | ||||
| 
 | ||||
|     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"),) | ||||
| 
 | ||||
|  | @ -77,7 +75,7 @@ class Event(db.Model, ModelSerializeMixin): | |||
|     """Model for an 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) | ||||
|     end: Optional[datetime] = db.Column(UtcDateTime) | ||||
|     name: Optional[str] = db.Column(db.String(255)) | ||||
|  | @ -89,15 +87,15 @@ class Event(db.Model, ModelSerializeMixin): | |||
|     ) | ||||
|     # Protected for internal use | ||||
|     _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): | ||||
|     __tablename__ = _table_prefix_ + "invite" | ||||
| 
 | ||||
|     id: int = db.Column(db.Integer, primary_key=True) | ||||
|     job_id: int = db.Column(db.Integer, db.ForeignKey(_table_prefix_ + "job.id"), nullable=False) | ||||
|     id: int = db.Column(Serial, primary_key=True) | ||||
|     job_id: int = db.Column(Serial, db.ForeignKey(_table_prefix_ + "job.id"), nullable=False) | ||||
|     # Dummy properties for API export | ||||
|     invitee_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") | ||||
|     sender_: User = db.relationship("User", foreign_keys="Invite._sender_id") | ||||
|     # Protected properties needed for internal use | ||||
|     _invitee_id = db.Column("invitee_id", db.Integer, db.ForeignKey("user.id"), nullable=False) | ||||
|     _sender_id = db.Column("sender_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", Serial, db.ForeignKey("user.id"), nullable=False) | ||||
| 
 | ||||
|     @property | ||||
|     def invitee_id(self): | ||||
|  |  | |||
|  | @ -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 | ||||
|  | @ -13,22 +13,21 @@ from flaschengeist.controller import userController | |||
| from . import models | ||||
| from . import pricelist_controller, permissions | ||||
| 
 | ||||
| plugin_name = "pricelist" | ||||
| pricelist_bp = Blueprint(plugin_name, __name__, url_prefix="/pricelist") | ||||
| plugin = LocalProxy(lambda: current_app.config["FG_PLUGINS"][plugin_name]) | ||||
| 
 | ||||
| 
 | ||||
| class PriceListPlugin(Plugin): | ||||
|     name = "pricelist" | ||||
|     blueprint = Blueprint(name, __name__, url_prefix="/pricelist") | ||||
|     plugin = LocalProxy(lambda: current_app.config["FG_PLUGINS"][PriceListPlugin.name]) | ||||
|     models = models | ||||
| 
 | ||||
|     def __init__(self, cfg): | ||||
|         super().__init__(blueprint=pricelist_bp, permissions=permissions.permissions) | ||||
|         super().__init__(cfg) | ||||
|         config = {"discount": 0} | ||||
|         config.update(cfg) | ||||
| 
 | ||||
| 
 | ||||
| @pricelist_bp.route("/drink-types", methods=["GET"]) | ||||
| @pricelist_bp.route("/drink-types/<int:identifier>", methods=["GET"]) | ||||
| @PriceListPlugin.blueprint.route("/drink-types", methods=["GET"]) | ||||
| @PriceListPlugin.blueprint.route("/drink-types/<int:identifier>", methods=["GET"]) | ||||
| def get_drink_types(identifier=None): | ||||
|     if identifier is None: | ||||
|         result = pricelist_controller.get_drink_types() | ||||
|  | @ -37,7 +36,7 @@ def get_drink_types(identifier=None): | |||
|     return jsonify(result) | ||||
| 
 | ||||
| 
 | ||||
| @pricelist_bp.route("/drink-types", methods=["POST"]) | ||||
| @PriceListPlugin.blueprint.route("/drink-types", methods=["POST"]) | ||||
| @login_required(permission=permissions.CREATE_TYPE) | ||||
| def new_drink_type(current_session): | ||||
|     data = request.get_json() | ||||
|  | @ -47,7 +46,7 @@ def new_drink_type(current_session): | |||
|     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) | ||||
| def update_drink_type(identifier, current_session): | ||||
|     data = request.get_json() | ||||
|  | @ -57,15 +56,15 @@ def update_drink_type(identifier, current_session): | |||
|     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) | ||||
| def delete_drink_type(identifier, current_session): | ||||
|     pricelist_controller.delete_drink_type(identifier) | ||||
|     return no_content() | ||||
| 
 | ||||
| 
 | ||||
| @pricelist_bp.route("/tags", methods=["GET"]) | ||||
| @pricelist_bp.route("/tags/<int:identifier>", methods=["GET"]) | ||||
| @PriceListPlugin.blueprint.route("/tags", methods=["GET"]) | ||||
| @PriceListPlugin.blueprint.route("/tags/<int:identifier>", methods=["GET"]) | ||||
| def get_tags(identifier=None): | ||||
|     if identifier: | ||||
|         result = pricelist_controller.get_tag(identifier) | ||||
|  | @ -74,7 +73,7 @@ def get_tags(identifier=None): | |||
|     return jsonify(result) | ||||
| 
 | ||||
| 
 | ||||
| @pricelist_bp.route("/tags", methods=["POST"]) | ||||
| @PriceListPlugin.blueprint.route("/tags", methods=["POST"]) | ||||
| @login_required(permission=permissions.CREATE_TAG) | ||||
| def new_tag(current_session): | ||||
|     data = request.get_json() | ||||
|  | @ -84,7 +83,7 @@ def new_tag(current_session): | |||
|     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) | ||||
| def update_tag(identifier, current_session): | ||||
|     data = request.get_json() | ||||
|  | @ -94,15 +93,15 @@ def update_tag(identifier, current_session): | |||
|     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) | ||||
| def delete_tag(identifier, current_session): | ||||
|     pricelist_controller.delete_tag(identifier) | ||||
|     return no_content() | ||||
| 
 | ||||
| 
 | ||||
| @pricelist_bp.route("/drinks", methods=["GET"]) | ||||
| @pricelist_bp.route("/drinks/<int:identifier>", methods=["GET"]) | ||||
| @PriceListPlugin.blueprint.route("/drinks", methods=["GET"]) | ||||
| @PriceListPlugin.blueprint.route("/drinks/<int:identifier>", methods=["GET"]) | ||||
| def get_drinks(identifier=None): | ||||
|     if identifier: | ||||
|         result = pricelist_controller.get_drink(identifier) | ||||
|  | @ -111,85 +110,86 @@ def get_drinks(identifier=None): | |||
|     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): | ||||
|     return jsonify(pricelist_controller.get_drinks(name)) | ||||
| 
 | ||||
| 
 | ||||
| @pricelist_bp.route("/drinks", methods=["POST"]) | ||||
| @PriceListPlugin.blueprint.route("/drinks", methods=["POST"]) | ||||
| @login_required(permission=permissions.CREATE) | ||||
| def create_drink(current_session): | ||||
|     data = request.get_json() | ||||
|     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): | ||||
|     data = request.get_json() | ||||
|     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): | ||||
|     pricelist_controller.delete_drink(identifier) | ||||
|     return no_content() | ||||
| 
 | ||||
| 
 | ||||
| @pricelist_bp.route("/prices/<int:identifier>", methods=["DELETE"]) | ||||
| @PriceListPlugin.blueprint.route("/prices/<int:identifier>", methods=["DELETE"]) | ||||
| def delete_price(identifier): | ||||
|     pricelist_controller.delete_price(identifier) | ||||
|     return no_content() | ||||
| 
 | ||||
| 
 | ||||
| @pricelist_bp.route("/volumes/<int:identifier>", methods=["DELETE"]) | ||||
| @PriceListPlugin.blueprint.route("/volumes/<int:identifier>", methods=["DELETE"]) | ||||
| def delete_volume(identifier): | ||||
|     pricelist_controller.delete_volume(identifier) | ||||
|     return no_content() | ||||
| 
 | ||||
| 
 | ||||
| @pricelist_bp.route("/ingredients/extraIngredients", methods=["GET"]) | ||||
| @PriceListPlugin.blueprint.route("/ingredients/extraIngredients", methods=["GET"]) | ||||
| def 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): | ||||
|     pricelist_controller.delete_ingredient(identifier) | ||||
|     return no_content() | ||||
| 
 | ||||
| 
 | ||||
| @pricelist_bp.route("/ingredients/extraIngredients", methods=["POST"]) | ||||
| @PriceListPlugin.blueprint.route("/ingredients/extraIngredients", methods=["POST"]) | ||||
| def set_extra_ingredient(): | ||||
|     data = request.get_json() | ||||
|     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): | ||||
|     data = request.get_json() | ||||
|     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): | ||||
|     pricelist_controller.delete_extra_ingredient(identifier) | ||||
|     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(): | ||||
|     if request.method == "GET": | ||||
|         return jsonify(plugin.get_setting("min_prices")) | ||||
|         # TODO: Handle if no prices are set! | ||||
|         return jsonify(PriceListPlugin.plugin.get_setting("min_prices")) | ||||
|     else: | ||||
|         data = request.get_json() | ||||
|         if not isinstance(data, list) or not all(isinstance(n, int) for n in data): | ||||
|             raise BadRequest | ||||
|         data.sort() | ||||
|         plugin.set_setting("min_prices", data) | ||||
|         PriceListPlugin.plugin.set_setting("min_prices", data) | ||||
|         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() | ||||
| def get_columns(userid, current_session: Session): | ||||
|     """Get pricecalc_columns of an user | ||||
|  |  | |||
|  | @ -12,17 +12,17 @@ from flaschengeist.utils.decorators import login_required | |||
| from flaschengeist.controller import roleController | ||||
| from flaschengeist.utils.HTTP import created | ||||
| 
 | ||||
| roles_bp = Blueprint("roles", __name__) | ||||
| _permission_edit = "roles_edit" | ||||
| _permission_delete = "roles_delete" | ||||
| 
 | ||||
| 
 | ||||
| class RolesPlugin(Plugin): | ||||
|     def __init__(self, config): | ||||
|         super().__init__(config, roles_bp, permissions=[_permission_edit, _permission_delete]) | ||||
|     name = "roles" | ||||
|     blueprint = Blueprint(name, __name__) | ||||
|     permissions = [_permission_edit, _permission_delete] | ||||
| 
 | ||||
| 
 | ||||
| @roles_bp.route("/roles", methods=["GET"]) | ||||
| @RolesPlugin.blueprint.route("/roles", methods=["GET"]) | ||||
| @login_required() | ||||
| def list_roles(current_session): | ||||
|     """List all existing roles | ||||
|  | @ -39,7 +39,7 @@ def list_roles(current_session): | |||
|     return jsonify(roles) | ||||
| 
 | ||||
| 
 | ||||
| @roles_bp.route("/roles", methods=["POST"]) | ||||
| @RolesPlugin.blueprint.route("/roles", methods=["POST"]) | ||||
| @login_required(permission=_permission_edit) | ||||
| def create_role(current_session): | ||||
|     """Create new role | ||||
|  | @ -62,7 +62,7 @@ def create_role(current_session): | |||
|     return created(roleController.create_role(data["name"], permissions)) | ||||
| 
 | ||||
| 
 | ||||
| @roles_bp.route("/roles/permissions", methods=["GET"]) | ||||
| @RolesPlugin.blueprint.route("/roles/permissions", methods=["GET"]) | ||||
| @login_required() | ||||
| def list_permissions(current_session): | ||||
|     """List all existing permissions | ||||
|  | @ -79,7 +79,7 @@ def list_permissions(current_session): | |||
|     return jsonify(permissions) | ||||
| 
 | ||||
| 
 | ||||
| @roles_bp.route("/roles/<role_name>", methods=["GET"]) | ||||
| @RolesPlugin.blueprint.route("/roles/<role_name>", methods=["GET"]) | ||||
| @login_required() | ||||
| def get_role(role_name, current_session): | ||||
|     """Get role by name | ||||
|  | @ -97,7 +97,7 @@ def get_role(role_name, current_session): | |||
|     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) | ||||
| def edit_role(role_id, current_session): | ||||
|     """Edit role, rename and / or set permissions | ||||
|  | @ -123,7 +123,7 @@ def edit_role(role_id, current_session): | |||
|     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) | ||||
| def delete_role(role_id, current_session): | ||||
|     """Delete role | ||||
|  |  | |||
|  | @ -13,18 +13,17 @@ from flaschengeist.plugins import Plugin | |||
| from flaschengeist.models.user import User, _Avatar | ||||
| from flaschengeist.utils.decorators import login_required, extract_session, headers | ||||
| 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 | ||||
| 
 | ||||
| users_bp = Blueprint("users", __name__) | ||||
| 
 | ||||
| 
 | ||||
| class UsersPlugin(Plugin): | ||||
|     def __init__(self, cfg): | ||||
|         super().__init__(blueprint=users_bp, permissions=permissions.permissions) | ||||
|     name = "users" | ||||
|     blueprint = Blueprint(name, __name__) | ||||
|     permissions = permissions.permissions | ||||
| 
 | ||||
| 
 | ||||
| @users_bp.route("/users", methods=["POST"]) | ||||
| @UsersPlugin.blueprint.route("/users", methods=["POST"]) | ||||
| def register(): | ||||
|     """Register a new user | ||||
|     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) | ||||
| 
 | ||||
| 
 | ||||
| @users_bp.route("/users", methods=["GET"]) | ||||
| @UsersPlugin.blueprint.route("/users", methods=["GET"]) | ||||
| @login_required() | ||||
| @headers({"Cache-Control": "private, must-revalidate, max-age=3600"}) | ||||
| def list_users(current_session): | ||||
|  | @ -74,7 +73,7 @@ def list_users(current_session): | |||
|     return jsonify(users) | ||||
| 
 | ||||
| 
 | ||||
| @users_bp.route("/users/<userid>", methods=["GET"]) | ||||
| @UsersPlugin.blueprint.route("/users/<userid>", methods=["GET"]) | ||||
| @login_required() | ||||
| @headers({"Cache-Control": "private, must-revalidate, max-age=3600"}) | ||||
| def get_user(userid, current_session): | ||||
|  | @ -97,7 +96,25 @@ def get_user(userid, current_session): | |||
|     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"}) | ||||
| def get_avatar(userid): | ||||
|     user = userController.get_user(userid) | ||||
|  | @ -109,7 +126,7 @@ def get_avatar(userid): | |||
|     raise NotFound | ||||
| 
 | ||||
| 
 | ||||
| @users_bp.route("/users/<userid>/avatar", methods=["POST"]) | ||||
| @UsersPlugin.blueprint.route("/users/<userid>/avatar", methods=["POST"]) | ||||
| @login_required() | ||||
| def set_avatar(userid, current_session): | ||||
|     user = userController.get_user(userid) | ||||
|  | @ -127,7 +144,7 @@ def set_avatar(userid, current_session): | |||
|         raise BadRequest | ||||
| 
 | ||||
| 
 | ||||
| @users_bp.route("/users/<userid>", methods=["DELETE"]) | ||||
| @UsersPlugin.blueprint.route("/users/<userid>", methods=["DELETE"]) | ||||
| @login_required(permission=permissions.DELETE) | ||||
| def delete_user(userid, current_session): | ||||
|     """Delete user by userid | ||||
|  | @ -147,7 +164,7 @@ def delete_user(userid, current_session): | |||
|     return "", NO_CONTENT | ||||
| 
 | ||||
| 
 | ||||
| @users_bp.route("/users/<userid>", methods=["PUT"]) | ||||
| @UsersPlugin.blueprint.route("/users/<userid>", methods=["PUT"]) | ||||
| @login_required() | ||||
| def edit_user(userid, current_session): | ||||
|     """Modify user by userid | ||||
|  | @ -198,3 +215,19 @@ def edit_user(userid, current_session): | |||
|     userController.modify_user(user, password, new_password) | ||||
|     userController.update_user(user) | ||||
|     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() | ||||
|  |  | |||
|  | @ -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() | ||||
|  | @ -160,7 +160,7 @@ def export(arguments): | |||
|         if arguments.plugins: | ||||
|             for entry_point in pkg_resources.iter_entry_points("flaschengeist.plugin"): | ||||
|                 plg = entry_point.load() | ||||
|                 if hasattr(plg, "models"): | ||||
|                 if hasattr(plg, "models") and plg.models is not None: | ||||
|                     gen.run(plg.models) | ||||
|         gen.write() | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue