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:
|
try:
|
||||||
logger.info(f"Load plugin {entry_point.name}")
|
logger.info(f"Load plugin {entry_point.name}")
|
||||||
plugin = entry_point.load()
|
plugin = entry_point.load()
|
||||||
setattr(plugin, "_plugin_name", entry_point.name)
|
if not hasattr(plugin, "name"):
|
||||||
|
setattr(plugin, "name", entry_point.name)
|
||||||
plugin = plugin(config[entry_point.name])
|
plugin = plugin(config[entry_point.name])
|
||||||
if plugin.blueprint:
|
if hasattr(plugin, "blueprint") and plugin.blueprint is not None:
|
||||||
app.register_blueprint(plugin.blueprint)
|
app.register_blueprint(plugin.blueprint)
|
||||||
except:
|
except:
|
||||||
logger.error(
|
logger.error(
|
||||||
|
|
|
@ -6,6 +6,7 @@ from werkzeug.exceptions import NotFound, BadRequest, Forbidden
|
||||||
from flaschengeist import logger
|
from flaschengeist import logger
|
||||||
from flaschengeist.config import config
|
from flaschengeist.config import config
|
||||||
from flaschengeist.database import db
|
from flaschengeist.database import db
|
||||||
|
from flaschengeist.models.notification import Notification
|
||||||
from flaschengeist.utils.hook import Hook
|
from flaschengeist.utils.hook import Hook
|
||||||
from flaschengeist.utils.datetime import from_iso_format
|
from flaschengeist.utils.datetime import from_iso_format
|
||||||
from flaschengeist.models.user import User, Role, _PasswordReset
|
from flaschengeist.models.user import User, Role, _PasswordReset
|
||||||
|
@ -210,3 +211,15 @@ def persist(user=None):
|
||||||
if user:
|
if user:
|
||||||
db.session.add(user)
|
db.session.add(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def get_notifications(user, start=None):
|
||||||
|
query = Notification.query.filter(Notification.user_id_ == user.id_)
|
||||||
|
if start is not None:
|
||||||
|
query = query.filter(Notification.time > start)
|
||||||
|
return query.order_by(Notification.time).all()
|
||||||
|
|
||||||
|
|
||||||
|
def delete_notification(nid, user):
|
||||||
|
Notification.query.filter(Notification.id == nid).filter(Notification.user_ == user).delete()
|
||||||
|
db.session.commit()
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import sys
|
import sys
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
|
from sqlalchemy import BigInteger
|
||||||
|
from sqlalchemy.dialects import mysql
|
||||||
from sqlalchemy.types import DateTime, TypeDecorator
|
from sqlalchemy.types import DateTime, TypeDecorator
|
||||||
|
|
||||||
|
|
||||||
|
@ -39,6 +41,12 @@ class ModelSerializeMixin:
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
class Serial(TypeDecorator):
|
||||||
|
"""Same as MariaDB Serial used for IDs"""
|
||||||
|
|
||||||
|
impl = BigInteger().with_variant(mysql.BIGINT(unsigned=True), "mysql")
|
||||||
|
|
||||||
|
|
||||||
class UtcDateTime(TypeDecorator):
|
class UtcDateTime(TypeDecorator):
|
||||||
"""Almost equivalent to `sqlalchemy.types.DateTime` with
|
"""Almost equivalent to `sqlalchemy.types.DateTime` with
|
||||||
``timezone=True`` option, but it differs from that by:
|
``timezone=True`` option, but it differs from that by:
|
||||||
|
|
|
@ -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 datetime import datetime, timedelta, timezone
|
||||||
|
|
||||||
from . import ModelSerializeMixin, UtcDateTime
|
from . import ModelSerializeMixin, UtcDateTime, Serial
|
||||||
from .user import User
|
from .user import User
|
||||||
from flaschengeist.database import db
|
from flaschengeist.database import db
|
||||||
from secrets import compare_digest
|
from secrets import compare_digest
|
||||||
|
@ -26,8 +26,8 @@ class Session(db.Model, ModelSerializeMixin):
|
||||||
platform: str = db.Column(db.String(30))
|
platform: str = db.Column(db.String(30))
|
||||||
userid: str = ""
|
userid: str = ""
|
||||||
|
|
||||||
_id = db.Column("id", db.Integer, primary_key=True)
|
_id = db.Column("id", Serial, primary_key=True)
|
||||||
_user_id = db.Column("user_id", db.Integer, db.ForeignKey("user.id"))
|
_user_id = db.Column("user_id", Serial, db.ForeignKey("user.id"))
|
||||||
user_: User = db.relationship("User", back_populates="sessions_")
|
user_: User = db.relationship("User", back_populates="sessions_")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
from __future__ import annotations # TODO: Remove if python requirement is >= 3.10
|
from __future__ import annotations # TODO: Remove if python requirement is >= 3.10
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from . import Serial
|
||||||
from ..database import db
|
from ..database import db
|
||||||
|
|
||||||
|
|
||||||
class _PluginSetting(db.Model):
|
class _PluginSetting(db.Model):
|
||||||
__tablename__ = "plugin_setting"
|
__tablename__ = "plugin_setting"
|
||||||
id = db.Column("id", db.Integer, primary_key=True)
|
id = db.Column("id", Serial, primary_key=True)
|
||||||
plugin: str = db.Column(db.String(30))
|
plugin: str = db.Column(db.String(30))
|
||||||
name: str = db.Column(db.String(30), nullable=False)
|
name: str = db.Column(db.String(30), nullable=False)
|
||||||
value: any = db.Column(db.PickleType(protocol=4))
|
value: Any = db.Column(db.PickleType(protocol=4))
|
||||||
|
|
|
@ -6,19 +6,19 @@ from datetime import date, datetime
|
||||||
from sqlalchemy.orm.collections import attribute_mapped_collection
|
from sqlalchemy.orm.collections import attribute_mapped_collection
|
||||||
|
|
||||||
from ..database import db
|
from ..database import db
|
||||||
from . import ModelSerializeMixin, UtcDateTime
|
from . import ModelSerializeMixin, UtcDateTime, Serial
|
||||||
|
|
||||||
|
|
||||||
association_table = db.Table(
|
association_table = db.Table(
|
||||||
"user_x_role",
|
"user_x_role",
|
||||||
db.Column("user_id", db.Integer, db.ForeignKey("user.id")),
|
db.Column("user_id", Serial, db.ForeignKey("user.id")),
|
||||||
db.Column("role_id", db.Integer, db.ForeignKey("role.id")),
|
db.Column("role_id", Serial, db.ForeignKey("role.id")),
|
||||||
)
|
)
|
||||||
|
|
||||||
role_permission_association_table = db.Table(
|
role_permission_association_table = db.Table(
|
||||||
"role_x_permission",
|
"role_x_permission",
|
||||||
db.Column("role_id", db.Integer, db.ForeignKey("role.id")),
|
db.Column("role_id", Serial, db.ForeignKey("role.id")),
|
||||||
db.Column("permission_id", db.Integer, db.ForeignKey("permission.id")),
|
db.Column("permission_id", Serial, db.ForeignKey("permission.id")),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -26,12 +26,12 @@ class Permission(db.Model, ModelSerializeMixin):
|
||||||
__tablename__ = "permission"
|
__tablename__ = "permission"
|
||||||
name: str = db.Column(db.String(30), unique=True)
|
name: str = db.Column(db.String(30), unique=True)
|
||||||
|
|
||||||
_id = db.Column("id", db.Integer, primary_key=True)
|
_id = db.Column("id", Serial, primary_key=True)
|
||||||
|
|
||||||
|
|
||||||
class Role(db.Model, ModelSerializeMixin):
|
class Role(db.Model, ModelSerializeMixin):
|
||||||
__tablename__ = "role"
|
__tablename__ = "role"
|
||||||
id: int = db.Column(db.Integer, primary_key=True)
|
id: int = db.Column(Serial, primary_key=True)
|
||||||
name: str = db.Column(db.String(30), unique=True)
|
name: str = db.Column(db.String(30), unique=True)
|
||||||
permissions: list[Permission] = db.relationship("Permission", secondary=role_permission_association_table)
|
permissions: list[Permission] = db.relationship("Permission", secondary=role_permission_association_table)
|
||||||
|
|
||||||
|
@ -62,7 +62,7 @@ class User(db.Model, ModelSerializeMixin):
|
||||||
permissions: Optional[list[str]] = None
|
permissions: Optional[list[str]] = None
|
||||||
avatar_url: Optional[str] = ""
|
avatar_url: Optional[str] = ""
|
||||||
|
|
||||||
id_ = db.Column("id", db.Integer, primary_key=True)
|
id_ = db.Column("id", Serial, primary_key=True)
|
||||||
roles_: list[Role] = db.relationship("Role", secondary=association_table, cascade="save-update, merge")
|
roles_: list[Role] = db.relationship("Role", secondary=association_table, cascade="save-update, merge")
|
||||||
sessions_ = db.relationship("Session", back_populates="user_")
|
sessions_ = db.relationship("Session", back_populates="user_")
|
||||||
|
|
||||||
|
@ -101,8 +101,8 @@ class User(db.Model, ModelSerializeMixin):
|
||||||
|
|
||||||
class _UserAttribute(db.Model, ModelSerializeMixin):
|
class _UserAttribute(db.Model, ModelSerializeMixin):
|
||||||
__tablename__ = "user_attribute"
|
__tablename__ = "user_attribute"
|
||||||
id = db.Column("id", db.Integer, primary_key=True)
|
id = db.Column("id", Serial, primary_key=True)
|
||||||
user: User = db.Column("user", db.Integer, db.ForeignKey("user.id"), nullable=False)
|
user: User = db.Column("user", Serial, db.ForeignKey("user.id"), nullable=False)
|
||||||
name: str = db.Column(db.String(30))
|
name: str = db.Column(db.String(30))
|
||||||
value: any = db.Column(db.PickleType(protocol=4))
|
value: any = db.Column(db.PickleType(protocol=4))
|
||||||
|
|
||||||
|
@ -111,7 +111,7 @@ class _PasswordReset(db.Model):
|
||||||
"""Table containing password reset requests"""
|
"""Table containing password reset requests"""
|
||||||
|
|
||||||
__tablename__ = "password_reset"
|
__tablename__ = "password_reset"
|
||||||
_user_id: User = db.Column("user", db.Integer, db.ForeignKey("user.id"), primary_key=True)
|
_user_id: User = db.Column("user", Serial, db.ForeignKey("user.id"), primary_key=True)
|
||||||
user: User = db.relationship("User", foreign_keys=[_user_id])
|
user: User = db.relationship("User", foreign_keys=[_user_id])
|
||||||
token: str = db.Column(db.String(32))
|
token: str = db.Column(db.String(32))
|
||||||
expires: datetime = db.Column(UtcDateTime)
|
expires: datetime = db.Column(UtcDateTime)
|
||||||
|
|
|
@ -3,6 +3,7 @@ import pkg_resources
|
||||||
from werkzeug.exceptions import MethodNotAllowed, NotFound
|
from werkzeug.exceptions import MethodNotAllowed, NotFound
|
||||||
|
|
||||||
from flaschengeist.database import db
|
from flaschengeist.database import db
|
||||||
|
from flaschengeist.models.notification import Notification
|
||||||
from flaschengeist.models.user import _Avatar
|
from flaschengeist.models.user import _Avatar
|
||||||
from flaschengeist.models.setting import _PluginSetting
|
from flaschengeist.models.setting import _PluginSetting
|
||||||
from flaschengeist.utils.hook import HookBefore, HookAfter
|
from flaschengeist.utils.hook import HookBefore, HookAfter
|
||||||
|
@ -30,15 +31,16 @@ class Plugin:
|
||||||
"""Base class for all Plugins
|
"""Base class for all Plugins
|
||||||
If your class uses custom models add a static property called ``models``"""
|
If your class uses custom models add a static property called ``models``"""
|
||||||
|
|
||||||
def __init__(self, config=None, blueprint=None, permissions=[]):
|
blueprint = None # You have to override
|
||||||
|
permissions = [] # You have to override
|
||||||
|
name = "plugin" # You have to override
|
||||||
|
models = None # You have to override
|
||||||
|
|
||||||
|
def __init__(self, config=None):
|
||||||
"""Constructor called by create_app
|
"""Constructor called by create_app
|
||||||
Args:
|
Args:
|
||||||
config: Dict configuration containing the plugin section
|
config: Dict configuration containing the plugin section
|
||||||
blueprint: A flask blueprint containing all plugin routes
|
|
||||||
permissions: List of permissions of this Plugin
|
|
||||||
"""
|
"""
|
||||||
self.blueprint = blueprint
|
|
||||||
self.permissions = permissions
|
|
||||||
self.version = pkg_resources.get_distribution(self.__module__.split(".")[0]).version
|
self.version = pkg_resources.get_distribution(self.__module__.split(".")[0]).version
|
||||||
|
|
||||||
def install(self):
|
def install(self):
|
||||||
|
@ -63,7 +65,7 @@ class Plugin:
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
setting = (
|
setting = (
|
||||||
_PluginSetting.query.filter(_PluginSetting.plugin == self._plugin_name)
|
_PluginSetting.query.filter(_PluginSetting.plugin == self.name)
|
||||||
.filter(_PluginSetting.name == name)
|
.filter(_PluginSetting.name == name)
|
||||||
.one()
|
.one()
|
||||||
)
|
)
|
||||||
|
@ -81,14 +83,19 @@ class Plugin:
|
||||||
value: Value to be stored
|
value: Value to be stored
|
||||||
"""
|
"""
|
||||||
setting = (
|
setting = (
|
||||||
_PluginSetting.query.filter(_PluginSetting.plugin == self._plugin_name)
|
_PluginSetting.query.filter(_PluginSetting.plugin == self.name)
|
||||||
.filter(_PluginSetting.name == name)
|
.filter(_PluginSetting.name == name)
|
||||||
.one_or_none()
|
.one_or_none()
|
||||||
)
|
)
|
||||||
if setting is not None:
|
if setting is not None:
|
||||||
setting.value = value
|
setting.value = value
|
||||||
else:
|
else:
|
||||||
db.session.add(_PluginSetting(plugin=self._plugin_name, name=name, value=value))
|
db.session.add(_PluginSetting(plugin=self.name, name=name, value=value))
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
def notify(self, user, text: str, data=None):
|
||||||
|
n = Notification(text=text, data=data, plugin=self.name, user_=user)
|
||||||
|
db.session.add(n)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
def serialize(self):
|
def serialize(self):
|
||||||
|
|
|
@ -11,15 +11,13 @@ from flaschengeist.utils.HTTP import no_content, created
|
||||||
from flaschengeist.utils.decorators import login_required
|
from flaschengeist.utils.decorators import login_required
|
||||||
from flaschengeist.controller import sessionController, userController
|
from flaschengeist.controller import sessionController, userController
|
||||||
|
|
||||||
auth_bp = Blueprint("auth", __name__)
|
|
||||||
|
|
||||||
|
|
||||||
class AuthRoutePlugin(Plugin):
|
class AuthRoutePlugin(Plugin):
|
||||||
def __init__(self, conf):
|
name = "auth"
|
||||||
super().__init__(blueprint=auth_bp)
|
blueprint = Blueprint(name, __name__)
|
||||||
|
|
||||||
|
|
||||||
@auth_bp.route("/auth", methods=["POST"])
|
@AuthRoutePlugin.blueprint.route("/auth", methods=["POST"])
|
||||||
def login():
|
def login():
|
||||||
"""Login in an user and create a session
|
"""Login in an user and create a session
|
||||||
|
|
||||||
|
@ -52,7 +50,7 @@ def login():
|
||||||
return created(session)
|
return created(session)
|
||||||
|
|
||||||
|
|
||||||
@auth_bp.route("/auth", methods=["GET"])
|
@AuthRoutePlugin.blueprint.route("/auth", methods=["GET"])
|
||||||
@login_required()
|
@login_required()
|
||||||
def get_sessions(current_session, **kwargs):
|
def get_sessions(current_session, **kwargs):
|
||||||
"""Get all valid sessions of current user
|
"""Get all valid sessions of current user
|
||||||
|
@ -66,7 +64,7 @@ def get_sessions(current_session, **kwargs):
|
||||||
return jsonify(sessions)
|
return jsonify(sessions)
|
||||||
|
|
||||||
|
|
||||||
@auth_bp.route("/auth/<token>", methods=["DELETE"])
|
@AuthRoutePlugin.blueprint.route("/auth/<token>", methods=["DELETE"])
|
||||||
@login_required()
|
@login_required()
|
||||||
def delete_session(token, current_session, **kwargs):
|
def delete_session(token, current_session, **kwargs):
|
||||||
"""Delete a session aka "logout"
|
"""Delete a session aka "logout"
|
||||||
|
@ -88,7 +86,7 @@ def delete_session(token, current_session, **kwargs):
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
@auth_bp.route("/auth/<token>", methods=["GET"])
|
@AuthRoutePlugin.blueprint.route("/auth/<token>", methods=["GET"])
|
||||||
@login_required()
|
@login_required()
|
||||||
def get_session(token, current_session, **kwargs):
|
def get_session(token, current_session, **kwargs):
|
||||||
"""Retrieve information about a session
|
"""Retrieve information about a session
|
||||||
|
@ -111,7 +109,7 @@ def get_session(token, current_session, **kwargs):
|
||||||
return jsonify(session)
|
return jsonify(session)
|
||||||
|
|
||||||
|
|
||||||
@auth_bp.route("/auth/<token>", methods=["PUT"])
|
@AuthRoutePlugin.blueprint.route("/auth/<token>", methods=["PUT"])
|
||||||
@login_required()
|
@login_required()
|
||||||
def set_lifetime(token, current_session, **kwargs):
|
def set_lifetime(token, current_session, **kwargs):
|
||||||
"""Set lifetime of a session
|
"""Set lifetime of a session
|
||||||
|
@ -141,7 +139,7 @@ def set_lifetime(token, current_session, **kwargs):
|
||||||
raise BadRequest
|
raise BadRequest
|
||||||
|
|
||||||
|
|
||||||
@auth_bp.route("/auth/<token>/user", methods=["GET"])
|
@AuthRoutePlugin.blueprint.route("/auth/<token>/user", methods=["GET"])
|
||||||
@login_required()
|
@login_required()
|
||||||
def get_assocd_user(token, current_session, **kwargs):
|
def get_assocd_user(token, current_session, **kwargs):
|
||||||
"""Retrieve user owning a session
|
"""Retrieve user owning a session
|
||||||
|
@ -164,7 +162,7 @@ def get_assocd_user(token, current_session, **kwargs):
|
||||||
return jsonify(session.user_)
|
return jsonify(session.user_)
|
||||||
|
|
||||||
|
|
||||||
@auth_bp.route("/auth/reset", methods=["POST"])
|
@AuthRoutePlugin.blueprint.route("/auth/reset", methods=["POST"])
|
||||||
def reset_password():
|
def reset_password():
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
if "userid" in data:
|
if "userid" in data:
|
||||||
|
|
|
@ -3,30 +3,25 @@
|
||||||
Extends users plugin with balance functions
|
Extends users plugin with balance functions
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from datetime import datetime, timezone
|
from werkzeug.local import LocalProxy
|
||||||
|
from flask import Blueprint, current_app
|
||||||
from flaschengeist.utils.HTTP import no_content
|
|
||||||
from flask import Blueprint, request, jsonify
|
|
||||||
from werkzeug.exceptions import Forbidden, BadRequest
|
|
||||||
|
|
||||||
from flaschengeist import logger
|
from flaschengeist import logger
|
||||||
from flaschengeist.utils import HTTP
|
|
||||||
from flaschengeist.models.session import Session
|
|
||||||
from flaschengeist.utils.datetime import from_iso_format
|
|
||||||
from flaschengeist.utils.decorators import login_required
|
|
||||||
from flaschengeist.controller import userController
|
|
||||||
from flaschengeist.plugins import Plugin, before_update_user
|
from flaschengeist.plugins import Plugin, before_update_user
|
||||||
|
|
||||||
from . import balance_controller, permissions, models
|
from . import permissions, models
|
||||||
|
|
||||||
balance_bp = Blueprint("balance", __name__)
|
|
||||||
|
|
||||||
|
|
||||||
class BalancePlugin(Plugin):
|
class BalancePlugin(Plugin):
|
||||||
|
name = "balance"
|
||||||
|
blueprint = Blueprint(name, __name__)
|
||||||
|
permissions = permissions.permissions
|
||||||
|
plugin = LocalProxy(lambda: current_app.config["FG_PLUGINS"][BalancePlugin.name])
|
||||||
models = models
|
models = models
|
||||||
|
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
super().__init__(blueprint=balance_bp, permissions=permissions.permissions)
|
super(BalancePlugin, self).__init__(config)
|
||||||
|
from . import routes, balance_controller
|
||||||
|
|
||||||
@before_update_user
|
@before_update_user
|
||||||
def set_default_limit(user):
|
def set_default_limit(user):
|
||||||
|
@ -36,277 +31,3 @@ class BalancePlugin(Plugin):
|
||||||
balance_controller.set_limit(user, limit, override=False)
|
balance_controller.set_limit(user, limit, override=False)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def install(self):
|
|
||||||
from flaschengeist.database import db
|
|
||||||
|
|
||||||
db.create_all()
|
|
||||||
|
|
||||||
|
|
||||||
def str2bool(string: str):
|
|
||||||
if string.lower() in ["true", "yes", "1"]:
|
|
||||||
return True
|
|
||||||
elif string.lower() in ["false", "no", "0"]:
|
|
||||||
return False
|
|
||||||
raise ValueError
|
|
||||||
|
|
||||||
|
|
||||||
@balance_bp.route("/users/<userid>/balance/shortcuts", methods=["GET", "PUT"])
|
|
||||||
@login_required()
|
|
||||||
def get_shortcuts(userid, current_session: Session):
|
|
||||||
"""Get balance shortcuts of an user
|
|
||||||
|
|
||||||
Route: ``/users/<userid>/balance/shortcuts`` | Method: ``GET`` or ``PUT``
|
|
||||||
POST-data: On ``PUT`` json encoded array of floats
|
|
||||||
|
|
||||||
Args:
|
|
||||||
userid: Userid identifying the user
|
|
||||||
current_session: Session sent with Authorization Header
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
GET: JSON object containing the shortcuts as float array or HTTP error
|
|
||||||
PUT: HTTP-created or HTTP error
|
|
||||||
"""
|
|
||||||
if userid != current_session.user_.userid:
|
|
||||||
raise Forbidden
|
|
||||||
|
|
||||||
user = userController.get_user(userid)
|
|
||||||
if request.method == "GET":
|
|
||||||
return jsonify(user.get_attribute("balance_shortcuts", []))
|
|
||||||
else:
|
|
||||||
data = request.get_json()
|
|
||||||
if not isinstance(data, list) or not all(isinstance(n, (int, float)) for n in data):
|
|
||||||
raise BadRequest
|
|
||||||
data.sort(reverse=True)
|
|
||||||
user.set_attribute("balance_shortcuts", data)
|
|
||||||
userController.persist()
|
|
||||||
return no_content()
|
|
||||||
|
|
||||||
|
|
||||||
@balance_bp.route("/users/<userid>/balance/limit", methods=["GET"])
|
|
||||||
@login_required()
|
|
||||||
def get_limit(userid, current_session: Session):
|
|
||||||
"""Get limit of an user
|
|
||||||
|
|
||||||
Route: ``/users/<userid>/balance/limit`` | Method: ``GET``
|
|
||||||
|
|
||||||
Args:
|
|
||||||
userid: Userid identifying the user
|
|
||||||
current_session: Session sent with Authorization Header
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
JSON object containing the limit (or Null if no limit set) or HTTP error
|
|
||||||
"""
|
|
||||||
user = userController.get_user(userid)
|
|
||||||
if (user != current_session.user_ and not current_session.user_.has_permission(permissions.SET_LIMIT)) or (
|
|
||||||
user == current_session.user_ and not user.has_permission(permissions.SHOW)
|
|
||||||
):
|
|
||||||
raise Forbidden
|
|
||||||
|
|
||||||
return {"limit": balance_controller.get_limit(user)}
|
|
||||||
|
|
||||||
|
|
||||||
@balance_bp.route("/users/<userid>/balance/limit", methods=["PUT"])
|
|
||||||
@login_required(permissions.SET_LIMIT)
|
|
||||||
def set_limit(userid, current_session: Session):
|
|
||||||
"""Set the limit of an user
|
|
||||||
|
|
||||||
Route: ``/users/<userid>/balance/limit`` | Method: ``PUT``
|
|
||||||
|
|
||||||
POST-data: ``{limit: float}``
|
|
||||||
|
|
||||||
Args:
|
|
||||||
userid: Userid identifying the user
|
|
||||||
current_session: Session sent with Authorization Header
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
HTTP-200 or HTTP error
|
|
||||||
"""
|
|
||||||
user = userController.get_user(userid)
|
|
||||||
data = request.get_json()
|
|
||||||
try:
|
|
||||||
limit = data["limit"]
|
|
||||||
except (TypeError, KeyError):
|
|
||||||
raise BadRequest
|
|
||||||
balance_controller.set_limit(user, limit)
|
|
||||||
return HTTP.no_content()
|
|
||||||
|
|
||||||
|
|
||||||
@balance_bp.route("/users/<userid>/balance", methods=["GET"])
|
|
||||||
@login_required(permission=permissions.SHOW)
|
|
||||||
def get_balance(userid, current_session: Session):
|
|
||||||
"""Get balance of user, optionally filtered
|
|
||||||
|
|
||||||
Route: ``/users/<userid>/balance`` | Method: ``GET``
|
|
||||||
|
|
||||||
GET-parameters: ```{from?: string, to?: string}```
|
|
||||||
|
|
||||||
Args:
|
|
||||||
userid: Userid of user to get balance from
|
|
||||||
current_session: Session sent with Authorization Header
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
JSON object containing credit, debit and balance or HTTP error
|
|
||||||
"""
|
|
||||||
if userid != current_session.user_.userid and not current_session.user_.has_permission(permissions.SHOW_OTHER):
|
|
||||||
raise Forbidden
|
|
||||||
|
|
||||||
# Might raise NotFound
|
|
||||||
user = userController.get_user(userid)
|
|
||||||
|
|
||||||
start = request.args.get("from")
|
|
||||||
if start:
|
|
||||||
start = from_iso_format(start)
|
|
||||||
else:
|
|
||||||
start = datetime.fromtimestamp(0, tz=timezone.utc)
|
|
||||||
|
|
||||||
end = request.args.get("to")
|
|
||||||
if end:
|
|
||||||
end = from_iso_format(end)
|
|
||||||
else:
|
|
||||||
end = datetime.now(tz=timezone.utc)
|
|
||||||
|
|
||||||
balance = balance_controller.get_balance(user, start, end)
|
|
||||||
return {"credit": balance[0], "debit": balance[1], "balance": balance[2]}
|
|
||||||
|
|
||||||
|
|
||||||
@balance_bp.route("/users/<userid>/balance/transactions", methods=["GET"])
|
|
||||||
@login_required(permission=permissions.SHOW)
|
|
||||||
def get_transactions(userid, current_session: Session):
|
|
||||||
"""Get transactions of user, optionally filtered
|
|
||||||
Returns also count of transactions if limit is set (e.g. just count with limit = 0)
|
|
||||||
|
|
||||||
Route: ``/users/<userid>/balance/transactions`` | Method: ``GET``
|
|
||||||
|
|
||||||
GET-parameters: ```{from?: string, to?: string, limit?: int, offset?: int}```
|
|
||||||
|
|
||||||
Args:
|
|
||||||
userid: Userid of user to get transactions from
|
|
||||||
current_session: Session sent with Authorization Header
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
JSON Object {transactions: Transaction[], count?: number} or HTTP error
|
|
||||||
"""
|
|
||||||
if userid != current_session.user_.userid and not current_session.user_.has_permission(permissions.SHOW_OTHER):
|
|
||||||
raise Forbidden
|
|
||||||
|
|
||||||
# Might raise NotFound
|
|
||||||
user = userController.get_user(userid)
|
|
||||||
|
|
||||||
start = request.args.get("from")
|
|
||||||
if start:
|
|
||||||
start = from_iso_format(start)
|
|
||||||
end = request.args.get("to")
|
|
||||||
if end:
|
|
||||||
end = from_iso_format(end)
|
|
||||||
show_reversals = request.args.get("showReversals", False)
|
|
||||||
show_cancelled = request.args.get("showCancelled", True)
|
|
||||||
limit = request.args.get("limit")
|
|
||||||
offset = request.args.get("offset")
|
|
||||||
try:
|
|
||||||
if limit is not None:
|
|
||||||
limit = int(limit)
|
|
||||||
if offset is not None:
|
|
||||||
offset = int(offset)
|
|
||||||
if not isinstance(show_reversals, bool):
|
|
||||||
show_reversals = str2bool(show_reversals)
|
|
||||||
if not isinstance(show_cancelled, bool):
|
|
||||||
show_cancelled = str2bool(show_cancelled)
|
|
||||||
except ValueError:
|
|
||||||
raise BadRequest
|
|
||||||
|
|
||||||
transactions, count = balance_controller.get_transactions(
|
|
||||||
user, start, end, limit, offset, show_reversal=show_reversals, show_cancelled=show_cancelled
|
|
||||||
)
|
|
||||||
return {"transactions": transactions, "count": count}
|
|
||||||
|
|
||||||
|
|
||||||
@balance_bp.route("/users/<userid>/balance", methods=["PUT"])
|
|
||||||
@login_required()
|
|
||||||
def change_balance(userid, current_session: Session):
|
|
||||||
"""Change balance of an user
|
|
||||||
If ``sender`` is preset in POST-data, the action is handled as a transfer from ``sender`` to user.
|
|
||||||
|
|
||||||
Route: ``/users/<userid>/balance`` | Method: ``PUT``
|
|
||||||
|
|
||||||
POST-data: ``{amount: float, sender: string}``
|
|
||||||
|
|
||||||
Args:
|
|
||||||
userid: userid identifying user to change balance
|
|
||||||
current_session: Session sent with Authorization Header
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
JSON encoded transaction (201) or HTTP error
|
|
||||||
"""
|
|
||||||
|
|
||||||
data = request.get_json()
|
|
||||||
try:
|
|
||||||
amount = data["amount"]
|
|
||||||
except (TypeError, KeyError):
|
|
||||||
raise BadRequest
|
|
||||||
|
|
||||||
sender = data.get("sender", None)
|
|
||||||
user = userController.get_user(userid)
|
|
||||||
|
|
||||||
if sender:
|
|
||||||
sender = userController.get_user(sender)
|
|
||||||
if sender == user:
|
|
||||||
raise BadRequest
|
|
||||||
|
|
||||||
if (sender == current_session.user_ and sender.has_permission(permissions.SEND)) or (
|
|
||||||
sender != current_session.user_ and current_session.user_.has_permission(permissions.SEND_OTHER)
|
|
||||||
):
|
|
||||||
return HTTP.created(balance_controller.send(sender, user, amount, current_session.user_))
|
|
||||||
|
|
||||||
elif (
|
|
||||||
amount < 0
|
|
||||||
and (
|
|
||||||
(user == current_session.user_ and user.has_permission(permissions.DEBIT_OWN))
|
|
||||||
or current_session.user_.has_permission(permissions.DEBIT)
|
|
||||||
)
|
|
||||||
) or (amount > 0 and current_session.user_.has_permission(permissions.CREDIT)):
|
|
||||||
return HTTP.created(balance_controller.change_balance(user, data["amount"], current_session.user_))
|
|
||||||
|
|
||||||
raise Forbidden
|
|
||||||
|
|
||||||
|
|
||||||
@balance_bp.route("/balance/<int:transaction_id>", methods=["DELETE"])
|
|
||||||
@login_required()
|
|
||||||
def reverse_transaction(transaction_id, current_session: Session):
|
|
||||||
"""Reverse a transaction
|
|
||||||
|
|
||||||
Route: ``/balance/<int:transaction_id>`` | Method: ``DELETE``
|
|
||||||
|
|
||||||
Args:
|
|
||||||
transaction_id: Identifier of the transaction
|
|
||||||
current_session: Session sent with Authorization Header
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
JSON encoded reversal (transaction) (201) or HTTP error
|
|
||||||
"""
|
|
||||||
|
|
||||||
transaction = balance_controller.get_transaction(transaction_id)
|
|
||||||
if current_session.user_.has_permission(permissions.REVERSAL) or (
|
|
||||||
transaction.sender_ == current_session.user_
|
|
||||||
and (datetime.now(tz=timezone.utc) - transaction.time).total_seconds() < 10
|
|
||||||
):
|
|
||||||
reversal = balance_controller.reverse_transaction(transaction, current_session.user_)
|
|
||||||
return HTTP.created(reversal)
|
|
||||||
raise Forbidden
|
|
||||||
|
|
||||||
|
|
||||||
@balance_bp.route("/balance", methods=["GET"])
|
|
||||||
@login_required(permission=permissions.SHOW_OTHER)
|
|
||||||
def get_balances(current_session: Session):
|
|
||||||
"""Get all balances
|
|
||||||
|
|
||||||
Route: ``/balance`` | Method: ``GET``
|
|
||||||
|
|
||||||
Args:
|
|
||||||
current_session: Session sent with Authorization Header
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
JSON Array containing credit, debit and userid for each user or HTTP error
|
|
||||||
"""
|
|
||||||
balances = balance_controller.get_balances()
|
|
||||||
return jsonify([{"userid": u, "credit": v[0], "debit": v[1]} for u, v in balances.items()])
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ from flaschengeist.database import db
|
||||||
from flaschengeist.models.user import User
|
from flaschengeist.models.user import User
|
||||||
|
|
||||||
from .models import Transaction
|
from .models import Transaction
|
||||||
from . import permissions
|
from . import permissions, BalancePlugin
|
||||||
|
|
||||||
__attribute_limit = "balance_limit"
|
__attribute_limit = "balance_limit"
|
||||||
|
|
||||||
|
@ -86,6 +86,10 @@ def send(sender: User, receiver, amount: float, author: User):
|
||||||
transaction = Transaction(sender_=sender, receiver_=receiver, amount=amount, author_=author)
|
transaction = Transaction(sender_=sender, receiver_=receiver, amount=amount, author_=author)
|
||||||
db.session.add(transaction)
|
db.session.add(transaction)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
if sender is not None and sender.id_ != author.id_:
|
||||||
|
BalancePlugin.plugin.notify(sender, "Neue Transaktion")
|
||||||
|
if receiver is not None and receiver.id_ != author.id_:
|
||||||
|
BalancePlugin.plugin.notify(receiver, "Neue Transaktion")
|
||||||
return transaction
|
return transaction
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -6,21 +6,21 @@ from sqlalchemy.ext.hybrid import hybrid_property
|
||||||
|
|
||||||
from flaschengeist.database import db
|
from flaschengeist.database import db
|
||||||
from flaschengeist.models.user import User
|
from flaschengeist.models.user import User
|
||||||
from flaschengeist.models import ModelSerializeMixin, UtcDateTime
|
from flaschengeist.models import ModelSerializeMixin, UtcDateTime, Serial
|
||||||
|
|
||||||
|
|
||||||
class Transaction(db.Model, ModelSerializeMixin):
|
class Transaction(db.Model, ModelSerializeMixin):
|
||||||
__tablename__ = "balance_transaction"
|
__tablename__ = "balance_transaction"
|
||||||
# Protected foreign key properties
|
# Protected foreign key properties
|
||||||
_receiver_id = db.Column("receiver_id", db.Integer, db.ForeignKey("user.id"))
|
_receiver_id = db.Column("receiver_id", Serial, db.ForeignKey("user.id"))
|
||||||
_sender_id = db.Column("sender_id", db.Integer, db.ForeignKey("user.id"))
|
_sender_id = db.Column("sender_id", Serial, db.ForeignKey("user.id"))
|
||||||
_author_id = db.Column("author_id", db.Integer, db.ForeignKey("user.id"), nullable=False)
|
_author_id = db.Column("author_id", Serial, db.ForeignKey("user.id"), nullable=False)
|
||||||
|
|
||||||
# Public and exported member
|
# Public and exported member
|
||||||
id: int = db.Column("id", db.Integer, primary_key=True)
|
id: int = db.Column("id", Serial, primary_key=True)
|
||||||
time: datetime = db.Column(UtcDateTime, nullable=False, default=UtcDateTime.current_utc)
|
time: datetime = db.Column(UtcDateTime, nullable=False, default=UtcDateTime.current_utc)
|
||||||
amount: float = db.Column(db.Numeric(precision=5, scale=2, asdecimal=False), nullable=False)
|
amount: float = db.Column(db.Numeric(precision=5, scale=2, asdecimal=False), nullable=False)
|
||||||
reversal_id: Optional[int] = db.Column(db.Integer, db.ForeignKey("balance_transaction.id"))
|
reversal_id: Optional[int] = db.Column(Serial, db.ForeignKey("balance_transaction.id"))
|
||||||
|
|
||||||
# Dummy properties used for JSON serialization (userid instead of full user)
|
# Dummy properties used for JSON serialization (userid instead of full user)
|
||||||
author_id: Optional[str] = None
|
author_id: Optional[str] = None
|
||||||
|
|
|
@ -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
|
Provides duty schedule / duty roster functions
|
||||||
"""
|
"""
|
||||||
from datetime import datetime, timedelta, timezone
|
from flask import Blueprint, current_app
|
||||||
from http.client import NO_CONTENT
|
from werkzeug.local import LocalProxy
|
||||||
from flask import Blueprint, request, jsonify
|
|
||||||
from werkzeug.exceptions import BadRequest, NotFound, Forbidden
|
|
||||||
|
|
||||||
from flaschengeist.plugins import Plugin
|
from flaschengeist.plugins import Plugin
|
||||||
from flaschengeist.models.session import Session
|
from . import permissions, models
|
||||||
from flaschengeist.utils.decorators import login_required
|
|
||||||
from flaschengeist.utils.datetime import from_iso_format
|
|
||||||
from flaschengeist.controller import userController
|
|
||||||
|
|
||||||
from . import event_controller, permissions
|
|
||||||
from . import models
|
|
||||||
from ...utils.HTTP import no_content
|
|
||||||
|
|
||||||
events_bp = Blueprint("events", __name__)
|
|
||||||
|
|
||||||
|
|
||||||
class EventPlugin(Plugin):
|
class EventPlugin(Plugin):
|
||||||
|
name = "events"
|
||||||
|
plugin = LocalProxy(lambda: current_app.config["FG_PLUGINS"][EventPlugin.name])
|
||||||
|
permissions = permissions.permissions
|
||||||
|
blueprint = Blueprint(name, __name__)
|
||||||
models = models
|
models = models
|
||||||
|
|
||||||
def __init__(self, config):
|
def __init__(self, cfg):
|
||||||
super().__init__(
|
super(EventPlugin, self).__init__(cfg)
|
||||||
blueprint=events_bp,
|
from . import routes
|
||||||
permissions=permissions.permissions,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@events_bp.route("/events/templates", methods=["GET"])
|
|
||||||
@login_required()
|
|
||||||
def get_templates(current_session):
|
|
||||||
return jsonify(event_controller.get_templates())
|
|
||||||
|
|
||||||
|
|
||||||
@events_bp.route("/events/event-types", methods=["GET"])
|
|
||||||
@events_bp.route("/events/event-types/<int:identifier>", methods=["GET"])
|
|
||||||
@login_required()
|
|
||||||
def get_event_types(current_session, identifier=None):
|
|
||||||
"""Get EventType(s)
|
|
||||||
|
|
||||||
Route: ``/events/event-types`` | Method: ``GET``
|
|
||||||
Route: ``/events/event-types/<identifier>`` | Method: ``GET``
|
|
||||||
|
|
||||||
Args:
|
|
||||||
current_session: Session sent with Authorization Header
|
|
||||||
identifier: If querying a specific EventType
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
JSON encoded (list of) EventType(s) or HTTP-error
|
|
||||||
"""
|
|
||||||
if identifier:
|
|
||||||
result = event_controller.get_event_type(identifier)
|
|
||||||
else:
|
|
||||||
result = event_controller.get_event_types()
|
|
||||||
return jsonify(result)
|
|
||||||
|
|
||||||
|
|
||||||
@events_bp.route("/events/event-types", methods=["POST"])
|
|
||||||
@login_required(permission=permissions.EVENT_TYPE)
|
|
||||||
def new_event_type(current_session):
|
|
||||||
"""Create a new EventType
|
|
||||||
|
|
||||||
Route: ``/events/event-types`` | Method: ``POST``
|
|
||||||
|
|
||||||
POST-data: ``{name: string}``
|
|
||||||
|
|
||||||
Args:
|
|
||||||
current_session: Session sent with Authorization Header
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
HTTP-Created or HTTP-error
|
|
||||||
"""
|
|
||||||
data = request.get_json()
|
|
||||||
if "name" not in data:
|
|
||||||
raise BadRequest
|
|
||||||
event_type = event_controller.create_event_type(data["name"])
|
|
||||||
return jsonify(event_type)
|
|
||||||
|
|
||||||
|
|
||||||
@events_bp.route("/events/event-types/<int:identifier>", methods=["PUT", "DELETE"])
|
|
||||||
@login_required(permission=permissions.EVENT_TYPE)
|
|
||||||
def modify_event_type(identifier, current_session):
|
|
||||||
"""Rename or delete an event type
|
|
||||||
|
|
||||||
Route: ``/events/event-types/<id>`` | Method: ``PUT`` or ``DELETE``
|
|
||||||
|
|
||||||
POST-data: (if renaming) ``{name: string}``
|
|
||||||
|
|
||||||
Args:
|
|
||||||
identifier: Identifier of the EventType
|
|
||||||
current_session: Session sent with Authorization Header
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
HTTP-NoContent or HTTP-error
|
|
||||||
"""
|
|
||||||
if request.method == "DELETE":
|
|
||||||
event_controller.delete_event_type(identifier)
|
|
||||||
else:
|
|
||||||
data = request.get_json()
|
|
||||||
if "name" not in data:
|
|
||||||
raise BadRequest("Parameter missing in data")
|
|
||||||
event_controller.rename_event_type(identifier, data["name"])
|
|
||||||
return "", NO_CONTENT
|
|
||||||
|
|
||||||
|
|
||||||
@events_bp.route("/events/job-types", methods=["GET"])
|
|
||||||
@login_required()
|
|
||||||
def get_job_types(current_session):
|
|
||||||
"""Get all JobTypes
|
|
||||||
|
|
||||||
Route: ``/events/job-types`` | Method: ``GET``
|
|
||||||
|
|
||||||
Args:
|
|
||||||
current_session: Session sent with Authorization Header
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
JSON encoded list of JobType HTTP-error
|
|
||||||
"""
|
|
||||||
types = event_controller.get_job_types()
|
|
||||||
return jsonify(types)
|
|
||||||
|
|
||||||
|
|
||||||
@events_bp.route("/events/job-types", methods=["POST"])
|
|
||||||
@login_required(permission=permissions.JOB_TYPE)
|
|
||||||
def new_job_type(current_session):
|
|
||||||
"""Create a new JobType
|
|
||||||
|
|
||||||
Route: ``/events/job-types`` | Method: ``POST``
|
|
||||||
|
|
||||||
POST-data: ``{name: string}``
|
|
||||||
|
|
||||||
Args:
|
|
||||||
current_session: Session sent with Authorization Header
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
JSON encoded JobType or HTTP-error
|
|
||||||
"""
|
|
||||||
data = request.get_json()
|
|
||||||
if "name" not in data:
|
|
||||||
raise BadRequest
|
|
||||||
jt = event_controller.create_job_type(data["name"])
|
|
||||||
return jsonify(jt)
|
|
||||||
|
|
||||||
|
|
||||||
@events_bp.route("/events/job-types/<int:type_id>", methods=["PUT", "DELETE"])
|
|
||||||
@login_required(permission=permissions.JOB_TYPE)
|
|
||||||
def modify_job_type(type_id, current_session):
|
|
||||||
"""Rename or delete a JobType
|
|
||||||
|
|
||||||
Route: ``/events/job-types/<name>`` | Method: ``PUT`` or ``DELETE``
|
|
||||||
|
|
||||||
POST-data: (if renaming) ``{name: string}``
|
|
||||||
|
|
||||||
Args:
|
|
||||||
type_id: Identifier of the JobType
|
|
||||||
current_session: Session sent with Authorization Header
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
HTTP-NoContent or HTTP-error
|
|
||||||
"""
|
|
||||||
if request.method == "DELETE":
|
|
||||||
event_controller.delete_job_type(type_id)
|
|
||||||
else:
|
|
||||||
data = request.get_json()
|
|
||||||
if "name" not in data:
|
|
||||||
raise BadRequest("Parameter missing in data")
|
|
||||||
event_controller.rename_job_type(type_id, data["name"])
|
|
||||||
return "", NO_CONTENT
|
|
||||||
|
|
||||||
|
|
||||||
@events_bp.route("/events/<int:event_id>", methods=["GET"])
|
|
||||||
@login_required()
|
|
||||||
def get_event(event_id, current_session):
|
|
||||||
"""Get event by id
|
|
||||||
|
|
||||||
Route: ``/events/<event_id>`` | Method: ``GET``
|
|
||||||
|
|
||||||
Args:
|
|
||||||
event_id: ID identifying the event
|
|
||||||
current_session: Session sent with Authorization Header
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
JSON encoded event object
|
|
||||||
"""
|
|
||||||
event = event_controller.get_event(event_id)
|
|
||||||
return jsonify(event)
|
|
||||||
|
|
||||||
|
|
||||||
@events_bp.route("/events", methods=["GET"])
|
|
||||||
@login_required()
|
|
||||||
def get_filtered_events(current_session):
|
|
||||||
begin = request.args.get("from")
|
|
||||||
if begin is not None:
|
|
||||||
begin = from_iso_format(begin)
|
|
||||||
end = request.args.get("to")
|
|
||||||
if end is not None:
|
|
||||||
end = from_iso_format(end)
|
|
||||||
if begin is None and end is None:
|
|
||||||
begin = datetime.now()
|
|
||||||
return jsonify(event_controller.get_events(begin, end))
|
|
||||||
|
|
||||||
|
|
||||||
@events_bp.route("/events/<int:year>/<int:month>", methods=["GET"])
|
|
||||||
@events_bp.route("/events/<int:year>/<int:month>/<int:day>", methods=["GET"])
|
|
||||||
@login_required()
|
|
||||||
def get_events(current_session, year=datetime.now().year, month=datetime.now().month, day=None):
|
|
||||||
"""Get Event objects for specified date (or month or year),
|
|
||||||
if nothing set then events for current month are returned
|
|
||||||
|
|
||||||
Route: ``/events[/<year>/<month>[/<int:day>]]`` | Method: ``GET``
|
|
||||||
|
|
||||||
Args:
|
|
||||||
year (int, optional): year to query, defaults to current year
|
|
||||||
month (int, optional): month to query (if set), defaults to current month
|
|
||||||
day (int, optional): day to query events for (if set)
|
|
||||||
current_session: Session sent with Authorization Header
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
JSON encoded list containing events found or HTTP-error
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
begin = datetime(year=year, month=month, day=1, tzinfo=timezone.utc)
|
|
||||||
if day:
|
|
||||||
begin += timedelta(days=day - 1)
|
|
||||||
end = begin + timedelta(days=1)
|
|
||||||
else:
|
|
||||||
if month == 12:
|
|
||||||
end = datetime(year=year + 1, month=1, day=1, tzinfo=timezone.utc)
|
|
||||||
else:
|
|
||||||
end = datetime(year=year, month=month + 1, day=1, tzinfo=timezone.utc)
|
|
||||||
|
|
||||||
events = event_controller.get_events(begin, end)
|
|
||||||
return jsonify(events)
|
|
||||||
except ValueError:
|
|
||||||
raise BadRequest("Invalid date given")
|
|
||||||
|
|
||||||
|
|
||||||
def _add_job(event, data):
|
|
||||||
try:
|
|
||||||
start = from_iso_format(data["start"])
|
|
||||||
end = None
|
|
||||||
if "end" in data:
|
|
||||||
end = from_iso_format(data["end"])
|
|
||||||
required_services = data["required_services"]
|
|
||||||
job_type = data["type"]
|
|
||||||
if isinstance(job_type, dict):
|
|
||||||
job_type = data["type"]["id"]
|
|
||||||
except (KeyError, ValueError):
|
|
||||||
raise BadRequest("Missing or invalid POST parameter")
|
|
||||||
|
|
||||||
job_type = event_controller.get_job_type(job_type)
|
|
||||||
event_controller.add_job(event, job_type, required_services, start, end, comment=data.get("comment", None))
|
|
||||||
|
|
||||||
|
|
||||||
@events_bp.route("/events", methods=["POST"])
|
|
||||||
@login_required(permission=permissions.CREATE)
|
|
||||||
def create_event(current_session):
|
|
||||||
"""Create an new event
|
|
||||||
|
|
||||||
Route: ``/events`` | Method: ``POST``
|
|
||||||
|
|
||||||
POST-data: See interfaces for Event, can already contain jobs
|
|
||||||
|
|
||||||
Args:
|
|
||||||
current_session: Session sent with Authorization Header
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
JSON encoded Event object or HTTP-error
|
|
||||||
"""
|
|
||||||
data = request.get_json()
|
|
||||||
end = data.get("end", None)
|
|
||||||
try:
|
|
||||||
start = from_iso_format(data["start"])
|
|
||||||
if end is not None:
|
|
||||||
end = from_iso_format(end)
|
|
||||||
data_type = data["type"]
|
|
||||||
if isinstance(data_type, dict):
|
|
||||||
data_type = data["type"]["id"]
|
|
||||||
event_type = event_controller.get_event_type(data_type)
|
|
||||||
except KeyError:
|
|
||||||
raise BadRequest("Missing POST parameter")
|
|
||||||
except (NotFound, ValueError):
|
|
||||||
raise BadRequest("Invalid parameter")
|
|
||||||
|
|
||||||
event = event_controller.create_event(
|
|
||||||
start=start,
|
|
||||||
end=end,
|
|
||||||
name=data.get("name", None),
|
|
||||||
is_template=data.get("is_template", None),
|
|
||||||
event_type=event_type,
|
|
||||||
description=data.get("description", None),
|
|
||||||
)
|
|
||||||
if "jobs" in data:
|
|
||||||
for job in data["jobs"]:
|
|
||||||
_add_job(event, job)
|
|
||||||
|
|
||||||
return jsonify(event)
|
|
||||||
|
|
||||||
|
|
||||||
@events_bp.route("/events/<int:event_id>", methods=["PUT"])
|
|
||||||
@login_required(permission=permissions.EDIT)
|
|
||||||
def modify_event(event_id, current_session):
|
|
||||||
"""Modify an event
|
|
||||||
|
|
||||||
Route: ``/events/<event_id>`` | Method: ``PUT``
|
|
||||||
|
|
||||||
POST-data: See interfaces for Event, can already contain slots
|
|
||||||
|
|
||||||
Args:
|
|
||||||
event_id: Identifier of the event
|
|
||||||
current_session: Session sent with Authorization Header
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
JSON encoded Event object or HTTP-error
|
|
||||||
"""
|
|
||||||
event = event_controller.get_event(event_id)
|
|
||||||
data = request.get_json()
|
|
||||||
if "start" in data:
|
|
||||||
event.start = from_iso_format(data["start"])
|
|
||||||
if "end" in data:
|
|
||||||
event.end = from_iso_format(data["end"])
|
|
||||||
if "description" in data:
|
|
||||||
event.description = data["description"]
|
|
||||||
if "type" in data:
|
|
||||||
event_type = event_controller.get_event_type(data["type"])
|
|
||||||
event.type = event_type
|
|
||||||
event_controller.update()
|
|
||||||
return jsonify(event)
|
|
||||||
|
|
||||||
|
|
||||||
@events_bp.route("/events/<int:event_id>", methods=["DELETE"])
|
|
||||||
@login_required(permission=permissions.DELETE)
|
|
||||||
def delete_event(event_id, current_session):
|
|
||||||
"""Delete an event
|
|
||||||
|
|
||||||
Route: ``/events/<event_id>`` | Method: ``DELETE``
|
|
||||||
|
|
||||||
Args:
|
|
||||||
event_id: Identifier of the event
|
|
||||||
current_session: Session sent with Authorization Header
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
HTTP-NoContent or HTTP-error
|
|
||||||
"""
|
|
||||||
event_controller.delete_event(event_id)
|
|
||||||
return "", NO_CONTENT
|
|
||||||
|
|
||||||
|
|
||||||
@events_bp.route("/events/<int:event_id>/jobs", methods=["POST"])
|
|
||||||
@login_required(permission=permissions.EDIT)
|
|
||||||
def add_job(event_id, current_session):
|
|
||||||
"""Add an new Job to an Event / EventSlot
|
|
||||||
|
|
||||||
Route: ``/events/<event_id>/jobs`` | Method: ``POST``
|
|
||||||
|
|
||||||
POST-data: See Job
|
|
||||||
|
|
||||||
Args:
|
|
||||||
event_id: Identifier of the event
|
|
||||||
current_session: Session sent with Authorization Header
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
JSON encoded Event object or HTTP-error
|
|
||||||
"""
|
|
||||||
event = event_controller.get_event(event_id)
|
|
||||||
_add_job(event, request.get_json())
|
|
||||||
return jsonify(event)
|
|
||||||
|
|
||||||
|
|
||||||
@events_bp.route("/events/<int:event_id>/jobs/<int:job_id>", methods=["DELETE"])
|
|
||||||
@login_required(permission=permissions.DELETE)
|
|
||||||
def delete_job(event_id, job_id, current_session):
|
|
||||||
"""Delete a Job
|
|
||||||
|
|
||||||
Route: ``/events/<event_id>/jobs/<job_id>`` | Method: ``DELETE``
|
|
||||||
|
|
||||||
Args:
|
|
||||||
event_id: Identifier of the event
|
|
||||||
job_id: Identifier of the Job
|
|
||||||
current_session: Session sent with Authorization Header
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
HTTP-no-content or HTTP error
|
|
||||||
"""
|
|
||||||
job_slot = event_controller.get_job(job_id, event_id)
|
|
||||||
event_controller.delete_job(job_slot)
|
|
||||||
return no_content()
|
|
||||||
|
|
||||||
|
|
||||||
@events_bp.route("/events/<int:event_id>/jobs/<int:job_id>", methods=["PUT"])
|
|
||||||
@login_required()
|
|
||||||
def update_job(event_id, job_id, current_session: Session):
|
|
||||||
"""Edit Job or assign user to the Job
|
|
||||||
|
|
||||||
Route: ``/events/<event_id>/jobs/<job_id>`` | Method: ``PUT``
|
|
||||||
|
|
||||||
POST-data: See TS interface for Job or ``{user: {userid: string, value: number}}``
|
|
||||||
|
|
||||||
Args:
|
|
||||||
event_id: Identifier of the event
|
|
||||||
job_id: Identifier of the Job
|
|
||||||
current_session: Session sent with Authorization Header
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
JSON encoded Job object or HTTP-error
|
|
||||||
"""
|
|
||||||
job = event_controller.get_job(job_id, event_id)
|
|
||||||
|
|
||||||
data = request.get_json()
|
|
||||||
if not data:
|
|
||||||
raise BadRequest
|
|
||||||
|
|
||||||
if ("user" not in data or len(data) > 1) and not current_session.user_.has_permission(permissions.EDIT):
|
|
||||||
raise Forbidden
|
|
||||||
|
|
||||||
if "user" in data:
|
|
||||||
try:
|
|
||||||
user = userController.get_user(data["user"]["userid"])
|
|
||||||
value = data["user"]["value"]
|
|
||||||
if (user == current_session.user_ and not user.has_permission(permissions.ASSIGN)) or (
|
|
||||||
user != current_session.user_ and not current_session.user_.has_permission(permissions.ASSIGN_OTHER)
|
|
||||||
):
|
|
||||||
raise Forbidden
|
|
||||||
event_controller.assign_to_job(job, user, value)
|
|
||||||
except (KeyError, ValueError):
|
|
||||||
raise BadRequest
|
|
||||||
|
|
||||||
if "required_services" in data:
|
|
||||||
job.required_services = data["required_services"]
|
|
||||||
if "type" in data:
|
|
||||||
job.type = event_controller.get_job_type(data["type"])
|
|
||||||
event_controller.update()
|
|
||||||
|
|
||||||
return jsonify(job)
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: JobTransfer
|
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta, timezone
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from sqlalchemy import or_, and_
|
|
||||||
from werkzeug.exceptions import BadRequest, NotFound
|
from werkzeug.exceptions import BadRequest, NotFound
|
||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
|
||||||
from flaschengeist import logger
|
from flaschengeist import logger
|
||||||
from flaschengeist.database import db
|
from flaschengeist.database import db
|
||||||
|
from flaschengeist.plugins.events import EventPlugin
|
||||||
from flaschengeist.plugins.events.models import EventType, Event, Job, JobType, Service
|
from flaschengeist.plugins.events.models import EventType, Event, Job, JobType, Service
|
||||||
from flaschengeist.utils.datetime import from_iso_format
|
from flaschengeist.utils.scheduler import scheduled
|
||||||
|
|
||||||
|
|
||||||
def update():
|
def update():
|
||||||
|
@ -102,10 +102,21 @@ def delete_job_type(name):
|
||||||
raise BadRequest("Type still in use")
|
raise BadRequest("Type still in use")
|
||||||
|
|
||||||
|
|
||||||
def get_event(event_id) -> Event:
|
def clear_backup(event: Event):
|
||||||
|
for job in event.jobs:
|
||||||
|
services = []
|
||||||
|
for service in job.services:
|
||||||
|
if not service.is_backup:
|
||||||
|
services.append(service)
|
||||||
|
job.services = services
|
||||||
|
|
||||||
|
|
||||||
|
def get_event(event_id, with_backup=False) -> Event:
|
||||||
event = Event.query.get(event_id)
|
event = Event.query.get(event_id)
|
||||||
if event is None:
|
if event is None:
|
||||||
raise NotFound
|
raise NotFound
|
||||||
|
if not with_backup:
|
||||||
|
return clear_backup(event)
|
||||||
return event
|
return event
|
||||||
|
|
||||||
|
|
||||||
|
@ -113,11 +124,12 @@ def get_templates():
|
||||||
return Event.query.filter(Event.is_template == True).all()
|
return Event.query.filter(Event.is_template == True).all()
|
||||||
|
|
||||||
|
|
||||||
def get_events(start: Optional[datetime] = None, end=None):
|
def get_events(start: Optional[datetime] = None, end=None, with_backup=False):
|
||||||
"""Query events which start from begin until end
|
"""Query events which start from begin until end
|
||||||
Args:
|
Args:
|
||||||
start (datetime): Earliest start
|
start (datetime): Earliest start
|
||||||
end (datetime): Latest start
|
end (datetime): Latest start
|
||||||
|
with_backup (bool): Export also backup services
|
||||||
|
|
||||||
Returns: collection of Event objects
|
Returns: collection of Event objects
|
||||||
"""
|
"""
|
||||||
|
@ -126,7 +138,11 @@ def get_events(start: Optional[datetime] = None, end=None):
|
||||||
query = query.filter(start <= Event.start)
|
query = query.filter(start <= Event.start)
|
||||||
if end is not None:
|
if end is not None:
|
||||||
query = query.filter(Event.start < end)
|
query = query.filter(Event.start < end)
|
||||||
return query.all()
|
events = query.all()
|
||||||
|
if not with_backup:
|
||||||
|
for event in events:
|
||||||
|
clear_backup(event)
|
||||||
|
return events
|
||||||
|
|
||||||
|
|
||||||
def delete_event(event_id):
|
def delete_event(event_id):
|
||||||
|
@ -202,3 +218,26 @@ def assign_to_job(job: Job, user, value):
|
||||||
service = Service(user_=user, value=value, job_=job)
|
service = Service(user_=user, value=value, job_=job)
|
||||||
db.session.add(service)
|
db.session.add(service)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
@scheduled
|
||||||
|
def assign_backups():
|
||||||
|
logger.debug("Notifications")
|
||||||
|
now = datetime.now(tz=timezone.utc)
|
||||||
|
# now + backup_time + next cron tick
|
||||||
|
start = now + timedelta(hours=16) + timedelta(minutes=30)
|
||||||
|
services = Service.query.filter(Service.is_backup == True).join(Job).filter(Job.start <= start).all()
|
||||||
|
for service in services:
|
||||||
|
if service.job_.start <= now or service.job_.is_full():
|
||||||
|
EventPlugin.plugin.notify(
|
||||||
|
service.user_, "Your backup assignment was cancelled.", {"event_id": service.job_.event_id_}
|
||||||
|
)
|
||||||
|
logger.debug(f"Service is outdated or full, removing. {service.serialize()}")
|
||||||
|
db.session.delete(service)
|
||||||
|
else:
|
||||||
|
service.is_backup = False
|
||||||
|
logger.debug(f"Service not full, assigning backup. {service.serialize()}")
|
||||||
|
EventPlugin.plugin.notify(
|
||||||
|
service.user_, "Your backup assignment was accepted.", {"event_id": service.job_.event_id_}
|
||||||
|
)
|
||||||
|
db.session.commit()
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
from __future__ import annotations # TODO: Remove if python requirement is >= 3.10
|
from __future__ import annotations # TODO: Remove if python requirement is >= 3.10
|
||||||
|
|
||||||
import enum
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional, Union
|
from typing import Optional, Union
|
||||||
|
|
||||||
from sqlalchemy import UniqueConstraint
|
from sqlalchemy import UniqueConstraint
|
||||||
|
|
||||||
from flaschengeist.models import ModelSerializeMixin, UtcDateTime
|
from flaschengeist.models import ModelSerializeMixin, UtcDateTime, Serial
|
||||||
from flaschengeist.models.user import User
|
from flaschengeist.models.user import User
|
||||||
from flaschengeist.database import db
|
from flaschengeist.database import db
|
||||||
|
|
||||||
|
@ -19,13 +18,13 @@ _table_prefix_ = "events_"
|
||||||
|
|
||||||
class EventType(db.Model, ModelSerializeMixin):
|
class EventType(db.Model, ModelSerializeMixin):
|
||||||
__tablename__ = _table_prefix_ + "event_type"
|
__tablename__ = _table_prefix_ + "event_type"
|
||||||
id: int = db.Column(db.Integer, primary_key=True)
|
id: int = db.Column(Serial, primary_key=True)
|
||||||
name: str = db.Column(db.String(30), nullable=False, unique=True)
|
name: str = db.Column(db.String(30), nullable=False, unique=True)
|
||||||
|
|
||||||
|
|
||||||
class JobType(db.Model, ModelSerializeMixin):
|
class JobType(db.Model, ModelSerializeMixin):
|
||||||
__tablename__ = _table_prefix_ + "job_type"
|
__tablename__ = _table_prefix_ + "job_type"
|
||||||
id: int = db.Column(db.Integer, primary_key=True)
|
id: int = db.Column(Serial, primary_key=True)
|
||||||
name: str = db.Column(db.String(30), nullable=False, unique=True)
|
name: str = db.Column(db.String(30), nullable=False, unique=True)
|
||||||
|
|
||||||
|
|
||||||
|
@ -37,12 +36,11 @@ class JobType(db.Model, ModelSerializeMixin):
|
||||||
class Service(db.Model, ModelSerializeMixin):
|
class Service(db.Model, ModelSerializeMixin):
|
||||||
__tablename__ = _table_prefix_ + "service"
|
__tablename__ = _table_prefix_ + "service"
|
||||||
userid: str = ""
|
userid: str = ""
|
||||||
|
is_backup: bool = db.Column(db.Boolean, default=False)
|
||||||
value: float = db.Column(db.Numeric(precision=3, scale=2, asdecimal=False), nullable=False)
|
value: float = db.Column(db.Numeric(precision=3, scale=2, asdecimal=False), nullable=False)
|
||||||
|
|
||||||
_job_id = db.Column(
|
_job_id = db.Column("job_id", Serial, db.ForeignKey(f"{_table_prefix_}job.id"), nullable=False, primary_key=True)
|
||||||
"job_id", db.Integer, db.ForeignKey(f"{_table_prefix_}job.id"), nullable=False, primary_key=True
|
_user_id = db.Column("user_id", Serial, db.ForeignKey("user.id"), nullable=False, primary_key=True)
|
||||||
)
|
|
||||||
_user_id = db.Column("user_id", db.Integer, db.ForeignKey("user.id"), nullable=False, primary_key=True)
|
|
||||||
|
|
||||||
user_: User = db.relationship("User")
|
user_: User = db.relationship("User")
|
||||||
job_: Job = db.relationship("Job")
|
job_: Job = db.relationship("Job")
|
||||||
|
@ -54,9 +52,9 @@ class Service(db.Model, ModelSerializeMixin):
|
||||||
|
|
||||||
class Job(db.Model, ModelSerializeMixin):
|
class Job(db.Model, ModelSerializeMixin):
|
||||||
__tablename__ = _table_prefix_ + "job"
|
__tablename__ = _table_prefix_ + "job"
|
||||||
_type_id = db.Column("type_id", db.Integer, db.ForeignKey(f"{_table_prefix_}job_type.id"), nullable=False)
|
_type_id = db.Column("type_id", Serial, db.ForeignKey(f"{_table_prefix_}job_type.id"), nullable=False)
|
||||||
|
|
||||||
id: int = db.Column(db.Integer, primary_key=True)
|
id: int = db.Column(Serial, primary_key=True)
|
||||||
start: datetime = db.Column(UtcDateTime, nullable=False)
|
start: datetime = db.Column(UtcDateTime, nullable=False)
|
||||||
end: Optional[datetime] = db.Column(UtcDateTime)
|
end: Optional[datetime] = db.Column(UtcDateTime)
|
||||||
type: Union[JobType, int] = db.relationship("JobType")
|
type: Union[JobType, int] = db.relationship("JobType")
|
||||||
|
@ -65,7 +63,7 @@ class Job(db.Model, ModelSerializeMixin):
|
||||||
required_services: float = db.Column(db.Numeric(precision=4, scale=2, asdecimal=False), nullable=False)
|
required_services: float = db.Column(db.Numeric(precision=4, scale=2, asdecimal=False), nullable=False)
|
||||||
|
|
||||||
event_ = db.relationship("Event", back_populates="jobs")
|
event_ = db.relationship("Event", back_populates="jobs")
|
||||||
event_id_ = db.Column("event_id", db.Integer, db.ForeignKey(f"{_table_prefix_}event.id"), nullable=False)
|
event_id_ = db.Column("event_id", Serial, db.ForeignKey(f"{_table_prefix_}event.id"), nullable=False)
|
||||||
|
|
||||||
__table_args__ = (UniqueConstraint("type_id", "start", "event_id", name="_type_start_uc"),)
|
__table_args__ = (UniqueConstraint("type_id", "start", "event_id", name="_type_start_uc"),)
|
||||||
|
|
||||||
|
@ -77,7 +75,7 @@ class Event(db.Model, ModelSerializeMixin):
|
||||||
"""Model for an Event"""
|
"""Model for an Event"""
|
||||||
|
|
||||||
__tablename__ = _table_prefix_ + "event"
|
__tablename__ = _table_prefix_ + "event"
|
||||||
id: int = db.Column(db.Integer, primary_key=True)
|
id: int = db.Column(Serial, primary_key=True)
|
||||||
start: datetime = db.Column(UtcDateTime, nullable=False)
|
start: datetime = db.Column(UtcDateTime, nullable=False)
|
||||||
end: Optional[datetime] = db.Column(UtcDateTime)
|
end: Optional[datetime] = db.Column(UtcDateTime)
|
||||||
name: Optional[str] = db.Column(db.String(255))
|
name: Optional[str] = db.Column(db.String(255))
|
||||||
|
@ -89,15 +87,15 @@ class Event(db.Model, ModelSerializeMixin):
|
||||||
)
|
)
|
||||||
# Protected for internal use
|
# Protected for internal use
|
||||||
_type_id = db.Column(
|
_type_id = db.Column(
|
||||||
"type_id", db.Integer, db.ForeignKey(f"{_table_prefix_}event_type.id", ondelete="CASCADE"), nullable=False
|
"type_id", Serial, db.ForeignKey(f"{_table_prefix_}event_type.id", ondelete="CASCADE"), nullable=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Invite(db.Model, ModelSerializeMixin):
|
class Invite(db.Model, ModelSerializeMixin):
|
||||||
__tablename__ = _table_prefix_ + "invite"
|
__tablename__ = _table_prefix_ + "invite"
|
||||||
|
|
||||||
id: int = db.Column(db.Integer, primary_key=True)
|
id: int = db.Column(Serial, primary_key=True)
|
||||||
job_id: int = db.Column(db.Integer, db.ForeignKey(_table_prefix_ + "job.id"), nullable=False)
|
job_id: int = db.Column(Serial, db.ForeignKey(_table_prefix_ + "job.id"), nullable=False)
|
||||||
# Dummy properties for API export
|
# Dummy properties for API export
|
||||||
invitee_id: str = None
|
invitee_id: str = None
|
||||||
sender_id: str = None
|
sender_id: str = None
|
||||||
|
@ -105,8 +103,8 @@ class Invite(db.Model, ModelSerializeMixin):
|
||||||
invitee_: User = db.relationship("User", foreign_keys="Invite._invitee_id")
|
invitee_: User = db.relationship("User", foreign_keys="Invite._invitee_id")
|
||||||
sender_: User = db.relationship("User", foreign_keys="Invite._sender_id")
|
sender_: User = db.relationship("User", foreign_keys="Invite._sender_id")
|
||||||
# Protected properties needed for internal use
|
# Protected properties needed for internal use
|
||||||
_invitee_id = db.Column("invitee_id", db.Integer, db.ForeignKey("user.id"), nullable=False)
|
_invitee_id = db.Column("invitee_id", Serial, db.ForeignKey("user.id"), nullable=False)
|
||||||
_sender_id = db.Column("sender_id", db.Integer, db.ForeignKey("user.id"), nullable=False)
|
_sender_id = db.Column("sender_id", Serial, db.ForeignKey("user.id"), nullable=False)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def invitee_id(self):
|
def invitee_id(self):
|
||||||
|
|
|
@ -1,22 +1,25 @@
|
||||||
CREATE = "schedule_create"
|
CREATE = "events_create"
|
||||||
"""Can create events"""
|
"""Can create events"""
|
||||||
|
|
||||||
EDIT = "schedule_edit"
|
EDIT = "events_edit"
|
||||||
"""Can edit events"""
|
"""Can edit events"""
|
||||||
|
|
||||||
DELETE = "schedule_delete"
|
DELETE = "events_delete"
|
||||||
"""Can delete events"""
|
"""Can delete events"""
|
||||||
|
|
||||||
EVENT_TYPE = "schedule_event_type"
|
EVENT_TYPE = "events_event_type"
|
||||||
"""Can create and edit EventTypes"""
|
"""Can create and edit EventTypes"""
|
||||||
|
|
||||||
JOB_TYPE = "schedule_job_type"
|
JOB_TYPE = "events_job_type"
|
||||||
"""Can create and edit JobTypes"""
|
"""Can create and edit JobTypes"""
|
||||||
|
|
||||||
ASSIGN = "schedule_assign"
|
ASSIGN = "events_assign"
|
||||||
"""Can self assign to jobs"""
|
"""Can self assign to jobs"""
|
||||||
|
|
||||||
ASSIGN_OTHER = "schedule_assign_other"
|
ASSIGN_OTHER = "events_assign_other"
|
||||||
"""Can assign other users to jobs"""
|
"""Can assign other users to jobs"""
|
||||||
|
|
||||||
|
SEE_BACKUP = "events_see_backup"
|
||||||
|
"""Can see users assigned as backup"""
|
||||||
|
|
||||||
permissions = [value for key, value in globals().items() if not key.startswith("_")]
|
permissions = [value for key, value in globals().items() if not key.startswith("_")]
|
||||||
|
|
|
@ -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"""
|
"""Pricelist plugin"""
|
||||||
|
|
||||||
from flask import Blueprint, jsonify, request
|
from flask import Blueprint, jsonify, request, current_app
|
||||||
from http.client import NO_CONTENT
|
from werkzeug.local import LocalProxy
|
||||||
|
from werkzeug.exceptions import BadRequest, Forbidden, Unauthorized
|
||||||
|
|
||||||
from flaschengeist.plugins import Plugin
|
from flaschengeist.plugins import Plugin
|
||||||
from flaschengeist.utils.decorators import login_required,extract_session
|
from flaschengeist.utils.decorators import login_required, extract_session
|
||||||
from werkzeug.exceptions import BadRequest, Forbidden, Unauthorized
|
from flaschengeist.utils.HTTP import no_content
|
||||||
from flaschengeist.config import config
|
from flaschengeist.models.session import Session
|
||||||
|
from flaschengeist.controller import userController
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
from . import pricelist_controller, permissions
|
from . import pricelist_controller, permissions
|
||||||
from ...controller import userController
|
|
||||||
from ...models.session import Session
|
|
||||||
from ...utils.HTTP import no_content
|
|
||||||
|
|
||||||
pricelist_bp = Blueprint("pricelist", __name__, url_prefix="/pricelist")
|
|
||||||
|
|
||||||
|
|
||||||
class PriceListPlugin(Plugin):
|
class PriceListPlugin(Plugin):
|
||||||
|
name = "pricelist"
|
||||||
|
blueprint = Blueprint(name, __name__, url_prefix="/pricelist")
|
||||||
|
plugin = LocalProxy(lambda: current_app.config["FG_PLUGINS"][PriceListPlugin.name])
|
||||||
models = models
|
models = models
|
||||||
|
|
||||||
def __init__(self, cfg):
|
def __init__(self, cfg):
|
||||||
super().__init__(blueprint=pricelist_bp, permissions=permissions.permissions)
|
super().__init__(cfg)
|
||||||
config = {"discount": 0}
|
config = {"discount": 0}
|
||||||
config.update(cfg)
|
config.update(cfg)
|
||||||
|
|
||||||
|
|
||||||
@pricelist_bp.route("/drink-types", methods=["GET"])
|
@PriceListPlugin.blueprint.route("/drink-types", methods=["GET"])
|
||||||
@pricelist_bp.route("/drink-types/<int:identifier>", methods=["GET"])
|
@PriceListPlugin.blueprint.route("/drink-types/<int:identifier>", methods=["GET"])
|
||||||
def get_drink_types(identifier=None):
|
def get_drink_types(identifier=None):
|
||||||
if identifier:
|
if identifier is None:
|
||||||
result = pricelist_controller.get_drink_type(identifier)
|
|
||||||
else:
|
|
||||||
result = pricelist_controller.get_drink_types()
|
result = pricelist_controller.get_drink_types()
|
||||||
|
else:
|
||||||
|
result = pricelist_controller.get_drink_type(identifier)
|
||||||
return jsonify(result)
|
return jsonify(result)
|
||||||
|
|
||||||
|
|
||||||
@pricelist_bp.route("/drink-types", methods=["POST"])
|
@PriceListPlugin.blueprint.route("/drink-types", methods=["POST"])
|
||||||
@login_required(permission=permissions.CREATE_TYPE)
|
@login_required(permission=permissions.CREATE_TYPE)
|
||||||
def new_drink_type(current_session):
|
def new_drink_type(current_session):
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
|
@ -46,25 +46,25 @@ def new_drink_type(current_session):
|
||||||
return jsonify(drink_type)
|
return jsonify(drink_type)
|
||||||
|
|
||||||
|
|
||||||
@pricelist_bp.route("/drink-types/<int:identifier>", methods=["PUT"])
|
@PriceListPlugin.blueprint.route("/drink-types/<int:identifier>", methods=["PUT"])
|
||||||
@login_required(permission=permissions.EDIT_TYPE)
|
@login_required(permission=permissions.EDIT_TYPE)
|
||||||
def update_drink_type(identifier, current_session):
|
def update_drink_type(identifier, current_session):
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
if "name" not in data:
|
if "name" not in data:
|
||||||
raise BadRequest
|
raise BadRequest
|
||||||
drink_type = pricelist_controller.rename_drink_type(data["id"], data["name"])
|
drink_type = pricelist_controller.rename_drink_type(identifier, data["name"])
|
||||||
return jsonify(drink_type)
|
return jsonify(drink_type)
|
||||||
|
|
||||||
|
|
||||||
@pricelist_bp.route("/drink-types/<int:identifier>", methods=["DELETE"])
|
@PriceListPlugin.blueprint.route("/drink-types/<int:identifier>", methods=["DELETE"])
|
||||||
@login_required(permission=permissions.DELETE_TYPE)
|
@login_required(permission=permissions.DELETE_TYPE)
|
||||||
def delete_drink_type(identifier, current_session):
|
def delete_drink_type(identifier, current_session):
|
||||||
pricelist_controller.delete_drink_type(identifier)
|
pricelist_controller.delete_drink_type(identifier)
|
||||||
return "", NO_CONTENT
|
return no_content()
|
||||||
|
|
||||||
|
|
||||||
@pricelist_bp.route("/tags", methods=["GET"])
|
@PriceListPlugin.blueprint.route("/tags", methods=["GET"])
|
||||||
@pricelist_bp.route("/tags/<int:identifier>", methods=["GET"])
|
@PriceListPlugin.blueprint.route("/tags/<int:identifier>", methods=["GET"])
|
||||||
def get_tags(identifier=None):
|
def get_tags(identifier=None):
|
||||||
if identifier:
|
if identifier:
|
||||||
result = pricelist_controller.get_tag(identifier)
|
result = pricelist_controller.get_tag(identifier)
|
||||||
|
@ -73,7 +73,7 @@ def get_tags(identifier=None):
|
||||||
return jsonify(result)
|
return jsonify(result)
|
||||||
|
|
||||||
|
|
||||||
@pricelist_bp.route("/tags", methods=["POST"])
|
@PriceListPlugin.blueprint.route("/tags", methods=["POST"])
|
||||||
@login_required(permission=permissions.CREATE_TAG)
|
@login_required(permission=permissions.CREATE_TAG)
|
||||||
def new_tag(current_session):
|
def new_tag(current_session):
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
|
@ -83,25 +83,25 @@ def new_tag(current_session):
|
||||||
return jsonify(drink_type)
|
return jsonify(drink_type)
|
||||||
|
|
||||||
|
|
||||||
@pricelist_bp.route("/tags/<int:identifier>", methods=["PUT"])
|
@PriceListPlugin.blueprint.route("/tags/<int:identifier>", methods=["PUT"])
|
||||||
@login_required(permission=permissions.EDIT_TAG)
|
@login_required(permission=permissions.EDIT_TAG)
|
||||||
def update_tag(identifier, current_session):
|
def update_tag(identifier, current_session):
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
if "name" not in data:
|
if "name" not in data:
|
||||||
raise BadRequest
|
raise BadRequest
|
||||||
drink_type = pricelist_controller.rename_tag(data["name"])
|
tag = pricelist_controller.rename_tag(identifier, data["name"])
|
||||||
return jsonify(drink_type)
|
return jsonify(tag)
|
||||||
|
|
||||||
|
|
||||||
@pricelist_bp.route("/tags/<int:identifier>", methods=["DELETE"])
|
@PriceListPlugin.blueprint.route("/tags/<int:identifier>", methods=["DELETE"])
|
||||||
@login_required(permission=permissions.DELETE_TAG)
|
@login_required(permission=permissions.DELETE_TAG)
|
||||||
def delete_tag(identifier, current_session):
|
def delete_tag(identifier, current_session):
|
||||||
pricelist_controller.delete_tag(identifier)
|
pricelist_controller.delete_tag(identifier)
|
||||||
return "", NO_CONTENT
|
return no_content()
|
||||||
|
|
||||||
|
|
||||||
@pricelist_bp.route("/drinks", methods=["GET"])
|
@PriceListPlugin.blueprint.route("/drinks", methods=["GET"])
|
||||||
@pricelist_bp.route("/drinks/<int:identifier>", methods=["GET"])
|
@PriceListPlugin.blueprint.route("/drinks/<int:identifier>", methods=["GET"])
|
||||||
def get_drinks(identifier=None):
|
def get_drinks(identifier=None):
|
||||||
public = True
|
public = True
|
||||||
try:
|
try:
|
||||||
|
@ -116,85 +116,87 @@ def get_drinks(identifier=None):
|
||||||
result = pricelist_controller.get_drinks(public=public)
|
result = pricelist_controller.get_drinks(public=public)
|
||||||
return jsonify(result)
|
return jsonify(result)
|
||||||
|
|
||||||
@pricelist_bp.route("/drinks/search/<string:name>", methods=["GET"])
|
|
||||||
|
@PriceListPlugin.blueprint.route("/drinks/search/<string:name>", methods=["GET"])
|
||||||
def search_drinks(name):
|
def search_drinks(name):
|
||||||
return jsonify(pricelist_controller.get_drinks(name))
|
return jsonify(pricelist_controller.get_drinks(name))
|
||||||
|
|
||||||
|
|
||||||
@pricelist_bp.route("/drinks", methods=["POST"])
|
@PriceListPlugin.blueprint.route("/drinks", methods=["POST"])
|
||||||
@login_required(permission=permissions.CREATE)
|
@login_required(permission=permissions.CREATE)
|
||||||
def create_drink(current_session):
|
def create_drink(current_session):
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
return jsonify(pricelist_controller.set_drink(data))
|
return jsonify(pricelist_controller.set_drink(data))
|
||||||
|
|
||||||
|
|
||||||
@pricelist_bp.route("/drinks/<int:identifier>", methods=["PUT"])
|
@PriceListPlugin.blueprint.route("/drinks/<int:identifier>", methods=["PUT"])
|
||||||
def update_drink(identifier):
|
def update_drink(identifier):
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
return jsonify(pricelist_controller.update_drink(identifier, data))
|
return jsonify(pricelist_controller.update_drink(identifier, data))
|
||||||
|
|
||||||
|
|
||||||
@pricelist_bp.route("/drinks/<int:identifier>", methods=["DELETE"])
|
@PriceListPlugin.blueprint.route("/drinks/<int:identifier>", methods=["DELETE"])
|
||||||
def delete_drink(identifier):
|
def delete_drink(identifier):
|
||||||
pricelist_controller.delete_drink(identifier)
|
pricelist_controller.delete_drink(identifier)
|
||||||
return "", NO_CONTENT
|
return no_content()
|
||||||
|
|
||||||
|
|
||||||
@pricelist_bp.route("/prices/<int:identifier>", methods=["DELETE"])
|
@PriceListPlugin.blueprint.route("/prices/<int:identifier>", methods=["DELETE"])
|
||||||
def delete_price(identifier):
|
def delete_price(identifier):
|
||||||
pricelist_controller.delete_price(identifier)
|
pricelist_controller.delete_price(identifier)
|
||||||
return "", NO_CONTENT
|
return no_content()
|
||||||
|
|
||||||
|
|
||||||
@pricelist_bp.route("/volumes/<int:identifier>", methods=["DELETE"])
|
@PriceListPlugin.blueprint.route("/volumes/<int:identifier>", methods=["DELETE"])
|
||||||
def delete_volume(identifier):
|
def delete_volume(identifier):
|
||||||
pricelist_controller.delete_volume(identifier)
|
pricelist_controller.delete_volume(identifier)
|
||||||
return "", NO_CONTENT
|
return no_content()
|
||||||
|
|
||||||
|
|
||||||
@pricelist_bp.route("/ingredients/extraIngredients", methods=["GET"])
|
@PriceListPlugin.blueprint.route("/ingredients/extraIngredients", methods=["GET"])
|
||||||
def get_extraIngredients():
|
def get_extra_ingredients():
|
||||||
return jsonify(pricelist_controller.get_extra_ingredients())
|
return jsonify(pricelist_controller.get_extra_ingredients())
|
||||||
|
|
||||||
|
|
||||||
@pricelist_bp.route("/ingredients/<int:identifier>", methods=["DELETE"])
|
@PriceListPlugin.blueprint.route("/ingredients/<int:identifier>", methods=["DELETE"])
|
||||||
def delete_ingredient(identifier):
|
def delete_ingredient(identifier):
|
||||||
pricelist_controller.delete_ingredient(identifier)
|
pricelist_controller.delete_ingredient(identifier)
|
||||||
return "", NO_CONTENT
|
return no_content()
|
||||||
|
|
||||||
|
|
||||||
@pricelist_bp.route("/ingredients/extraIngredients", methods=["POST"])
|
@PriceListPlugin.blueprint.route("/ingredients/extraIngredients", methods=["POST"])
|
||||||
def set_extra_ingredient():
|
def set_extra_ingredient():
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
return jsonify(pricelist_controller.set_extra_ingredient(data))
|
return jsonify(pricelist_controller.set_extra_ingredient(data))
|
||||||
|
|
||||||
|
|
||||||
@pricelist_bp.route("/ingredients/extraIngredients/<int:identifier>", methods=["PUT"])
|
@PriceListPlugin.blueprint.route("/ingredients/extraIngredients/<int:identifier>", methods=["PUT"])
|
||||||
def update_extra_ingredient(identifier):
|
def update_extra_ingredient(identifier):
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
return jsonify(pricelist_controller.update_extra_ingredient(identifier, data))
|
return jsonify(pricelist_controller.update_extra_ingredient(identifier, data))
|
||||||
|
|
||||||
|
|
||||||
@pricelist_bp.route("/ingredients/extraIngredients/<int:identifier>", methods=["DELETE"])
|
@PriceListPlugin.blueprint.route("/ingredients/extraIngredients/<int:identifier>", methods=["DELETE"])
|
||||||
def delete_extra_ingredient(identifier):
|
def delete_extra_ingredient(identifier):
|
||||||
pricelist_controller.delete_extra_ingredient(identifier)
|
pricelist_controller.delete_extra_ingredient(identifier)
|
||||||
return "", NO_CONTENT
|
return no_content()
|
||||||
|
|
||||||
|
|
||||||
@pricelist_bp.route("/settings/min_prices", methods=["POST", "GET"])
|
@PriceListPlugin.blueprint.route("/settings/min_prices", methods=["POST", "GET"])
|
||||||
def pricelist_settings_min_prices():
|
def pricelist_settings_min_prices():
|
||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
return jsonify(PriceListPlugin.get_setting(PriceListPlugin, "min_prices"))
|
# TODO: Handle if no prices are set!
|
||||||
|
return jsonify(PriceListPlugin.plugin.get_setting("min_prices"))
|
||||||
else:
|
else:
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
if not isinstance(data, list) or not all(isinstance(n, int) for n in data):
|
if not isinstance(data, list) or not all(isinstance(n, int) for n in data):
|
||||||
raise BadRequest
|
raise BadRequest
|
||||||
data.sort()
|
data.sort()
|
||||||
PriceListPlugin.set_setting(PriceListPlugin, "min_prices", data)
|
PriceListPlugin.plugin.set_setting("min_prices", data)
|
||||||
return no_content()
|
return no_content()
|
||||||
|
|
||||||
|
|
||||||
@pricelist_bp.route("/users/<userid>/pricecalc_columns", methods=["GET", "PUT"])
|
@PriceListPlugin.blueprint.route("/users/<userid>/pricecalc_columns", methods=["GET", "PUT"])
|
||||||
@login_required()
|
@login_required()
|
||||||
def get_columns(userid, current_session: Session):
|
def get_columns(userid, current_session: Session):
|
||||||
"""Get pricecalc_columns of an user
|
"""Get pricecalc_columns of an user
|
||||||
|
@ -225,7 +227,7 @@ def get_columns(userid, current_session: Session):
|
||||||
userController.persist()
|
userController.persist()
|
||||||
return no_content()
|
return no_content()
|
||||||
|
|
||||||
@pricelist_bp.route("/drinks/<int:identifier>/picture", methods=["POST", "GET", "DELETE"])
|
@PriceListPlugin.route("/drinks/<int:identifier>/picture", methods=["POST", "GET", "DELETE"])
|
||||||
def set_picture(identifier):
|
def set_picture(identifier):
|
||||||
|
|
||||||
if request.method == "DELETE":
|
if request.method == "DELETE":
|
||||||
|
@ -241,10 +243,10 @@ def set_picture(identifier):
|
||||||
else:
|
else:
|
||||||
raise BadRequest
|
raise BadRequest
|
||||||
|
|
||||||
@pricelist_bp.route("/picture/<identifier>", methods=["GET"])
|
@PriceListPlugin.route("/picture/<identifier>", methods=["GET"])
|
||||||
def _get_picture(identifier):
|
def _get_picture(identifier):
|
||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
size = request.args.get("size")
|
size = request.args.get("size")
|
||||||
path = config["pricelist"]["path"]
|
path = PriceListPlugin.plugin["path"]
|
||||||
response = pricelist_controller.get_drink_picture(identifier, size)
|
response = pricelist_controller.get_drink_picture(identifier, size)
|
||||||
return response.make_conditional(request)
|
return response.make_conditional(request)
|
|
@ -3,7 +3,7 @@ from __future__ import annotations # TODO: Remove if python requirement is >= 3
|
||||||
from flaschengeist.database import db
|
from flaschengeist.database import db
|
||||||
from flaschengeist.models import ModelSerializeMixin
|
from flaschengeist.models import ModelSerializeMixin
|
||||||
|
|
||||||
from typing import Optional, Union
|
from typing import Optional
|
||||||
|
|
||||||
drink_tag_association = db.Table(
|
drink_tag_association = db.Table(
|
||||||
"drink_x_tag",
|
"drink_x_tag",
|
||||||
|
@ -71,7 +71,7 @@ class DrinkIngredient(db.Model, ModelSerializeMixin):
|
||||||
__tablename__ = "drink_ingredient"
|
__tablename__ = "drink_ingredient"
|
||||||
id: int = db.Column("id", db.Integer, primary_key=True)
|
id: int = db.Column("id", db.Integer, primary_key=True)
|
||||||
volume: float = db.Column(db.Numeric(precision=5, scale=2, asdecimal=False), nullable=False)
|
volume: float = db.Column(db.Numeric(precision=5, scale=2, asdecimal=False), nullable=False)
|
||||||
drink_ingredient_id: int = db.Column("drink_ingredient_id", db.Integer, db.ForeignKey("drink.id"))
|
ingredient_id: int = db.Column(db.Integer, db.ForeignKey("drink.id"))
|
||||||
# drink_ingredient: Drink = db.relationship("Drink")
|
# drink_ingredient: Drink = db.relationship("Drink")
|
||||||
# price: float = 0
|
# price: float = 0
|
||||||
|
|
||||||
|
@ -92,11 +92,12 @@ class Ingredient(db.Model, ModelSerializeMixin):
|
||||||
__tablename__ = "ingredient_association"
|
__tablename__ = "ingredient_association"
|
||||||
id: int = db.Column("id", db.Integer, primary_key=True)
|
id: int = db.Column("id", db.Integer, primary_key=True)
|
||||||
volume_id = db.Column(db.Integer, db.ForeignKey("drink_price_volume.id"))
|
volume_id = db.Column(db.Integer, db.ForeignKey("drink_price_volume.id"))
|
||||||
drink_ingredient_id = db.Column(db.Integer, db.ForeignKey("drink_ingredient.id"))
|
|
||||||
drink_ingredient: Optional[DrinkIngredient] = db.relationship(DrinkIngredient)
|
drink_ingredient: Optional[DrinkIngredient] = db.relationship(DrinkIngredient)
|
||||||
extra_ingredient_id = db.Column(db.Integer, db.ForeignKey("extra_ingredient.id"))
|
|
||||||
extra_ingredient: Optional[ExtraIngredient] = db.relationship(ExtraIngredient)
|
extra_ingredient: Optional[ExtraIngredient] = db.relationship(ExtraIngredient)
|
||||||
|
|
||||||
|
_drink_ingredient_id = db.Column(db.Integer, db.ForeignKey("drink_ingredient.id"))
|
||||||
|
_extra_ingredient_id = db.Column(db.Integer, db.ForeignKey("extra_ingredient.id"))
|
||||||
|
|
||||||
|
|
||||||
class MinPrices(ModelSerializeMixin):
|
class MinPrices(ModelSerializeMixin):
|
||||||
"""
|
"""
|
||||||
|
@ -134,8 +135,8 @@ class Drink(db.Model, ModelSerializeMixin):
|
||||||
package_size: Optional[int] = db.Column(db.Integer)
|
package_size: Optional[int] = db.Column(db.Integer)
|
||||||
name: str = db.Column(db.String(60), nullable=False)
|
name: str = db.Column(db.String(60), nullable=False)
|
||||||
volume: Optional[float] = db.Column(db.Numeric(precision=5, scale=2, asdecimal=False))
|
volume: Optional[float] = db.Column(db.Numeric(precision=5, scale=2, asdecimal=False))
|
||||||
cost_price_pro_volume: Optional[float] = db.Column(db.Numeric(precision=5, scale=3, asdecimal=False))
|
cost_per_volume: Optional[float] = db.Column(db.Numeric(precision=5, scale=3, asdecimal=False))
|
||||||
cost_price_package_netto: Optional[float] = db.Column(db.Numeric(precision=5, scale=3, asdecimal=False))
|
cost_per_package: Optional[float] = db.Column(db.Numeric(precision=5, scale=3, asdecimal=False))
|
||||||
|
|
||||||
uuid: str = db.Column(db.String(36))
|
uuid: str = db.Column(db.String(36))
|
||||||
receipt: Optional[str] = db.Column(db.String)
|
receipt: Optional[str] = db.Column(db.String)
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
from werkzeug.exceptions import BadRequest, NotFound
|
from werkzeug.exceptions import BadRequest, NotFound
|
||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
from flaschengeist import logger
|
from flaschengeist import logger
|
||||||
from flaschengeist.config import config
|
from flaschengeist.config import config
|
||||||
from flaschengeist.database import db
|
from flaschengeist.database import db
|
||||||
|
from flaschengeist.utils.picture import save_picture, get_picture
|
||||||
|
|
||||||
from .models import Drink, DrinkPrice, Ingredient, Tag, DrinkType, DrinkPriceVolume, DrinkIngredient, ExtraIngredient
|
from .models import Drink, DrinkPrice, Ingredient, Tag, DrinkType, DrinkPriceVolume, DrinkIngredient, ExtraIngredient
|
||||||
|
|
||||||
from flaschengeist.utils.picture import save_picture, get_picture, delete_picture
|
|
||||||
|
|
||||||
from uuid import uuid4
|
|
||||||
|
|
||||||
|
|
||||||
def update():
|
def update():
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
@ -21,15 +20,15 @@ def get_tags():
|
||||||
|
|
||||||
def get_tag(identifier):
|
def get_tag(identifier):
|
||||||
if isinstance(identifier, int):
|
if isinstance(identifier, int):
|
||||||
retVal = Tag.query.get(identifier)
|
ret = Tag.query.get(identifier)
|
||||||
elif isinstance(identifier, str):
|
elif isinstance(identifier, str):
|
||||||
retVal = Tag.query.filter(Tag.name == identifier).one_or_none()
|
ret = Tag.query.filter(Tag.name == identifier).one_or_none()
|
||||||
else:
|
else:
|
||||||
logger.debug("Invalid identifier type for Tag")
|
logger.debug("Invalid identifier type for Tag")
|
||||||
raise BadRequest
|
raise BadRequest
|
||||||
if not retVal:
|
if ret is None:
|
||||||
raise NotFound
|
raise NotFound
|
||||||
return retVal
|
return ret
|
||||||
|
|
||||||
|
|
||||||
def create_tag(name):
|
def create_tag(name):
|
||||||
|
@ -66,23 +65,23 @@ def get_drink_types():
|
||||||
|
|
||||||
def get_drink_type(identifier):
|
def get_drink_type(identifier):
|
||||||
if isinstance(identifier, int):
|
if isinstance(identifier, int):
|
||||||
retVal = DrinkType.query.get(identifier)
|
ret = DrinkType.query.get(identifier)
|
||||||
elif isinstance(identifier, str):
|
elif isinstance(identifier, str):
|
||||||
retVal = DrinkType.query.filter(Tag.name == identifier).one_or_none()
|
ret = DrinkType.query.filter(Tag.name == identifier).one_or_none()
|
||||||
else:
|
else:
|
||||||
logger.debug("Invalid identifier type for DrinkType")
|
logger.debug("Invalid identifier type for DrinkType")
|
||||||
raise BadRequest
|
raise BadRequest
|
||||||
if not retVal:
|
if ret is None:
|
||||||
raise NotFound
|
raise NotFound
|
||||||
return retVal
|
return ret
|
||||||
|
|
||||||
|
|
||||||
def create_drink_type(name):
|
def create_drink_type(name):
|
||||||
try:
|
try:
|
||||||
drinkType = DrinkType(name=name)
|
drink_type = DrinkType(name=name)
|
||||||
db.session.add(drinkType)
|
db.session.add(drink_type)
|
||||||
update()
|
update()
|
||||||
return drinkType
|
return drink_type
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
raise BadRequest("Name already exists")
|
raise BadRequest("Name already exists")
|
||||||
|
|
||||||
|
@ -98,8 +97,8 @@ def rename_drink_type(identifier, new_name):
|
||||||
|
|
||||||
|
|
||||||
def delete_drink_type(identifier):
|
def delete_drink_type(identifier):
|
||||||
drinkType = get_drink_type(identifier)
|
drink_type = get_drink_type(identifier)
|
||||||
db.session.delete(drinkType)
|
db.session.delete(drink_type)
|
||||||
try:
|
try:
|
||||||
update()
|
update()
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
|
@ -137,13 +136,12 @@ def get_drink(identifier, public=False):
|
||||||
elif isinstance(identifier, str):
|
elif isinstance(identifier, str):
|
||||||
drink = Drink.query.filter(Tag.name == identifier).one_or_none()
|
drink = Drink.query.filter(Tag.name == identifier).one_or_none()
|
||||||
else:
|
else:
|
||||||
logger.debug("Invalid identifier type for Drink")
|
raise BadRequest("Invalid identifier type for Drink")
|
||||||
raise BadRequest
|
if drink is None:
|
||||||
if drink:
|
raise NotFound
|
||||||
if public:
|
if public:
|
||||||
return _create_public_drink(drink)
|
return _create_public_drink(drink)
|
||||||
return drink
|
return drink
|
||||||
raise NotFound
|
|
||||||
|
|
||||||
|
|
||||||
def set_drink(data):
|
def set_drink(data):
|
||||||
|
@ -151,44 +149,40 @@ def set_drink(data):
|
||||||
|
|
||||||
|
|
||||||
def update_drink(identifier, data):
|
def update_drink(identifier, data):
|
||||||
allowedKeys = Drink().serialize().keys()
|
try:
|
||||||
if "id" in data:
|
if "id" in data:
|
||||||
data.pop("id")
|
data.pop("id")
|
||||||
if "volumes" in data:
|
volumes = data.pop("volumes") if "volumes" in data else None
|
||||||
volumes = data.pop("volumes")
|
if "tags" in data:
|
||||||
if "tags" in data:
|
data.pop("tags")
|
||||||
data.pop("tags")
|
drink_type = data.pop("type")
|
||||||
type = None
|
if isinstance(drink_type, dict) and "id" in drink_type:
|
||||||
if "type" in data:
|
drink_type = drink_type["id"]
|
||||||
_type = data.pop("type")
|
drink_type = get_drink_type(drink_type)
|
||||||
if isinstance(_type, dict) and "id" in _type:
|
if identifier == -1:
|
||||||
type = get_drink_type(_type.get("id"))
|
drink = Drink()
|
||||||
if identifier == -1:
|
db.session.add(drink)
|
||||||
drink = Drink()
|
else:
|
||||||
db.session.add(drink)
|
drink = get_drink(identifier)
|
||||||
else:
|
for key, value in data.items():
|
||||||
drink = get_drink(identifier)
|
if hasattr(drink, key):
|
||||||
if not drink:
|
setattr(drink, key, value if value != "" else None)
|
||||||
raise NotFound
|
|
||||||
for key, value in data.items():
|
|
||||||
if hasattr(drink, key):
|
|
||||||
setattr(drink, key, value if value != "" else None)
|
|
||||||
|
|
||||||
if type:
|
if drink_type:
|
||||||
drink.type = type
|
drink.type = drink_type
|
||||||
if volumes:
|
if volumes is not None:
|
||||||
set_volumes(volumes, drink)
|
set_volumes(volumes, drink)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return drink
|
return drink
|
||||||
|
except (NotFound, KeyError):
|
||||||
|
raise BadRequest
|
||||||
|
|
||||||
|
|
||||||
def set_volumes(volumes, drink):
|
def set_volumes(volumes, drink):
|
||||||
if isinstance(volumes, list):
|
if not isinstance(volumes, list):
|
||||||
_volumes = []
|
raise BadRequest
|
||||||
for _volume in volumes:
|
for volume in volumes:
|
||||||
volume = set_volume(_volume)
|
drink.volumes.append(set_volume(volume))
|
||||||
_volumes.append(volume)
|
|
||||||
drink.volumes = _volumes
|
|
||||||
|
|
||||||
|
|
||||||
def delete_drink(identifier):
|
def delete_drink(identifier):
|
||||||
|
@ -216,15 +210,12 @@ def set_volume(data):
|
||||||
prices = values.pop("prices")
|
prices = values.pop("prices")
|
||||||
if "ingredients" in values:
|
if "ingredients" in values:
|
||||||
ingredients = values.pop("ingredients")
|
ingredients = values.pop("ingredients")
|
||||||
id = None
|
vol_id = values.pop("id", None)
|
||||||
if "id" in values:
|
if vol_id < 0:
|
||||||
id = values.pop("id")
|
|
||||||
volume = None
|
|
||||||
if id < 0:
|
|
||||||
volume = DrinkPriceVolume(**values)
|
volume = DrinkPriceVolume(**values)
|
||||||
db.session.add(volume)
|
db.session.add(volume)
|
||||||
else:
|
else:
|
||||||
volume = get_volume(id)
|
volume = get_volume(vol_id)
|
||||||
if not volume:
|
if not volume:
|
||||||
raise NotFound
|
raise NotFound
|
||||||
for key, value in values.items():
|
for key, value in values.items():
|
||||||
|
@ -276,15 +267,12 @@ def get_prices(volume_id=None):
|
||||||
def set_price(data):
|
def set_price(data):
|
||||||
allowed_keys = DrinkPrice().serialize().keys()
|
allowed_keys = DrinkPrice().serialize().keys()
|
||||||
values = {key: value for key, value in data.items() if key in allowed_keys}
|
values = {key: value for key, value in data.items() if key in allowed_keys}
|
||||||
id = None
|
price_id = values.pop("id", -1)
|
||||||
if "id" in values:
|
if price_id < 0:
|
||||||
id = values.pop("id")
|
|
||||||
price = None
|
|
||||||
if id < 0:
|
|
||||||
price = DrinkPrice(**values)
|
price = DrinkPrice(**values)
|
||||||
db.session.add(price)
|
db.session.add(price)
|
||||||
else:
|
else:
|
||||||
price = get_price(id)
|
price = get_price(price_id)
|
||||||
if not price:
|
if not price:
|
||||||
raise NotFound
|
raise NotFound
|
||||||
for key, value in values.items():
|
for key, value in values.items():
|
||||||
|
@ -300,17 +288,14 @@ def delete_price(identifier):
|
||||||
|
|
||||||
|
|
||||||
def set_drink_ingredient(data):
|
def set_drink_ingredient(data):
|
||||||
allowedKeys = DrinkIngredient().serialize().keys()
|
allowed_keys = DrinkIngredient().serialize().keys()
|
||||||
drink = None
|
values = {key: value for key, value in data.items() if key in allowed_keys}
|
||||||
values = {key: value for key, value in data.items() if key in allowedKeys}
|
ingredient_id = values.pop("id", -1)
|
||||||
id = None
|
if ingredient_id < 0:
|
||||||
if "id" in values:
|
|
||||||
id = values.pop("id")
|
|
||||||
if id < 0:
|
|
||||||
drink_ingredient = DrinkIngredient(**values)
|
drink_ingredient = DrinkIngredient(**values)
|
||||||
db.session.add(drink_ingredient)
|
db.session.add(drink_ingredient)
|
||||||
else:
|
else:
|
||||||
drink_ingredient = DrinkIngredient.query.get(id)
|
drink_ingredient = DrinkIngredient.query.get(ingredient_id)
|
||||||
if not drink_ingredient:
|
if not drink_ingredient:
|
||||||
raise NotFound
|
raise NotFound
|
||||||
for key, value in values.items():
|
for key, value in values.items():
|
||||||
|
@ -329,15 +314,12 @@ def set_ingredient(data):
|
||||||
drink_ingredient_value = data.pop("drink_ingredient")
|
drink_ingredient_value = data.pop("drink_ingredient")
|
||||||
if "extra_ingredient" in data:
|
if "extra_ingredient" in data:
|
||||||
extra_ingredient_value = data.pop("extra_ingredient")
|
extra_ingredient_value = data.pop("extra_ingredient")
|
||||||
id = None
|
ingredient_id = data.pop("id", -1)
|
||||||
if "id" in data:
|
if ingredient_id < 0:
|
||||||
id = data.pop("id")
|
|
||||||
ingredient = None
|
|
||||||
if id < 0:
|
|
||||||
ingredient = Ingredient(**data)
|
ingredient = Ingredient(**data)
|
||||||
db.session.add(ingredient)
|
db.session.add(ingredient)
|
||||||
else:
|
else:
|
||||||
ingredient = get_ingredient(id)
|
ingredient = get_ingredient(ingredient_id)
|
||||||
if not ingredient:
|
if not ingredient:
|
||||||
raise NotFound
|
raise NotFound
|
||||||
if drink_ingredient_value:
|
if drink_ingredient_value:
|
||||||
|
@ -365,10 +347,10 @@ def get_extra_ingredient(identifier):
|
||||||
|
|
||||||
|
|
||||||
def set_extra_ingredient(data):
|
def set_extra_ingredient(data):
|
||||||
allowedKeys = ExtraIngredient().serialize().keys()
|
allowed_keys = ExtraIngredient().serialize().keys()
|
||||||
if "id" in data:
|
if "id" in data:
|
||||||
data.pop("id")
|
data.pop("id")
|
||||||
values = {key: value for key, value in data.items() if key in allowedKeys}
|
values = {key: value for key, value in data.items() if key in allowed_keys}
|
||||||
extra_ingredient = ExtraIngredient(**values)
|
extra_ingredient = ExtraIngredient(**values)
|
||||||
db.session.add(extra_ingredient)
|
db.session.add(extra_ingredient)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
@ -376,10 +358,10 @@ def set_extra_ingredient(data):
|
||||||
|
|
||||||
|
|
||||||
def update_extra_ingredient(identifier, data):
|
def update_extra_ingredient(identifier, data):
|
||||||
allowedKeys = ExtraIngredient().serialize().keys()
|
allowed_keys = ExtraIngredient().serialize().keys()
|
||||||
if "id" in data:
|
if "id" in data:
|
||||||
data.pop("id")
|
data.pop("id")
|
||||||
values = {key: value for key, value in data.items() if key in allowedKeys}
|
values = {key: value for key, value in data.items() if key in allowed_keys}
|
||||||
extra_ingredient = get_extra_ingredient(identifier)
|
extra_ingredient = get_extra_ingredient(identifier)
|
||||||
if extra_ingredient:
|
if extra_ingredient:
|
||||||
for key, value in values.items():
|
for key, value in values.items():
|
||||||
|
|
|
@ -12,17 +12,17 @@ from flaschengeist.utils.decorators import login_required
|
||||||
from flaschengeist.controller import roleController
|
from flaschengeist.controller import roleController
|
||||||
from flaschengeist.utils.HTTP import created
|
from flaschengeist.utils.HTTP import created
|
||||||
|
|
||||||
roles_bp = Blueprint("roles", __name__)
|
|
||||||
_permission_edit = "roles_edit"
|
_permission_edit = "roles_edit"
|
||||||
_permission_delete = "roles_delete"
|
_permission_delete = "roles_delete"
|
||||||
|
|
||||||
|
|
||||||
class RolesPlugin(Plugin):
|
class RolesPlugin(Plugin):
|
||||||
def __init__(self, config):
|
name = "roles"
|
||||||
super().__init__(config, roles_bp, permissions=[_permission_edit, _permission_delete])
|
blueprint = Blueprint(name, __name__)
|
||||||
|
permissions = [_permission_edit, _permission_delete]
|
||||||
|
|
||||||
|
|
||||||
@roles_bp.route("/roles", methods=["GET"])
|
@RolesPlugin.blueprint.route("/roles", methods=["GET"])
|
||||||
@login_required()
|
@login_required()
|
||||||
def list_roles(current_session):
|
def list_roles(current_session):
|
||||||
"""List all existing roles
|
"""List all existing roles
|
||||||
|
@ -39,7 +39,7 @@ def list_roles(current_session):
|
||||||
return jsonify(roles)
|
return jsonify(roles)
|
||||||
|
|
||||||
|
|
||||||
@roles_bp.route("/roles", methods=["POST"])
|
@RolesPlugin.blueprint.route("/roles", methods=["POST"])
|
||||||
@login_required(permission=_permission_edit)
|
@login_required(permission=_permission_edit)
|
||||||
def create_role(current_session):
|
def create_role(current_session):
|
||||||
"""Create new role
|
"""Create new role
|
||||||
|
@ -62,7 +62,7 @@ def create_role(current_session):
|
||||||
return created(roleController.create_role(data["name"], permissions))
|
return created(roleController.create_role(data["name"], permissions))
|
||||||
|
|
||||||
|
|
||||||
@roles_bp.route("/roles/permissions", methods=["GET"])
|
@RolesPlugin.blueprint.route("/roles/permissions", methods=["GET"])
|
||||||
@login_required()
|
@login_required()
|
||||||
def list_permissions(current_session):
|
def list_permissions(current_session):
|
||||||
"""List all existing permissions
|
"""List all existing permissions
|
||||||
|
@ -79,7 +79,7 @@ def list_permissions(current_session):
|
||||||
return jsonify(permissions)
|
return jsonify(permissions)
|
||||||
|
|
||||||
|
|
||||||
@roles_bp.route("/roles/<role_name>", methods=["GET"])
|
@RolesPlugin.blueprint.route("/roles/<role_name>", methods=["GET"])
|
||||||
@login_required()
|
@login_required()
|
||||||
def get_role(role_name, current_session):
|
def get_role(role_name, current_session):
|
||||||
"""Get role by name
|
"""Get role by name
|
||||||
|
@ -97,7 +97,7 @@ def get_role(role_name, current_session):
|
||||||
return jsonify(role)
|
return jsonify(role)
|
||||||
|
|
||||||
|
|
||||||
@roles_bp.route("/roles/<int:role_id>", methods=["PUT"])
|
@RolesPlugin.blueprint.route("/roles/<int:role_id>", methods=["PUT"])
|
||||||
@login_required(permission=_permission_edit)
|
@login_required(permission=_permission_edit)
|
||||||
def edit_role(role_id, current_session):
|
def edit_role(role_id, current_session):
|
||||||
"""Edit role, rename and / or set permissions
|
"""Edit role, rename and / or set permissions
|
||||||
|
@ -123,7 +123,7 @@ def edit_role(role_id, current_session):
|
||||||
return "", NO_CONTENT
|
return "", NO_CONTENT
|
||||||
|
|
||||||
|
|
||||||
@roles_bp.route("/roles/<int:role_id>", methods=["DELETE"])
|
@RolesPlugin.blueprint.route("/roles/<int:role_id>", methods=["DELETE"])
|
||||||
@login_required(permission=_permission_delete)
|
@login_required(permission=_permission_delete)
|
||||||
def delete_role(role_id, current_session):
|
def delete_role(role_id, current_session):
|
||||||
"""Delete role
|
"""Delete role
|
||||||
|
|
|
@ -13,18 +13,17 @@ from flaschengeist.plugins import Plugin
|
||||||
from flaschengeist.models.user import User, _Avatar
|
from flaschengeist.models.user import User, _Avatar
|
||||||
from flaschengeist.utils.decorators import login_required, extract_session, headers
|
from flaschengeist.utils.decorators import login_required, extract_session, headers
|
||||||
from flaschengeist.controller import userController
|
from flaschengeist.controller import userController
|
||||||
from flaschengeist.utils.HTTP import created
|
from flaschengeist.utils.HTTP import created, no_content
|
||||||
from flaschengeist.utils.datetime import from_iso_format
|
from flaschengeist.utils.datetime import from_iso_format
|
||||||
|
|
||||||
users_bp = Blueprint("users", __name__)
|
|
||||||
|
|
||||||
|
|
||||||
class UsersPlugin(Plugin):
|
class UsersPlugin(Plugin):
|
||||||
def __init__(self, cfg):
|
name = "users"
|
||||||
super().__init__(blueprint=users_bp, permissions=permissions.permissions)
|
blueprint = Blueprint(name, __name__)
|
||||||
|
permissions = permissions.permissions
|
||||||
|
|
||||||
|
|
||||||
@users_bp.route("/users", methods=["POST"])
|
@UsersPlugin.blueprint.route("/users", methods=["POST"])
|
||||||
def register():
|
def register():
|
||||||
"""Register a new user
|
"""Register a new user
|
||||||
The password will be set to a random string of at lease 16byte entropy.
|
The password will be set to a random string of at lease 16byte entropy.
|
||||||
|
@ -55,7 +54,7 @@ def register():
|
||||||
return make_response(jsonify(userController.register(data)), CREATED)
|
return make_response(jsonify(userController.register(data)), CREATED)
|
||||||
|
|
||||||
|
|
||||||
@users_bp.route("/users", methods=["GET"])
|
@UsersPlugin.blueprint.route("/users", methods=["GET"])
|
||||||
@login_required()
|
@login_required()
|
||||||
@headers({"Cache-Control": "private, must-revalidate, max-age=3600"})
|
@headers({"Cache-Control": "private, must-revalidate, max-age=3600"})
|
||||||
def list_users(current_session):
|
def list_users(current_session):
|
||||||
|
@ -74,7 +73,7 @@ def list_users(current_session):
|
||||||
return jsonify(users)
|
return jsonify(users)
|
||||||
|
|
||||||
|
|
||||||
@users_bp.route("/users/<userid>", methods=["GET"])
|
@UsersPlugin.blueprint.route("/users/<userid>", methods=["GET"])
|
||||||
@login_required()
|
@login_required()
|
||||||
@headers({"Cache-Control": "private, must-revalidate, max-age=3600"})
|
@headers({"Cache-Control": "private, must-revalidate, max-age=3600"})
|
||||||
def get_user(userid, current_session):
|
def get_user(userid, current_session):
|
||||||
|
@ -97,7 +96,25 @@ def get_user(userid, current_session):
|
||||||
return jsonify(serial)
|
return jsonify(serial)
|
||||||
|
|
||||||
|
|
||||||
@users_bp.route("/users/<userid>/avatar", methods=["GET"])
|
@UsersPlugin.blueprint.route("/users/<userid>/frontend", methods=["POST", "GET"])
|
||||||
|
@login_required()
|
||||||
|
def frontend(userid, current_session):
|
||||||
|
if current_session.user_.userid != userid:
|
||||||
|
raise Forbidden
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
if request.content_length > 1024 ** 2:
|
||||||
|
raise BadRequest
|
||||||
|
current_session.user_.set_attribute("frontend", request.get_json())
|
||||||
|
return no_content()
|
||||||
|
else:
|
||||||
|
content = current_session.user_.get_attribute("frontend", None)
|
||||||
|
if content is None:
|
||||||
|
return no_content()
|
||||||
|
return jsonify(content)
|
||||||
|
|
||||||
|
|
||||||
|
@UsersPlugin.blueprint.route("/users/<userid>/avatar", methods=["GET"])
|
||||||
@headers({"Cache-Control": "public, max-age=604800"})
|
@headers({"Cache-Control": "public, max-age=604800"})
|
||||||
def get_avatar(userid):
|
def get_avatar(userid):
|
||||||
user = userController.get_user(userid)
|
user = userController.get_user(userid)
|
||||||
|
@ -109,7 +126,7 @@ def get_avatar(userid):
|
||||||
raise NotFound
|
raise NotFound
|
||||||
|
|
||||||
|
|
||||||
@users_bp.route("/users/<userid>/avatar", methods=["POST"])
|
@UsersPlugin.blueprint.route("/users/<userid>/avatar", methods=["POST"])
|
||||||
@login_required()
|
@login_required()
|
||||||
def set_avatar(userid, current_session):
|
def set_avatar(userid, current_session):
|
||||||
user = userController.get_user(userid)
|
user = userController.get_user(userid)
|
||||||
|
@ -127,7 +144,7 @@ def set_avatar(userid, current_session):
|
||||||
raise BadRequest
|
raise BadRequest
|
||||||
|
|
||||||
|
|
||||||
@users_bp.route("/users/<userid>", methods=["DELETE"])
|
@UsersPlugin.blueprint.route("/users/<userid>", methods=["DELETE"])
|
||||||
@login_required(permission=permissions.DELETE)
|
@login_required(permission=permissions.DELETE)
|
||||||
def delete_user(userid, current_session):
|
def delete_user(userid, current_session):
|
||||||
"""Delete user by userid
|
"""Delete user by userid
|
||||||
|
@ -147,7 +164,7 @@ def delete_user(userid, current_session):
|
||||||
return "", NO_CONTENT
|
return "", NO_CONTENT
|
||||||
|
|
||||||
|
|
||||||
@users_bp.route("/users/<userid>", methods=["PUT"])
|
@UsersPlugin.blueprint.route("/users/<userid>", methods=["PUT"])
|
||||||
@login_required()
|
@login_required()
|
||||||
def edit_user(userid, current_session):
|
def edit_user(userid, current_session):
|
||||||
"""Modify user by userid
|
"""Modify user by userid
|
||||||
|
@ -198,3 +215,19 @@ def edit_user(userid, current_session):
|
||||||
userController.modify_user(user, password, new_password)
|
userController.modify_user(user, password, new_password)
|
||||||
userController.update_user(user)
|
userController.update_user(user)
|
||||||
return "", NO_CONTENT
|
return "", NO_CONTENT
|
||||||
|
|
||||||
|
|
||||||
|
@UsersPlugin.blueprint.route("/notifications", methods=["GET"])
|
||||||
|
@login_required()
|
||||||
|
def notifications(current_session):
|
||||||
|
f = request.args.get("from", None)
|
||||||
|
if f is not None:
|
||||||
|
f = from_iso_format(f)
|
||||||
|
return jsonify(userController.get_notifications(current_session.user_, f))
|
||||||
|
|
||||||
|
|
||||||
|
@UsersPlugin.blueprint.route("/notifications/<nid>", methods=["DELETE"])
|
||||||
|
@login_required()
|
||||||
|
def remove_notifications(nid, current_session):
|
||||||
|
userController.delete_notification(nid, current_session.user_)
|
||||||
|
return no_content()
|
||||||
|
|
|
@ -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:
|
if arguments.plugins:
|
||||||
for entry_point in pkg_resources.iter_entry_points("flaschengeist.plugin"):
|
for entry_point in pkg_resources.iter_entry_points("flaschengeist.plugin"):
|
||||||
plg = entry_point.load()
|
plg = entry_point.load()
|
||||||
if hasattr(plg, "models"):
|
if hasattr(plg, "models") and plg.models is not None:
|
||||||
gen.run(plg.models)
|
gen.run(plg.models)
|
||||||
gen.write()
|
gen.write()
|
||||||
|
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -6,7 +6,7 @@ mysql_driver = "PyMySQL" if os.name == "nt" else "mysqlclient"
|
||||||
|
|
||||||
|
|
||||||
class DocsCommand(Command):
|
class DocsCommand(Command):
|
||||||
description = "Generate and export API documentation"
|
description = "Generate and export API documentation using pdoc3"
|
||||||
user_options = [
|
user_options = [
|
||||||
# The format is (long option, short option, description).
|
# The format is (long option, short option, description).
|
||||||
("output=", "o", "Documentation output path"),
|
("output=", "o", "Documentation output path"),
|
||||||
|
|
Loading…
Reference in New Issue