Compare commits

..

6 Commits

23 changed files with 124 additions and 55 deletions

1
.gitignore vendored
View File

@ -122,6 +122,7 @@ dmypy.json
*.swo *.swo
.vscode/ .vscode/
*.log *.log
.fleet/
data/ data/

View File

@ -1,9 +1,12 @@
import enum import enum
import json
from flask import Flask from flask import Flask
from flask_cors import CORS from flask_cors import CORS
from datetime import datetime, date from datetime import datetime, date
from flask.json import JSONEncoder, jsonify from flask.json import jsonify
from json import JSONEncoder
from flask.json.provider import JSONProvider
from sqlalchemy.exc import OperationalError from sqlalchemy.exc import OperationalError
from werkzeug.exceptions import HTTPException from werkzeug.exceptions import HTTPException
@ -12,6 +15,8 @@ from flaschengeist.controller import pluginController
from flaschengeist.utils.hook import Hook from flaschengeist.utils.hook import Hook
from flaschengeist.config import configure_app from flaschengeist.config import configure_app
from flaschengeist.database import db
class CustomJSONEncoder(JSONEncoder): class CustomJSONEncoder(JSONEncoder):
def default(self, o): def default(self, o):
@ -34,6 +39,19 @@ class CustomJSONEncoder(JSONEncoder):
return JSONEncoder.default(self, o) return JSONEncoder.default(self, o)
class CustomJSONProvider(JSONProvider):
ensure_ascii: bool = True
sort_keys: bool = True
def dumps(self, obj, **kwargs):
kwargs.setdefault("ensure_ascii", self.ensure_ascii)
kwargs.setdefault("sort_keys", self.sort_keys)
return json.dumps(obj, **kwargs, cls=CustomJSONEncoder)
def loads(self, s: str | bytes, **kwargs):
return json.loads(s, **kwargs)
@Hook("plugins.loaded") @Hook("plugins.loaded")
def load_plugins(app: Flask): def load_plugins(app: Flask):
app.config["FG_PLUGINS"] = {} app.config["FG_PLUGINS"] = {}
@ -43,7 +61,9 @@ def load_plugins(app: Flask):
try: try:
# Load class # Load class
cls = plugin.entry_point.load() cls = plugin.entry_point.load()
plugin = cls.query.get(plugin.id) if plugin.id is not None else plugin # plugin = cls.query.get(plugin.id) if plugin.id is not None else plugin
# plugin = db.session.query(cls).get(plugin.id) if plugin.id is not None else plugin
plugin = db.session.get(cls, plugin.id) if plugin.id is not None else plugin
# Custom loading tasks # Custom loading tasks
plugin.load() plugin.load()
# Register blueprint # Register blueprint
@ -58,9 +78,11 @@ def load_plugins(app: Flask):
logger.info(f"Loaded plugin: {plugin.name}") logger.info(f"Loaded plugin: {plugin.name}")
app.config["FG_PLUGINS"][plugin.name] = plugin app.config["FG_PLUGINS"][plugin.name] = plugin
def create_app(test_config=None, cli=False): def create_app(test_config=None, cli=False):
app = Flask("flaschengeist") app = Flask("flaschengeist")
app.json_encoder = CustomJSONEncoder app.json_provider_class = CustomJSONProvider
app.json = CustomJSONProvider(app)
CORS(app) CORS(app)
with app.app_context(): with app.app_context():

View File

@ -37,7 +37,6 @@ class InterfaceGenerator:
if origin is typing.ForwardRef: # isinstance(cls, typing.ForwardRef): if origin is typing.ForwardRef: # isinstance(cls, typing.ForwardRef):
return "", "this" if cls.__forward_arg__ == self.this_type else cls.__forward_arg__ return "", "this" if cls.__forward_arg__ == self.this_type else cls.__forward_arg__
if origin is typing.Union: if origin is typing.Union:
if len(arguments) == 2 and arguments[1] is type(None): if len(arguments) == 2 and arguments[1] is type(None):
return "?", self.pytype(arguments[0])[1] return "?", self.pytype(arguments[0])[1]
else: else:
@ -81,7 +80,6 @@ class InterfaceGenerator:
d = {} d = {}
for param, ptype in typing.get_type_hints(module[1], globalns=None, localns=None).items(): for param, ptype in typing.get_type_hints(module[1], globalns=None, localns=None).items():
if not param.startswith("_") and not param.endswith("_"): if not param.startswith("_") and not param.endswith("_"):
d[param] = self.pytype(ptype) d[param] = self.pytype(ptype)
if len(d) == 1: if len(d) == 1:
@ -115,7 +113,7 @@ class InterfaceGenerator:
return buffer return buffer
def write(self): def write(self):
with (open(self.filename, "w") if self.filename else sys.stdout) as file: with open(self.filename, "w") if self.filename else sys.stdout as file:
if self.namespace: if self.namespace:
file.write(f"declare namespace {self.namespace} {{\n") file.write(f"declare namespace {self.namespace} {{\n")
for line in self._write_types().getvalue().split("\n"): for line in self._write_types().getvalue().split("\n"):

View File

@ -9,7 +9,7 @@ from importlib.metadata import entry_points
@click.option("--no-core", help="Skip models / types from flaschengeist core", is_flag=True) @click.option("--no-core", help="Skip models / types from flaschengeist core", is_flag=True)
def export(namespace, output, no_core, plugin): def export(namespace, output, no_core, plugin):
from flaschengeist import logger, models from flaschengeist import logger, models
from .InterfaceGenerator import InterfaceGenerator from flaschengeist.cli.InterfaceGenerator import InterfaceGenerator
gen = InterfaceGenerator(namespace, output, logger) gen = InterfaceGenerator(namespace, output, logger)
if not no_core: if not no_core:

View File

@ -53,7 +53,6 @@ def disable(ctx, plugin):
def install(ctx: click.Context, plugin, all): def install(ctx: click.Context, plugin, all):
"""Install one or more plugins""" """Install one or more plugins"""
all_plugins = entry_points(group="flaschengeist.plugins") all_plugins = entry_points(group="flaschengeist.plugins")
if all: if all:
plugins = [ep.name for ep in all_plugins] plugins = [ep.name for ep in all_plugins]
elif len(plugin) > 0: elif len(plugin) > 0:

View File

@ -10,7 +10,6 @@ class PrefixMiddleware(object):
self.prefix = prefix self.prefix = prefix
def __call__(self, environ, start_response): def __call__(self, environ, start_response):
if environ["PATH_INFO"].startswith(self.prefix): if environ["PATH_INFO"].startswith(self.prefix):
environ["PATH_INFO"] = environ["PATH_INFO"][len(self.prefix) :] environ["PATH_INFO"] = environ["PATH_INFO"][len(self.prefix) :]
environ["SCRIPT_NAME"] = self.prefix environ["SCRIPT_NAME"] = self.prefix

View File

@ -28,7 +28,7 @@ def read_configuration(test_config):
if not test_config: if not test_config:
paths.append(Path.home() / ".config") paths.append(Path.home() / ".config")
if "FLASCHENGEIST_CONF" in os.environ: if "FLASCHENGEIST_CONF" in os.environ:
paths.append(Path(os.environ.get("FLASCHENGEIST_CONF"))) paths.append(Path(str(os.environ.get("FLASCHENGEIST_CONF"))))
for loc in paths: for loc in paths:
try: try:

View File

@ -3,7 +3,7 @@
Used by plugins for setting and notification functionality. Used by plugins for setting and notification functionality.
""" """
from typing import Union from typing import Union, List
from flask import current_app from flask import current_app
from werkzeug.exceptions import NotFound, BadRequest from werkzeug.exceptions import NotFound, BadRequest
from sqlalchemy.exc import OperationalError, ProgrammingError from sqlalchemy.exc import OperationalError, ProgrammingError
@ -23,7 +23,11 @@ __required_plugins = ["users", "roles", "scheduler", "auth"]
def get_authentication_provider(): def get_authentication_provider():
return [current_app.config["FG_PLUGINS"][plugin.name] for plugin in get_loaded_plugins().values() if isinstance(plugin, AuthPlugin)] return [
current_app.config["FG_PLUGINS"][plugin.name]
for plugin in get_loaded_plugins().values()
if isinstance(plugin, AuthPlugin)
]
def get_loaded_plugins(plugin_name: str = None): def get_loaded_plugins(plugin_name: str = None):
@ -56,7 +60,7 @@ def get_enabled_plugins() -> list[Plugin]:
return enabled_plugins return enabled_plugins
def notify(plugin_id: str, user, text: str, data=None): def notify(plugin_id: int, user, text: str, data=None):
"""Create a new notification for an user """Create a new notification for an user
Args: Args:
@ -70,12 +74,23 @@ def notify(plugin_id: str, user, text: str, data=None):
Hint: use the data for frontend actions. Hint: use the data for frontend actions.
""" """
if not user.deleted: if not user.deleted:
n = Notification(text=text, data=data, plugin=plugin_id, user_=user) n = Notification(text=text, data=data, plugin_id_=plugin_id, user_=user)
db.session.add(n) db.session.add(n)
db.session.commit() db.session.commit()
return n.id return n.id
def get_notifications(plugin_id) -> List[Notification]:
"""Get all notifications for a plugin
Args:
plugin_id: ID of the plugin
Returns:
List of `flaschengeist.models.notification.Notification`
"""
return db.session.execute(db.select(Notification).where(Notification.plugin_id_ == plugin_id)).scalars().all()
@Hook("plugins.installed") @Hook("plugins.installed")
def install_plugin(plugin_name: str): def install_plugin(plugin_name: str):
logger.debug(f"Installing plugin {plugin_name}") logger.debug(f"Installing plugin {plugin_name}")
@ -97,7 +112,7 @@ def install_plugin(plugin_name: str):
directory /= loc directory /= loc
if directory.exists(): if directory.exists():
database_upgrade(revision=f"{plugin_name}@head") database_upgrade(revision=f"{plugin_name}@head")
db.session.commit()
return plugin return plugin

View File

@ -2,6 +2,7 @@ import re
import secrets import secrets
from io import BytesIO from io import BytesIO
from typing import Optional
from sqlalchemy import exc from sqlalchemy import exc
from sqlalchemy_utils import merge_references from sqlalchemy_utils import merge_references
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
@ -41,15 +42,17 @@ def _generate_password_reset(user):
return reset return reset
def get_provider(userid: str): def get_provider(userid: str) -> AuthPlugin:
return [p for p in pluginController.get_authentication_provider() if p.user_exists(userid)][0] return [p for p in pluginController.get_authentication_provider() if p.user_exists(userid)][0]
@Hook @Hook
def update_user(user: User, backend: AuthPlugin): def update_user(user: User, backend: Optional[AuthPlugin] = None):
"""Update user data from backend """Update user data from backend
This is seperate function to provide a hook""" This is seperate function to provide a hook"""
if not backend:
backend = get_provider(user.userid)
backend.update_user(user) backend.update_user(user)
if not user.display_name: if not user.display_name:
user.display_name = "{} {}.".format(user.firstname, user.lastname[0]) user.display_name = "{} {}.".format(user.firstname, user.lastname[0])

View File

@ -6,7 +6,8 @@ from sqlalchemy import MetaData
from flaschengeist.alembic import alembic_script_path from flaschengeist.alembic import alembic_script_path
from flaschengeist import logger from flaschengeist import logger
from flaschengeist.controller import pluginController
# from flaschengeist.controller import pluginController
# https://alembic.sqlalchemy.org/en/latest/naming.html # https://alembic.sqlalchemy.org/en/latest/naming.html
metadata = MetaData( metadata = MetaData(
@ -20,7 +21,7 @@ metadata = MetaData(
) )
db = SQLAlchemy(metadata=metadata) db = SQLAlchemy(metadata=metadata, session_options={"expire_on_commit": False})
migrate = Migrate() migrate = Migrate()

View File

@ -16,13 +16,16 @@ class ModelSerializeMixin:
module = import_module("flaschengeist.models").__dict__ module = import_module("flaschengeist.models").__dict__
hint = typing.get_type_hints(self.__class__, globalns=module)[param] try:
if ( hint = typing.get_type_hints(self.__class__, globalns=module, locals=locals())[param]
typing.get_origin(hint) is typing.Union if (
and len(typing.get_args(hint)) == 2 typing.get_origin(hint) is typing.Union
and typing.get_args(hint)[1] is type(None) and len(typing.get_args(hint)) == 2
): and typing.get_args(hint)[1] is type(None)
return getattr(self, param) is None ):
return getattr(self, param) is None
except:
pass
def serialize(self): def serialize(self):
"""Serialize class to dict """Serialize class to dict
@ -35,7 +38,7 @@ class ModelSerializeMixin:
if not param.startswith("_") and not param.endswith("_") and not self.__is_optional(param) if not param.startswith("_") and not param.endswith("_") and not self.__is_optional(param)
} }
if len(d) == 1: if len(d) == 1:
key, value = d.popitem() _, value = d.popitem()
return value return value
return d return d

View File

@ -8,6 +8,7 @@ from ..database.types import ModelSerializeMixin, Serial
class Image(db.Model, ModelSerializeMixin): class Image(db.Model, ModelSerializeMixin):
__allow_unmapped__ = True
__tablename__ = "image" __tablename__ = "image"
id: int = db.Column(Serial, primary_key=True) id: int = db.Column(Serial, primary_key=True)
filename_: str = db.Column("filename", db.String(255), nullable=False) filename_: str = db.Column("filename", db.String(255), nullable=False)

View File

@ -8,6 +8,7 @@ from ..database.types import Serial, UtcDateTime, ModelSerializeMixin
class Notification(db.Model, ModelSerializeMixin): class Notification(db.Model, ModelSerializeMixin):
__allow_unmapped__ = True
__tablename__ = "notification" __tablename__ = "notification"
id: int = db.Column("id", Serial, primary_key=True) id: int = db.Column("id", Serial, primary_key=True)
text: str = db.Column(db.Text) text: str = db.Column(db.Text)
@ -20,7 +21,8 @@ class Notification(db.Model, ModelSerializeMixin):
plugin_: Plugin = db.relationship( plugin_: Plugin = db.relationship(
"Plugin", backref=db.backref("notifications_", cascade="all, delete, delete-orphan") "Plugin", backref=db.backref("notifications_", cascade="all, delete, delete-orphan")
) )
plugin: str
@property @property
def plugin(self): def plugin(self) -> str:
return self.plugin_.name return self.plugin_.name

View File

@ -1,6 +1,6 @@
from __future__ import annotations # TODO: Remove if python requirement is >= 3.12 (? PEP 563 is defered) from __future__ import annotations # TODO: Remove if python requirement is >= 3.12 (? PEP 563 is defered)
from typing import Any from typing import Any, List, Dict
from sqlalchemy.orm.collections import attribute_mapped_collection from sqlalchemy.orm.collections import attribute_mapped_collection
from ..database import db from ..database import db
@ -8,6 +8,7 @@ from ..database.types import Serial
class PluginSetting(db.Model): class PluginSetting(db.Model):
__allow_unmapped__ = True
__tablename__ = "plugin_setting" __tablename__ = "plugin_setting"
id = db.Column("id", Serial, primary_key=True) id = db.Column("id", Serial, primary_key=True)
plugin_id: int = db.Column("plugin", Serial, db.ForeignKey("plugin.id")) plugin_id: int = db.Column("plugin", Serial, db.ForeignKey("plugin.id"))
@ -16,6 +17,7 @@ class PluginSetting(db.Model):
class BasePlugin(db.Model): class BasePlugin(db.Model):
__allow_unmapped__ = True
__tablename__ = "plugin" __tablename__ = "plugin"
id: int = db.Column("id", Serial, primary_key=True) id: int = db.Column("id", Serial, primary_key=True)
name: str = db.Column(db.String(127), nullable=False) name: str = db.Column(db.String(127), nullable=False)
@ -24,7 +26,7 @@ class BasePlugin(db.Model):
"""The latest installed version""" """The latest installed version"""
enabled: bool = db.Column(db.Boolean, default=False) enabled: bool = db.Column(db.Boolean, default=False)
"""Enabled state of the plugin""" """Enabled state of the plugin"""
permissions: list = db.relationship( permissions: List["Permission"] = db.relationship(
"Permission", cascade="all, delete, delete-orphan", back_populates="plugin_", lazy="select" "Permission", cascade="all, delete, delete-orphan", back_populates="plugin_", lazy="select"
) )
"""Optional list of custom permissions used by the plugin """Optional list of custom permissions used by the plugin
@ -33,11 +35,11 @@ class BasePlugin(db.Model):
to prevent clashes with other plugins. E. g. instead of *delete* use *plugin_delete*. to prevent clashes with other plugins. E. g. instead of *delete* use *plugin_delete*.
""" """
__settings: dict[str, "PluginSetting"] = db.relationship( __settings: Dict[str, "PluginSetting"] = db.relationship(
"PluginSetting", "PluginSetting",
collection_class=attribute_mapped_collection("name"), collection_class=attribute_mapped_collection("name"),
cascade="all, delete, delete-orphan", cascade="all, delete, delete-orphan",
lazy="select", lazy="subquery",
) )
def get_setting(self, name: str, **kwargs): def get_setting(self, name: str, **kwargs):

View File

@ -17,6 +17,7 @@ class Session(db.Model, ModelSerializeMixin):
token: String to verify access later. token: String to verify access later.
""" """
__allow_unmapped__ = True
__tablename__ = "session" __tablename__ = "session"
expires: datetime = db.Column(UtcDateTime) expires: datetime = db.Column(UtcDateTime)
token: str = db.Column(db.String(32), unique=True) token: str = db.Column(db.String(32), unique=True)

View File

@ -1,6 +1,6 @@
from __future__ import annotations # TODO: Remove if python requirement is >= 3.12 (? PEP 563 is defered) from __future__ import annotations # TODO: Remove if python requirement is >= 3.12 (? PEP 563 is defered)
from typing import Optional from typing import Optional, Union, List
from datetime import date, datetime from datetime import date, datetime
from sqlalchemy.orm.collections import attribute_mapped_collection from sqlalchemy.orm.collections import attribute_mapped_collection
@ -21,19 +21,21 @@ role_permission_association_table = db.Table(
class Permission(db.Model, ModelSerializeMixin): class Permission(db.Model, ModelSerializeMixin):
__allow_unmapped__ = True
__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", Serial, primary_key=True) id_ = db.Column("id", Serial, primary_key=True)
plugin_id_: int = db.Column("plugin", Serial, db.ForeignKey("plugin.id")) plugin_id_: int = db.Column("plugin", Serial, db.ForeignKey("plugin.id"))
plugin_ = db.relationship("Plugin", lazy="select", back_populates="permissions", enable_typechecks=False) plugin_ = db.relationship("Plugin", lazy="subquery", back_populates="permissions", enable_typechecks=False)
class Role(db.Model, ModelSerializeMixin): class Role(db.Model, ModelSerializeMixin):
__allow_unmapped__ = True
__tablename__ = "role" __tablename__ = "role"
id: int = db.Column(Serial, 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)
class User(db.Model, ModelSerializeMixin): class User(db.Model, ModelSerializeMixin):
@ -51,6 +53,7 @@ class User(db.Model, ModelSerializeMixin):
birthday: Birthday of the user birthday: Birthday of the user
""" """
__allow_unmapped__ = True
__tablename__ = "user" __tablename__ = "user"
userid: str = db.Column(db.String(30), unique=True, nullable=False) userid: str = db.Column(db.String(30), unique=True, nullable=False)
display_name: str = db.Column(db.String(30)) display_name: str = db.Column(db.String(30))
@ -59,15 +62,15 @@ class User(db.Model, ModelSerializeMixin):
deleted: bool = db.Column(db.Boolean(), default=False) deleted: bool = db.Column(db.Boolean(), default=False)
birthday: Optional[date] = db.Column(db.Date) birthday: Optional[date] = db.Column(db.Date)
mail: str = db.Column(db.String(60)) mail: str = db.Column(db.String(60))
roles: list[str] = [] roles: List[str] = []
permissions: Optional[list[str]] = None permissions: Optional[list[str]] = []
# Protected stuff for backend use only # Protected stuff for backend use only
id_ = db.Column("id", Serial, 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_: list[Session] = db.relationship("Session", back_populates="user_", cascade="all, delete, delete-orphan") sessions_: List[Session] = db.relationship("Session", back_populates="user_", cascade="all, delete, delete-orphan")
avatar_: Optional[Image] = db.relationship("Image", cascade="all, delete, delete-orphan", single_parent=True) avatar_: Optional[Image] = db.relationship("Image", cascade="all, delete, delete-orphan", single_parent=True)
reset_requests_: list["_PasswordReset"] = db.relationship("_PasswordReset", cascade="all, delete, delete-orphan") reset_requests_: List["_PasswordReset"] = db.relationship("_PasswordReset", cascade="all, delete, delete-orphan")
# Private stuff for internal use # Private stuff for internal use
_avatar_id = db.Column("avatar", Serial, db.ForeignKey("image.id")) _avatar_id = db.Column("avatar", Serial, db.ForeignKey("image.id"))
@ -78,7 +81,7 @@ class User(db.Model, ModelSerializeMixin):
) )
@property @property
def roles(self): def roles(self) -> List[str]:
return [role.name for role in self.roles_] return [role.name for role in self.roles_]
def set_attribute(self, name, value): def set_attribute(self, name, value):
@ -107,6 +110,7 @@ class User(db.Model, ModelSerializeMixin):
class _UserAttribute(db.Model, ModelSerializeMixin): class _UserAttribute(db.Model, ModelSerializeMixin):
__allow_unmapped__ = True
__tablename__ = "user_attribute" __tablename__ = "user_attribute"
id = db.Column("id", Serial, primary_key=True) id = db.Column("id", Serial, primary_key=True)
user: User = db.Column("user", Serial, db.ForeignKey("user.id"), nullable=False) user: User = db.Column("user", Serial, db.ForeignKey("user.id"), nullable=False)
@ -117,6 +121,7 @@ class _UserAttribute(db.Model, ModelSerializeMixin):
class _PasswordReset(db.Model): class _PasswordReset(db.Model):
"""Table containing password reset requests""" """Table containing password reset requests"""
__allow_unmapped__ = True
__tablename__ = "password_reset" __tablename__ = "password_reset"
_user_id: User = db.Column("user", Serial, 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", back_populates="reset_requests_", foreign_keys=[_user_id]) user: User = db.relationship("User", back_populates="reset_requests_", foreign_keys=[_user_id])

View File

@ -4,7 +4,7 @@
""" """
from typing import Union from typing import Union, List
from importlib.metadata import entry_points from importlib.metadata import entry_points
from werkzeug.exceptions import NotFound from werkzeug.exceptions import NotFound
from werkzeug.datastructures import FileStorage from werkzeug.datastructures import FileStorage
@ -100,7 +100,7 @@ class Plugin(BasePlugin):
@property @property
def entry_point(self): def entry_point(self):
ep = entry_points(group="flaschengeist.plugins", name=self.name) ep = tuple(entry_points(group="flaschengeist.plugins", name=self.name))
return ep[0] return ep[0]
def load(self): def load(self):
@ -142,7 +142,18 @@ class Plugin(BasePlugin):
""" """
from ..controller import pluginController from ..controller import pluginController
return pluginController.notify(self.name, user, text, data) return pluginController.notify(self.id, user, text, data)
@property
def notifications(self) -> List["Notification"]:
"""Get all notifications for this plugin
Returns:
List of `flaschengeist.models.notification.Notification`
"""
from ..controller import pluginController
return pluginController.get_notifications(self.id)
def serialize(self): def serialize(self):
"""Serialize a plugin into a dict """Serialize a plugin into a dict
@ -158,13 +169,13 @@ class Plugin(BasePlugin):
Args: Args:
permissions: List of permissions to install permissions: List of permissions to install
""" """
cur_perm = set(x.name for x in self.permissions) cur_perm = set(x for x in self.permissions or [])
all_perm = set(permissions) all_perm = set(permissions)
new_perms = all_perm - cur_perm new_perms = all_perm - cur_perm
self.permissions = list(filter(lambda x: x.name in permissions, self.permissions)) + [ _perms = [Permission(name=x, plugin_=self) for x in new_perms]
Permission(name=x, plugin_=self) for x in new_perms # self.permissions = list(filter(lambda x: x.name in permissions, self.permissions and isinstance(self.permissions, list) or []))
] self.permissions.extend(_perms)
class AuthPlugin(Plugin): class AuthPlugin(Plugin):

View File

@ -56,6 +56,7 @@ def service_debit():
class BalancePlugin(Plugin): class BalancePlugin(Plugin):
# id = "dev.flaschengeist.balance"
models = models models = models
def install(self): def install(self):
@ -63,6 +64,7 @@ class BalancePlugin(Plugin):
def load(self): def load(self):
from .routes import blueprint from .routes import blueprint
self.blueprint = blueprint self.blueprint = blueprint
@plugins_loaded @plugins_loaded
@ -71,7 +73,7 @@ class BalancePlugin(Plugin):
add_scheduled(f"{id}.service_debit", service_debit, minutes=1) add_scheduled(f"{id}.service_debit", service_debit, minutes=1)
@before_update_user @before_update_user
def set_default_limit(user): def set_default_limit(user, *args):
from . import balance_controller from . import balance_controller
try: try:

View File

@ -147,7 +147,6 @@ def get_balances(start: datetime = None, end: datetime = None, limit=None, offse
all = {} all = {}
for user in users: for user in users:
all[user.userid] = [user.get_credit(start, end), 0] all[user.userid] = [user.get_credit(start, end), 0]
all[user.userid][1] = user.get_debit(start, end) all[user.userid][1] = user.get_debit(start, end)

View File

@ -8,6 +8,7 @@ from flaschengeist.models import ModelSerializeMixin, UtcDateTime, Serial
class Transaction(db.Model, ModelSerializeMixin): class Transaction(db.Model, ModelSerializeMixin):
__allow_unmapped__ = True
__tablename__ = "balance_transaction" __tablename__ = "balance_transaction"
# Protected foreign key properties # Protected foreign key properties
_receiver_id = db.Column("receiver_id", Serial, db.ForeignKey("user.id")) _receiver_id = db.Column("receiver_id", Serial, db.ForeignKey("user.id"))

View File

@ -61,6 +61,7 @@ class SchedulerPlugin(Plugin):
def run_tasks(self): def run_tasks(self):
from ..database import db from ..database import db
self = db.session.merge(self) self = db.session.merge(self)
changed = False changed = False

View File

@ -106,7 +106,7 @@ def frontend(userid, current_session):
raise Forbidden raise Forbidden
if request.method == "POST": if request.method == "POST":
if request.content_length > 1024 ** 2: if request.content_length > 1024**2:
raise BadRequest raise BadRequest
current_session.user_.set_attribute("frontend", request.get_json()) current_session.user_.set_attribute("frontend", request.get_json())
return no_content() return no_content()
@ -218,7 +218,9 @@ def edit_user(userid, current_session):
userController.set_roles(user, roles) userController.set_roles(user, roles)
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()

View File

@ -22,7 +22,7 @@ include_package_data = True
python_requires = >=3.10 python_requires = >=3.10
packages = find: packages = find:
install_requires = install_requires =
Flask>=2.2.2 Flask>=2.2.2, <2.3
Pillow>=9.2 Pillow>=9.2
flask_cors flask_cors
flask_migrate>=3.1.0 flask_migrate>=3.1.0
@ -30,7 +30,8 @@ install_requires =
sqlalchemy_utils>=0.38.3 sqlalchemy_utils>=0.38.3
# Importlib requirement can be dropped when python requirement is >= 3.10 # Importlib requirement can be dropped when python requirement is >= 3.10
importlib_metadata>=4.3 importlib_metadata>=4.3
sqlalchemy>=1.4.40, <2.0 #sqlalchemy>=1.4.40, <2.0
sqlalchemy >= 2.0
toml toml
werkzeug>=2.2.2 werkzeug>=2.2.2
@ -73,4 +74,4 @@ testpaths = tests
[coverage:run] [coverage:run]
branch = True branch = True
source = flaschengeist source = flaschengeist