Merge remote-tracking branch 'origin/develop' into feature/pricelist
This commit is contained in:
commit
3a4e90f50e
|
@ -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):
|
||||
|
|
|
@ -1,22 +1,25 @@
|
|||
CREATE = "schedule_create"
|
||||
CREATE = "events_create"
|
||||
"""Can create events"""
|
||||
|
||||
EDIT = "schedule_edit"
|
||||
EDIT = "events_edit"
|
||||
"""Can edit events"""
|
||||
|
||||
DELETE = "schedule_delete"
|
||||
DELETE = "events_delete"
|
||||
"""Can delete events"""
|
||||
|
||||
EVENT_TYPE = "schedule_event_type"
|
||||
EVENT_TYPE = "events_event_type"
|
||||
"""Can create and edit EventTypes"""
|
||||
|
||||
JOB_TYPE = "schedule_job_type"
|
||||
JOB_TYPE = "events_job_type"
|
||||
"""Can create and edit JobTypes"""
|
||||
|
||||
ASSIGN = "schedule_assign"
|
||||
ASSIGN = "events_assign"
|
||||
"""Can self assign to jobs"""
|
||||
|
||||
ASSIGN_OTHER = "schedule_assign_other"
|
||||
ASSIGN_OTHER = "events_assign_other"
|
||||
"""Can assign other users to jobs"""
|
||||
|
||||
SEE_BACKUP = "events_see_backup"
|
||||
"""Can see users assigned as backup"""
|
||||
|
||||
permissions = [value for key, value in globals().items() if not key.startswith("_")]
|
||||
|
|
|
@ -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
|
|
@ -1,42 +1,42 @@
|
|||
"""Pricelist plugin"""
|
||||
|
||||
from flask import Blueprint, jsonify, request
|
||||
from http.client import NO_CONTENT
|
||||
from flask import Blueprint, jsonify, request, current_app
|
||||
from werkzeug.local import LocalProxy
|
||||
from werkzeug.exceptions import BadRequest, Forbidden, Unauthorized
|
||||
|
||||
from flaschengeist.plugins import Plugin
|
||||
from flaschengeist.utils.decorators import login_required, extract_session
|
||||
from werkzeug.exceptions import BadRequest, Forbidden, Unauthorized
|
||||
from flaschengeist.config import config
|
||||
from flaschengeist.utils.HTTP import no_content
|
||||
from flaschengeist.models.session import Session
|
||||
from flaschengeist.controller import userController
|
||||
|
||||
from . import models
|
||||
from . import pricelist_controller, permissions
|
||||
from ...controller import userController
|
||||
from ...models.session import Session
|
||||
from ...utils.HTTP import no_content
|
||||
|
||||
pricelist_bp = Blueprint("pricelist", __name__, url_prefix="/pricelist")
|
||||
|
||||
|
||||
class PriceListPlugin(Plugin):
|
||||
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:
|
||||
result = pricelist_controller.get_drink_type(identifier)
|
||||
else:
|
||||
if identifier is None:
|
||||
result = pricelist_controller.get_drink_types()
|
||||
else:
|
||||
result = pricelist_controller.get_drink_type(identifier)
|
||||
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()
|
||||
|
@ -46,25 +46,25 @@ 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()
|
||||
if "name" not in data:
|
||||
raise BadRequest
|
||||
drink_type = pricelist_controller.rename_drink_type(data["id"], data["name"])
|
||||
drink_type = pricelist_controller.rename_drink_type(identifier, data["name"])
|
||||
return jsonify(drink_type)
|
||||
|
||||
|
||||
@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
|
||||
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)
|
||||
|
@ -73,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()
|
||||
|
@ -83,25 +83,25 @@ 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()
|
||||
if "name" not in data:
|
||||
raise BadRequest
|
||||
drink_type = pricelist_controller.rename_tag(data["name"])
|
||||
return jsonify(drink_type)
|
||||
tag = pricelist_controller.rename_tag(identifier, data["name"])
|
||||
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
|
||||
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):
|
||||
public = True
|
||||
try:
|
||||
|
@ -116,85 +116,87 @@ def get_drinks(identifier=None):
|
|||
result = pricelist_controller.get_drinks(public=public)
|
||||
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
|
||||
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
|
||||
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
|
||||
return no_content()
|
||||
|
||||
|
||||
@pricelist_bp.route("/ingredients/extraIngredients", methods=["GET"])
|
||||
def get_extraIngredients():
|
||||
@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
|
||||
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
|
||||
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(PriceListPlugin.get_setting(PriceListPlugin, "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()
|
||||
PriceListPlugin.set_setting(PriceListPlugin, "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
|
||||
|
@ -225,7 +227,7 @@ def get_columns(userid, current_session: Session):
|
|||
userController.persist()
|
||||
return no_content()
|
||||
|
||||
@pricelist_bp.route("/drinks/<int:identifier>/picture", methods=["POST", "GET", "DELETE"])
|
||||
@PriceListPlugin.route("/drinks/<int:identifier>/picture", methods=["POST", "GET", "DELETE"])
|
||||
def set_picture(identifier):
|
||||
|
||||
if request.method == "DELETE":
|
||||
|
@ -241,10 +243,10 @@ def set_picture(identifier):
|
|||
else:
|
||||
raise BadRequest
|
||||
|
||||
@pricelist_bp.route("/picture/<identifier>", methods=["GET"])
|
||||
@PriceListPlugin.route("/picture/<identifier>", methods=["GET"])
|
||||
def _get_picture(identifier):
|
||||
if request.method == "GET":
|
||||
size = request.args.get("size")
|
||||
path = config["pricelist"]["path"]
|
||||
path = PriceListPlugin.plugin["path"]
|
||||
response = pricelist_controller.get_drink_picture(identifier, size)
|
||||
return response.make_conditional(request)
|
|
@ -3,7 +3,7 @@ from __future__ import annotations # TODO: Remove if python requirement is >= 3
|
|||
from flaschengeist.database import db
|
||||
from flaschengeist.models import ModelSerializeMixin
|
||||
|
||||
from typing import Optional, Union
|
||||
from typing import Optional
|
||||
|
||||
drink_tag_association = db.Table(
|
||||
"drink_x_tag",
|
||||
|
@ -71,7 +71,7 @@ class DrinkIngredient(db.Model, ModelSerializeMixin):
|
|||
__tablename__ = "drink_ingredient"
|
||||
id: int = db.Column("id", db.Integer, primary_key=True)
|
||||
volume: float = db.Column(db.Numeric(precision=5, scale=2, asdecimal=False), nullable=False)
|
||||
drink_ingredient_id: int = db.Column("drink_ingredient_id", db.Integer, db.ForeignKey("drink.id"))
|
||||
ingredient_id: int = db.Column(db.Integer, db.ForeignKey("drink.id"))
|
||||
# drink_ingredient: Drink = db.relationship("Drink")
|
||||
# price: float = 0
|
||||
|
||||
|
@ -92,11 +92,12 @@ class Ingredient(db.Model, ModelSerializeMixin):
|
|||
__tablename__ = "ingredient_association"
|
||||
id: int = db.Column("id", db.Integer, primary_key=True)
|
||||
volume_id = db.Column(db.Integer, db.ForeignKey("drink_price_volume.id"))
|
||||
drink_ingredient_id = db.Column(db.Integer, db.ForeignKey("drink_ingredient.id"))
|
||||
drink_ingredient: Optional[DrinkIngredient] = db.relationship(DrinkIngredient)
|
||||
extra_ingredient_id = db.Column(db.Integer, db.ForeignKey("extra_ingredient.id"))
|
||||
extra_ingredient: Optional[ExtraIngredient] = db.relationship(ExtraIngredient)
|
||||
|
||||
_drink_ingredient_id = db.Column(db.Integer, db.ForeignKey("drink_ingredient.id"))
|
||||
_extra_ingredient_id = db.Column(db.Integer, db.ForeignKey("extra_ingredient.id"))
|
||||
|
||||
|
||||
class MinPrices(ModelSerializeMixin):
|
||||
"""
|
||||
|
@ -134,8 +135,8 @@ class Drink(db.Model, ModelSerializeMixin):
|
|||
package_size: Optional[int] = db.Column(db.Integer)
|
||||
name: str = db.Column(db.String(60), nullable=False)
|
||||
volume: Optional[float] = db.Column(db.Numeric(precision=5, scale=2, asdecimal=False))
|
||||
cost_price_pro_volume: Optional[float] = db.Column(db.Numeric(precision=5, scale=3, asdecimal=False))
|
||||
cost_price_package_netto: Optional[float] = db.Column(db.Numeric(precision=5, scale=3, asdecimal=False))
|
||||
cost_per_volume: Optional[float] = db.Column(db.Numeric(precision=5, scale=3, asdecimal=False))
|
||||
cost_per_package: Optional[float] = db.Column(db.Numeric(precision=5, scale=3, asdecimal=False))
|
||||
|
||||
uuid: str = db.Column(db.String(36))
|
||||
receipt: Optional[str] = db.Column(db.String)
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
from werkzeug.exceptions import BadRequest, NotFound
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from uuid import uuid4
|
||||
|
||||
from flaschengeist import logger
|
||||
from flaschengeist.config import config
|
||||
from flaschengeist.database import db
|
||||
from flaschengeist.utils.picture import save_picture, get_picture
|
||||
|
||||
from .models import Drink, DrinkPrice, Ingredient, Tag, DrinkType, DrinkPriceVolume, DrinkIngredient, ExtraIngredient
|
||||
|
||||
from flaschengeist.utils.picture import save_picture, get_picture, delete_picture
|
||||
|
||||
from uuid import uuid4
|
||||
|
||||
|
||||
def update():
|
||||
db.session.commit()
|
||||
|
@ -21,15 +20,15 @@ def get_tags():
|
|||
|
||||
def get_tag(identifier):
|
||||
if isinstance(identifier, int):
|
||||
retVal = Tag.query.get(identifier)
|
||||
ret = Tag.query.get(identifier)
|
||||
elif isinstance(identifier, str):
|
||||
retVal = Tag.query.filter(Tag.name == identifier).one_or_none()
|
||||
ret = Tag.query.filter(Tag.name == identifier).one_or_none()
|
||||
else:
|
||||
logger.debug("Invalid identifier type for Tag")
|
||||
raise BadRequest
|
||||
if not retVal:
|
||||
if ret is None:
|
||||
raise NotFound
|
||||
return retVal
|
||||
return ret
|
||||
|
||||
|
||||
def create_tag(name):
|
||||
|
@ -66,23 +65,23 @@ def get_drink_types():
|
|||
|
||||
def get_drink_type(identifier):
|
||||
if isinstance(identifier, int):
|
||||
retVal = DrinkType.query.get(identifier)
|
||||
ret = DrinkType.query.get(identifier)
|
||||
elif isinstance(identifier, str):
|
||||
retVal = DrinkType.query.filter(Tag.name == identifier).one_or_none()
|
||||
ret = DrinkType.query.filter(Tag.name == identifier).one_or_none()
|
||||
else:
|
||||
logger.debug("Invalid identifier type for DrinkType")
|
||||
raise BadRequest
|
||||
if not retVal:
|
||||
if ret is None:
|
||||
raise NotFound
|
||||
return retVal
|
||||
return ret
|
||||
|
||||
|
||||
def create_drink_type(name):
|
||||
try:
|
||||
drinkType = DrinkType(name=name)
|
||||
db.session.add(drinkType)
|
||||
drink_type = DrinkType(name=name)
|
||||
db.session.add(drink_type)
|
||||
update()
|
||||
return drinkType
|
||||
return drink_type
|
||||
except IntegrityError:
|
||||
raise BadRequest("Name already exists")
|
||||
|
||||
|
@ -98,8 +97,8 @@ def rename_drink_type(identifier, new_name):
|
|||
|
||||
|
||||
def delete_drink_type(identifier):
|
||||
drinkType = get_drink_type(identifier)
|
||||
db.session.delete(drinkType)
|
||||
drink_type = get_drink_type(identifier)
|
||||
db.session.delete(drink_type)
|
||||
try:
|
||||
update()
|
||||
except IntegrityError:
|
||||
|
@ -137,13 +136,12 @@ def get_drink(identifier, public=False):
|
|||
elif isinstance(identifier, str):
|
||||
drink = Drink.query.filter(Tag.name == identifier).one_or_none()
|
||||
else:
|
||||
logger.debug("Invalid identifier type for Drink")
|
||||
raise BadRequest
|
||||
if drink:
|
||||
raise BadRequest("Invalid identifier type for Drink")
|
||||
if drink is None:
|
||||
raise NotFound
|
||||
if public:
|
||||
return _create_public_drink(drink)
|
||||
return drink
|
||||
raise NotFound
|
||||
|
||||
|
||||
def set_drink(data):
|
||||
|
@ -151,44 +149,40 @@ def set_drink(data):
|
|||
|
||||
|
||||
def update_drink(identifier, data):
|
||||
allowedKeys = Drink().serialize().keys()
|
||||
try:
|
||||
if "id" in data:
|
||||
data.pop("id")
|
||||
if "volumes" in data:
|
||||
volumes = data.pop("volumes")
|
||||
volumes = data.pop("volumes") if "volumes" in data else None
|
||||
if "tags" in data:
|
||||
data.pop("tags")
|
||||
type = None
|
||||
if "type" in data:
|
||||
_type = data.pop("type")
|
||||
if isinstance(_type, dict) and "id" in _type:
|
||||
type = get_drink_type(_type.get("id"))
|
||||
drink_type = data.pop("type")
|
||||
if isinstance(drink_type, dict) and "id" in drink_type:
|
||||
drink_type = drink_type["id"]
|
||||
drink_type = get_drink_type(drink_type)
|
||||
if identifier == -1:
|
||||
drink = Drink()
|
||||
db.session.add(drink)
|
||||
else:
|
||||
drink = get_drink(identifier)
|
||||
if not drink:
|
||||
raise NotFound
|
||||
for key, value in data.items():
|
||||
if hasattr(drink, key):
|
||||
setattr(drink, key, value if value != "" else None)
|
||||
|
||||
if type:
|
||||
drink.type = type
|
||||
if volumes:
|
||||
if drink_type:
|
||||
drink.type = drink_type
|
||||
if volumes is not None:
|
||||
set_volumes(volumes, drink)
|
||||
db.session.commit()
|
||||
return drink
|
||||
except (NotFound, KeyError):
|
||||
raise BadRequest
|
||||
|
||||
|
||||
def set_volumes(volumes, drink):
|
||||
if isinstance(volumes, list):
|
||||
_volumes = []
|
||||
for _volume in volumes:
|
||||
volume = set_volume(_volume)
|
||||
_volumes.append(volume)
|
||||
drink.volumes = _volumes
|
||||
if not isinstance(volumes, list):
|
||||
raise BadRequest
|
||||
for volume in volumes:
|
||||
drink.volumes.append(set_volume(volume))
|
||||
|
||||
|
||||
def delete_drink(identifier):
|
||||
|
@ -216,15 +210,12 @@ def set_volume(data):
|
|||
prices = values.pop("prices")
|
||||
if "ingredients" in values:
|
||||
ingredients = values.pop("ingredients")
|
||||
id = None
|
||||
if "id" in values:
|
||||
id = values.pop("id")
|
||||
volume = None
|
||||
if id < 0:
|
||||
vol_id = values.pop("id", None)
|
||||
if vol_id < 0:
|
||||
volume = DrinkPriceVolume(**values)
|
||||
db.session.add(volume)
|
||||
else:
|
||||
volume = get_volume(id)
|
||||
volume = get_volume(vol_id)
|
||||
if not volume:
|
||||
raise NotFound
|
||||
for key, value in values.items():
|
||||
|
@ -276,15 +267,12 @@ def get_prices(volume_id=None):
|
|||
def set_price(data):
|
||||
allowed_keys = DrinkPrice().serialize().keys()
|
||||
values = {key: value for key, value in data.items() if key in allowed_keys}
|
||||
id = None
|
||||
if "id" in values:
|
||||
id = values.pop("id")
|
||||
price = None
|
||||
if id < 0:
|
||||
price_id = values.pop("id", -1)
|
||||
if price_id < 0:
|
||||
price = DrinkPrice(**values)
|
||||
db.session.add(price)
|
||||
else:
|
||||
price = get_price(id)
|
||||
price = get_price(price_id)
|
||||
if not price:
|
||||
raise NotFound
|
||||
for key, value in values.items():
|
||||
|
@ -300,17 +288,14 @@ def delete_price(identifier):
|
|||
|
||||
|
||||
def set_drink_ingredient(data):
|
||||
allowedKeys = DrinkIngredient().serialize().keys()
|
||||
drink = None
|
||||
values = {key: value for key, value in data.items() if key in allowedKeys}
|
||||
id = None
|
||||
if "id" in values:
|
||||
id = values.pop("id")
|
||||
if id < 0:
|
||||
allowed_keys = DrinkIngredient().serialize().keys()
|
||||
values = {key: value for key, value in data.items() if key in allowed_keys}
|
||||
ingredient_id = values.pop("id", -1)
|
||||
if ingredient_id < 0:
|
||||
drink_ingredient = DrinkIngredient(**values)
|
||||
db.session.add(drink_ingredient)
|
||||
else:
|
||||
drink_ingredient = DrinkIngredient.query.get(id)
|
||||
drink_ingredient = DrinkIngredient.query.get(ingredient_id)
|
||||
if not drink_ingredient:
|
||||
raise NotFound
|
||||
for key, value in values.items():
|
||||
|
@ -329,15 +314,12 @@ def set_ingredient(data):
|
|||
drink_ingredient_value = data.pop("drink_ingredient")
|
||||
if "extra_ingredient" in data:
|
||||
extra_ingredient_value = data.pop("extra_ingredient")
|
||||
id = None
|
||||
if "id" in data:
|
||||
id = data.pop("id")
|
||||
ingredient = None
|
||||
if id < 0:
|
||||
ingredient_id = data.pop("id", -1)
|
||||
if ingredient_id < 0:
|
||||
ingredient = Ingredient(**data)
|
||||
db.session.add(ingredient)
|
||||
else:
|
||||
ingredient = get_ingredient(id)
|
||||
ingredient = get_ingredient(ingredient_id)
|
||||
if not ingredient:
|
||||
raise NotFound
|
||||
if drink_ingredient_value:
|
||||
|
@ -365,10 +347,10 @@ def get_extra_ingredient(identifier):
|
|||
|
||||
|
||||
def set_extra_ingredient(data):
|
||||
allowedKeys = ExtraIngredient().serialize().keys()
|
||||
allowed_keys = ExtraIngredient().serialize().keys()
|
||||
if "id" in data:
|
||||
data.pop("id")
|
||||
values = {key: value for key, value in data.items() if key in allowedKeys}
|
||||
values = {key: value for key, value in data.items() if key in allowed_keys}
|
||||
extra_ingredient = ExtraIngredient(**values)
|
||||
db.session.add(extra_ingredient)
|
||||
db.session.commit()
|
||||
|
@ -376,10 +358,10 @@ def set_extra_ingredient(data):
|
|||
|
||||
|
||||
def update_extra_ingredient(identifier, data):
|
||||
allowedKeys = ExtraIngredient().serialize().keys()
|
||||
allowed_keys = ExtraIngredient().serialize().keys()
|
||||
if "id" in data:
|
||||
data.pop("id")
|
||||
values = {key: value for key, value in data.items() if key in allowedKeys}
|
||||
values = {key: value for key, value in data.items() if key in allowed_keys}
|
||||
extra_ingredient = get_extra_ingredient(identifier)
|
||||
if extra_ingredient:
|
||||
for key, value in values.items():
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
2
setup.py
2
setup.py
|
@ -6,7 +6,7 @@ mysql_driver = "PyMySQL" if os.name == "nt" else "mysqlclient"
|
|||
|
||||
|
||||
class DocsCommand(Command):
|
||||
description = "Generate and export API documentation"
|
||||
description = "Generate and export API documentation using pdoc3"
|
||||
user_options = [
|
||||
# The format is (long option, short option, description).
|
||||
("output=", "o", "Documentation output path"),
|
||||
|
|
Loading…
Reference in New Issue