Compare commits
No commits in common. "0c319aab1a1cc59aecad692b5e3d901d1cf496d6" and "1201505586246c6fe9f0b293ab1cdd3cb85c18f3" have entirely different histories.
0c319aab1a
...
1201505586
|
@ -44,12 +44,11 @@ If not you need to create user and database manually do (or similar on Windows):
|
||||||
) | sudo mysql
|
) | sudo mysql
|
||||||
|
|
||||||
Then you can install the database tables, this will update all tables from core + all enabled plugins.
|
Then you can install the database tables, this will update all tables from core + all enabled plugins.
|
||||||
*Hint:* The same command can be later used to upgrade the database after plugins or core are updated.
|
|
||||||
|
|
||||||
$ flaschengeist db upgrade heads
|
$ flaschengeist db upgrade heads
|
||||||
|
|
||||||
## Plugins
|
## Plugins
|
||||||
To only upgrade one plugin (for example the `events` plugin):
|
To only upgrade one plugin:
|
||||||
|
|
||||||
$ flaschengeist db upgrade events@head
|
$ flaschengeist db upgrade events@head
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
"""Flaschengeist"""
|
"""Flaschengeist"""
|
||||||
import logging
|
import logging
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
|
from pathlib import Path
|
||||||
from werkzeug.local import LocalProxy
|
from werkzeug.local import LocalProxy
|
||||||
|
|
||||||
__version__ = pkg_resources.get_distribution("flaschengeist").version
|
__version__ = pkg_resources.get_distribution("flaschengeist").version
|
||||||
|
_module_path = Path(__file__).parent
|
||||||
__pdoc__ = {}
|
__pdoc__ = {}
|
||||||
|
|
||||||
logger: logging.Logger = LocalProxy(lambda: logging.getLogger(__name__))
|
logger: logging.Logger = LocalProxy(lambda: logging.getLogger(__name__))
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
import enum
|
import enum
|
||||||
|
|
||||||
from flask import Flask
|
import pkg_resources
|
||||||
|
from flask import Flask, current_app
|
||||||
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 JSONEncoder, jsonify
|
||||||
from sqlalchemy.exc import OperationalError
|
from sqlalchemy.exc import OperationalError
|
||||||
from werkzeug.exceptions import HTTPException
|
from werkzeug.exceptions import HTTPException
|
||||||
|
|
||||||
from flaschengeist.utils.plugin import get_plugins
|
|
||||||
|
|
||||||
from . import logger
|
from . import logger
|
||||||
from .plugins import Plugin
|
from .plugins import AuthPlugin
|
||||||
from .config import config, configure_app
|
from flaschengeist.config import config, configure_app
|
||||||
from .utils.hook import Hook
|
from flaschengeist.controller import roleController
|
||||||
|
from flaschengeist.utils.hook import Hook
|
||||||
|
|
||||||
|
|
||||||
class CustomJSONEncoder(JSONEncoder):
|
class CustomJSONEncoder(JSONEncoder):
|
||||||
|
@ -37,35 +37,64 @@ class CustomJSONEncoder(JSONEncoder):
|
||||||
|
|
||||||
|
|
||||||
@Hook("plugins.loaded")
|
@Hook("plugins.loaded")
|
||||||
def load_plugins(app: Flask):
|
def __load_plugins(app):
|
||||||
def load_plugin(cls: type[Plugin]):
|
logger.debug("Search for plugins")
|
||||||
logger.debug(f"Load plugin {cls.id}")
|
|
||||||
# Initialize plugin with config section
|
|
||||||
plugin = cls(config.get(plugin_class.id, config.get(plugin_class.id.split(".")[-1], {})))
|
|
||||||
# Register blueprint if provided
|
|
||||||
if plugin.blueprint is not None:
|
|
||||||
app.register_blueprint(plugin.blueprint)
|
|
||||||
# Save plugin application context
|
|
||||||
app.config.setdefault("FG_PLUGINS", {})[plugin.id] = plugin
|
|
||||||
return plugin
|
|
||||||
|
|
||||||
for plugin_class in get_plugins():
|
app.config["FG_PLUGINS"] = {}
|
||||||
names = [plugin_class.id, plugin_class.id.split(".")[-1]]
|
for entry_point in pkg_resources.iter_entry_points("flaschengeist.plugins"):
|
||||||
if config["FLASCHENGEIST"]["auth"] in names:
|
logger.debug(f"Found plugin: >{entry_point.name}<")
|
||||||
# Load authentification plugin
|
|
||||||
app.config["FG_AUTH_BACKEND"] = load_plugin(plugin_class)
|
if entry_point.name == config["FLASCHENGEIST"]["auth"] or (
|
||||||
logger.info(f"Using authentication plugin: {plugin_class.id}")
|
entry_point.name in config and config[entry_point.name].get("enabled", False)
|
||||||
elif any([i in config and config[i].get("enabled", False) for i in names]):
|
):
|
||||||
# Load all other enabled plugins
|
logger.debug(f"Load plugin {entry_point.name}")
|
||||||
load_plugin(plugin_class)
|
try:
|
||||||
logger.info(f"Using plugin: {plugin_class.id}")
|
plugin = entry_point.load()
|
||||||
|
if not hasattr(plugin, "name"):
|
||||||
|
setattr(plugin, "name", entry_point.name)
|
||||||
|
plugin = plugin(config.get(entry_point.name, {}))
|
||||||
|
if hasattr(plugin, "blueprint") and plugin.blueprint is not None:
|
||||||
|
app.register_blueprint(plugin.blueprint)
|
||||||
|
except:
|
||||||
|
logger.error(
|
||||||
|
f"Plugin {entry_point.name} was enabled, but could not be loaded due to an error.",
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
if isinstance(plugin, AuthPlugin):
|
||||||
|
if entry_point.name != config["FLASCHENGEIST"]["auth"]:
|
||||||
|
logger.debug(f"Unload not configured AuthPlugin {entry_point.name}")
|
||||||
|
del plugin
|
||||||
|
continue
|
||||||
else:
|
else:
|
||||||
logger.debug(f"Skip disabled plugin {plugin_class.id}")
|
logger.info(f"Using authentication plugin: {entry_point.name}")
|
||||||
|
app.config["FG_AUTH_BACKEND"] = plugin
|
||||||
|
else:
|
||||||
|
logger.info(f"Using plugin: {entry_point.name}")
|
||||||
|
app.config["FG_PLUGINS"][entry_point.name] = plugin
|
||||||
|
else:
|
||||||
|
logger.debug(f"Skip disabled plugin {entry_point.name}")
|
||||||
if "FG_AUTH_BACKEND" not in app.config:
|
if "FG_AUTH_BACKEND" not in app.config:
|
||||||
logger.fatal("No authentication plugin configured or authentication plugin not found")
|
logger.error("No authentication plugin configured or authentication plugin not found")
|
||||||
raise RuntimeError("No authentication plugin configured or authentication plugin not found")
|
raise RuntimeError("No authentication plugin configured or authentication plugin not found")
|
||||||
|
|
||||||
|
|
||||||
|
@Hook("plugins.installed")
|
||||||
|
def install_all():
|
||||||
|
from flaschengeist.database import db
|
||||||
|
|
||||||
|
db.create_all()
|
||||||
|
db.session.commit()
|
||||||
|
for name, plugin in current_app.config["FG_PLUGINS"].items():
|
||||||
|
if not plugin:
|
||||||
|
logger.debug(f"Skip disabled plugin: {name}")
|
||||||
|
continue
|
||||||
|
logger.info(f"Install plugin {name}")
|
||||||
|
plugin.install()
|
||||||
|
if plugin.permissions:
|
||||||
|
roleController.create_permissions(plugin.permissions)
|
||||||
|
|
||||||
|
|
||||||
def create_app(test_config=None, cli=False):
|
def create_app(test_config=None, cli=False):
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.json_encoder = CustomJSONEncoder
|
app.json_encoder = CustomJSONEncoder
|
||||||
|
@ -77,7 +106,7 @@ def create_app(test_config=None, cli=False):
|
||||||
configure_app(app, test_config, cli)
|
configure_app(app, test_config, cli)
|
||||||
db.init_app(app)
|
db.init_app(app)
|
||||||
migrate.init_app(app, db, compare_type=True)
|
migrate.init_app(app, db, compare_type=True)
|
||||||
load_plugins(app)
|
__load_plugins(app)
|
||||||
|
|
||||||
@app.route("/", methods=["GET"])
|
@app.route("/", methods=["GET"])
|
||||||
def __get_state():
|
def __get_state():
|
||||||
|
|
|
@ -5,7 +5,7 @@ import collections.abc
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||||
from flaschengeist import logger
|
from flaschengeist import _module_path, logger
|
||||||
|
|
||||||
|
|
||||||
# Default config:
|
# Default config:
|
||||||
|
@ -23,7 +23,7 @@ def update_dict(d, u):
|
||||||
|
|
||||||
def read_configuration(test_config):
|
def read_configuration(test_config):
|
||||||
global config
|
global config
|
||||||
paths = [Path(__file__).parent]
|
paths = [_module_path]
|
||||||
|
|
||||||
if not test_config:
|
if not test_config:
|
||||||
paths.append(Path.home() / ".config")
|
paths.append(Path.home() / ".config")
|
||||||
|
@ -44,7 +44,7 @@ def read_configuration(test_config):
|
||||||
def configure_logger(cli=False):
|
def configure_logger(cli=False):
|
||||||
global config
|
global config
|
||||||
# Read default config
|
# Read default config
|
||||||
logger_config = toml.load(Path(__file__).parent / "logging.toml")
|
logger_config = toml.load(_module_path / "logging.toml")
|
||||||
|
|
||||||
if "LOGGING" in config:
|
if "LOGGING" in config:
|
||||||
# Override with user config
|
# Override with user config
|
||||||
|
|
|
@ -22,18 +22,8 @@ migrate = Migrate()
|
||||||
|
|
||||||
@migrate.configure
|
@migrate.configure
|
||||||
def configure_alembic(config):
|
def configure_alembic(config):
|
||||||
"""Alembic configuration hook
|
|
||||||
|
|
||||||
Inject all migrations paths into the ``version_locations`` config option.
|
|
||||||
This includes even disabled plugins, as simply disabling a plugin without
|
|
||||||
uninstall can break the alembic version management.
|
|
||||||
"""
|
|
||||||
import inspect, pathlib
|
|
||||||
from flaschengeist.utils.plugin import get_plugins
|
|
||||||
|
|
||||||
# Load migration paths from plugins
|
# Load migration paths from plugins
|
||||||
migrations = [(pathlib.Path(inspect.getfile(p)).parent / "migrations") for p in get_plugins()]
|
migrations = [str(p.migrations_path) for p in current_app.config["FG_PLUGINS"].values() if p and p.migrations_path]
|
||||||
migrations = [str(m.resolve()) for m in migrations if m.exists()]
|
|
||||||
if len(migrations) > 0:
|
if len(migrations) > 0:
|
||||||
# Get configured paths
|
# Get configured paths
|
||||||
paths = config.get_main_option("version_locations")
|
paths = config.get_main_option("version_locations")
|
||||||
|
|
|
@ -1,142 +1,81 @@
|
||||||
"""Flaschengeist Plugins
|
|
||||||
|
|
||||||
## Custom database tables
|
|
||||||
|
|
||||||
You can add tables by declaring them using the SQLAlchemy syntax,
|
|
||||||
then use Alembic to generate migrations for your tables.
|
|
||||||
This allows Flaschengeist to proper up- or downgrade the
|
|
||||||
database tables if an user updates your plugin.
|
|
||||||
|
|
||||||
migrations have to be provided in a directory called `migrations`
|
|
||||||
next to your plugin. E.G.
|
|
||||||
|
|
||||||
myplugin
|
|
||||||
- __init__.py
|
|
||||||
- other/
|
|
||||||
- ...
|
|
||||||
- migrations/
|
|
||||||
|
|
||||||
## Useful Hooks
|
|
||||||
There are some predefined hooks, which might get handy for you.
|
|
||||||
|
|
||||||
For more information, please refer to
|
|
||||||
- `flaschengeist.utils.hook.HookBefore` and
|
|
||||||
- `flaschengeist.utils.hook.HookAfter`
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
|
import pkg_resources
|
||||||
|
from werkzeug.datastructures import FileStorage
|
||||||
from werkzeug.exceptions import MethodNotAllowed, NotFound
|
from werkzeug.exceptions import MethodNotAllowed, NotFound
|
||||||
|
from flaschengeist.controller import imageController
|
||||||
|
|
||||||
|
from flaschengeist.database import db
|
||||||
|
from flaschengeist.models.notification import Notification
|
||||||
|
from flaschengeist.models.user import _Avatar, User
|
||||||
|
from flaschengeist.models.setting import _PluginSetting
|
||||||
from flaschengeist.utils.hook import HookBefore, HookAfter
|
from flaschengeist.utils.hook import HookBefore, HookAfter
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"plugins_installed",
|
|
||||||
"plugins_loaded",
|
|
||||||
"before_delete_user",
|
|
||||||
"before_role_updated",
|
|
||||||
"before_update_user",
|
|
||||||
"after_role_updated",
|
|
||||||
"Plugin",
|
|
||||||
"AuthPlugin",
|
|
||||||
]
|
|
||||||
|
|
||||||
# Documentation hacks, see https://github.com/mitmproxy/pdoc/issues/320
|
|
||||||
plugins_installed = HookAfter("plugins.installed")
|
plugins_installed = HookAfter("plugins.installed")
|
||||||
plugins_installed.__doc__ = """Hook decorator for when all plugins are installed
|
"""Hook decorator for when all plugins are installed
|
||||||
|
|
||||||
Possible use case would be to populate the database with some presets.
|
Possible use case would be to populate the database with some presets.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
hook_result: void (kwargs)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
plugins_loaded = HookAfter("plugins.loaded")
|
plugins_loaded = HookAfter("plugins.loaded")
|
||||||
plugins_loaded.__doc__ = """Hook decorator for when all plugins are loaded
|
"""Hook decorator for when all plugins are loaded
|
||||||
|
|
||||||
Possible use case would be to check if a specific other plugin is loaded and change own behavior
|
Possible use case would be to check if a specific other plugin is loaded and change own behavior
|
||||||
|
|
||||||
Passed args:
|
Args:
|
||||||
- *app:* Current flask app instance (args)
|
app: Current flask app instance (args)
|
||||||
|
hook_result: void (kwargs)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
before_role_updated = HookBefore("update_role")
|
before_role_updated = HookBefore("update_role")
|
||||||
before_role_updated.__doc__ = """Hook decorator for when roles are modified
|
"""Hook decorator for when roles are modified
|
||||||
|
Args:
|
||||||
Passed args:
|
role: Role object to modify
|
||||||
- *role:* `flaschengeist.models.user.Role` to modify
|
new_name: New name if the name was changed (None if delete)
|
||||||
- *new_name:* New name if the name was changed (*None* if delete)
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
after_role_updated = HookAfter("update_role")
|
after_role_updated = HookAfter("update_role")
|
||||||
after_role_updated.__doc__ = """Hook decorator for when roles are modified
|
"""Hook decorator for when roles are modified
|
||||||
|
Args:
|
||||||
Passed args:
|
role: Role object containing the modified role
|
||||||
- *role:* modified `flaschengeist.models.user.Role`
|
new_name: New name if the name was changed (None if deleted)
|
||||||
- *new_name:* New name if the name was changed (*None* if deleted)
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
before_update_user = HookBefore("update_user")
|
before_update_user = HookBefore("update_user")
|
||||||
before_update_user.__doc__ = """Hook decorator, when ever an user update is done, this is called before.
|
"""Hook decorator, when ever an user update is done, this is called before.
|
||||||
|
Args:
|
||||||
Passed args:
|
user: User object
|
||||||
- *user:* `flaschengeist.models.user.User` object
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
before_delete_user = HookBefore("delete_user")
|
before_delete_user = HookBefore("delete_user")
|
||||||
before_delete_user.__doc__ = """Hook decorator,this is called before an user gets deleted.
|
"""Hook decorator,this is called before an user gets deleted.
|
||||||
|
Args:
|
||||||
Passed args:
|
user: User object
|
||||||
- *user:* `flaschengeist.models.user.User` object
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class Plugin:
|
class Plugin:
|
||||||
"""Base class for all Plugins
|
"""Base class for all Plugins
|
||||||
|
If your class uses custom models add a static property called ``models``"""
|
||||||
|
|
||||||
All plugins must be derived from this class.
|
blueprint = None # You have to override
|
||||||
There are some static properties a plugin must provide,
|
|
||||||
and some properties a plugin can provide if you might want
|
|
||||||
to use more functionality.
|
|
||||||
|
|
||||||
Required:
|
|
||||||
- *id*: Unique identifier of your plugin
|
|
||||||
|
|
||||||
Optional:
|
|
||||||
- *blueprint*: `flask.Blueprint` providing your routes
|
|
||||||
- *permissions*: List of your custom permissions
|
|
||||||
- *models*: Your models, used for API export
|
|
||||||
- *version*: Version of your plugin, can also be guessed by Flaschengeist
|
|
||||||
"""
|
|
||||||
|
|
||||||
id: str = None
|
|
||||||
"""Override with the unique ID of the plugin
|
|
||||||
|
|
||||||
Hint: Use a fully qualified name like "dev.flaschengeist.plugin"
|
|
||||||
"""
|
|
||||||
|
|
||||||
blueprint = None
|
|
||||||
"""Override with a `flask.blueprint` if the plugin uses custom routes"""
|
"""Override with a `flask.blueprint` if the plugin uses custom routes"""
|
||||||
|
permissions = [] # You have to override
|
||||||
permissions: list[str] = [] # You have to override
|
|
||||||
"""Override to add custom permissions used by the plugin
|
"""Override to add custom permissions used by the plugin
|
||||||
|
|
||||||
A good style is to name the permissions with a prefix related to the plugin name,
|
A good style is to name the permissions with a prefix related to the plugin name,
|
||||||
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*.
|
||||||
"""
|
"""
|
||||||
|
id = "dev.flaschengeist.plugin" # You have to override
|
||||||
models = None
|
"""Override with the unique ID of the plugin (Hint: FQN)"""
|
||||||
"""Override with models module
|
name = "plugin" # You have to override
|
||||||
|
"""Override with human readable name of the plugin"""
|
||||||
Used for API export, has to be a static property
|
models = None # You have to override
|
||||||
"""
|
"""Override with models module"""
|
||||||
|
migrations_path = None # Override this with the location of your db migrations directory
|
||||||
version = None
|
"""Override with path to migration files, if custome db tables are used"""
|
||||||
"""Override with a custom version, optional
|
|
||||||
|
|
||||||
If not set, the version is guessed from the package / distribution
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, config=None):
|
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
|
||||||
"""
|
"""
|
||||||
|
self.version = pkg_resources.get_distribution(self.__module__.split(".")[0]).version
|
||||||
|
|
||||||
def install(self):
|
def install(self):
|
||||||
"""Installation routine
|
"""Installation routine
|
||||||
|
@ -145,17 +84,6 @@ class Plugin:
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def uninstall(self):
|
|
||||||
"""Uninstall routine
|
|
||||||
|
|
||||||
If the plugin has custom database tables, make sure to remove them.
|
|
||||||
This can be either done by downgrading the plugin *head* to the *base*.
|
|
||||||
Or use custom migrations for the uninstall and *stamp* some version.
|
|
||||||
|
|
||||||
Is always called with Flask application context.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_setting(self, name: str, **kwargs):
|
def get_setting(self, name: str, **kwargs):
|
||||||
"""Get plugin setting from database
|
"""Get plugin setting from database
|
||||||
|
|
||||||
|
@ -167,8 +95,6 @@ class Plugin:
|
||||||
Raises:
|
Raises:
|
||||||
`KeyError` if no such setting exists in the database
|
`KeyError` if no such setting exists in the database
|
||||||
"""
|
"""
|
||||||
from flaschengeist.models.setting import _PluginSetting
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
setting = (
|
setting = (
|
||||||
_PluginSetting.query.filter(_PluginSetting.plugin == self.name)
|
_PluginSetting.query.filter(_PluginSetting.plugin == self.name)
|
||||||
|
@ -189,9 +115,6 @@ class Plugin:
|
||||||
name: String identifying the setting
|
name: String identifying the setting
|
||||||
value: Value to be stored
|
value: Value to be stored
|
||||||
"""
|
"""
|
||||||
from flaschengeist.models.setting import _PluginSetting
|
|
||||||
from flaschengeist.database import db
|
|
||||||
|
|
||||||
setting = (
|
setting = (
|
||||||
_PluginSetting.query.filter(_PluginSetting.plugin == self.name)
|
_PluginSetting.query.filter(_PluginSetting.plugin == self.name)
|
||||||
.filter(_PluginSetting.name == name)
|
.filter(_PluginSetting.name == name)
|
||||||
|
@ -218,9 +141,6 @@ class Plugin:
|
||||||
|
|
||||||
Hint: use the data for frontend actions.
|
Hint: use the data for frontend actions.
|
||||||
"""
|
"""
|
||||||
from flaschengeist.models.notification import Notification
|
|
||||||
from flaschengeist.database import db
|
|
||||||
|
|
||||||
if not user.deleted:
|
if not user.deleted:
|
||||||
n = Notification(text=text, data=data, plugin=self.id, user_=user)
|
n = Notification(text=text, data=data, plugin=self.id, user_=user)
|
||||||
db.session.add(n)
|
db.session.add(n)
|
||||||
|
@ -237,11 +157,6 @@ class Plugin:
|
||||||
|
|
||||||
|
|
||||||
class AuthPlugin(Plugin):
|
class AuthPlugin(Plugin):
|
||||||
"""Base class for all authentification plugins
|
|
||||||
|
|
||||||
See also `Plugin`
|
|
||||||
"""
|
|
||||||
|
|
||||||
def login(self, user, pw):
|
def login(self, user, pw):
|
||||||
"""Login routine, MUST BE IMPLEMENTED!
|
"""Login routine, MUST BE IMPLEMENTED!
|
||||||
|
|
||||||
|
@ -304,7 +219,7 @@ class AuthPlugin(Plugin):
|
||||||
"""
|
"""
|
||||||
raise MethodNotAllowed
|
raise MethodNotAllowed
|
||||||
|
|
||||||
def get_avatar(self, user):
|
def get_avatar(self, user: User) -> _Avatar:
|
||||||
"""Retrieve avatar for given user (if supported by auth backend)
|
"""Retrieve avatar for given user (if supported by auth backend)
|
||||||
|
|
||||||
Default behavior is to use native Image objects,
|
Default behavior is to use native Image objects,
|
||||||
|
@ -318,23 +233,21 @@ class AuthPlugin(Plugin):
|
||||||
"""
|
"""
|
||||||
raise NotFound
|
raise NotFound
|
||||||
|
|
||||||
def set_avatar(self, user, file):
|
def set_avatar(self, user: User, file: FileStorage):
|
||||||
"""Set the avatar for given user (if supported by auth backend)
|
"""Set the avatar for given user (if supported by auth backend)
|
||||||
|
|
||||||
Default behavior is to use native Image objects stored on the Flaschengeist server
|
Default behavior is to use native Image objects stored on the Flaschengeist server
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
user: User to set the avatar for
|
user: User to set the avatar for
|
||||||
file: `werkzeug.datastructures.FileStorage` uploaded by the user
|
file: FileStorage object uploaded by the user
|
||||||
Raises:
|
Raises:
|
||||||
MethodNotAllowed: If not supported by Backend
|
MethodNotAllowed: If not supported by Backend
|
||||||
Any valid HTTP exception
|
Any valid HTTP exception
|
||||||
"""
|
"""
|
||||||
from flaschengeist.controller import imageController
|
|
||||||
|
|
||||||
user.avatar_ = imageController.upload_image(file)
|
user.avatar_ = imageController.upload_image(file)
|
||||||
|
|
||||||
def delete_avatar(self, user):
|
def delete_avatar(self, user: User):
|
||||||
"""Delete the avatar for given user (if supported by auth backend)
|
"""Delete the avatar for given user (if supported by auth backend)
|
||||||
|
|
||||||
Default behavior is to use the imageController and native Image objects.
|
Default behavior is to use the imageController and native Image objects.
|
||||||
|
|
|
@ -13,8 +13,8 @@ from flaschengeist.controller import sessionController, userController
|
||||||
|
|
||||||
|
|
||||||
class AuthRoutePlugin(Plugin):
|
class AuthRoutePlugin(Plugin):
|
||||||
id = "dev.flaschengeist.auth"
|
name = "auth"
|
||||||
blueprint = Blueprint("auth", __name__)
|
blueprint = Blueprint(name, __name__)
|
||||||
|
|
||||||
|
|
||||||
@AuthRoutePlugin.blueprint.route("/auth", methods=["POST"])
|
@AuthRoutePlugin.blueprint.route("/auth", methods=["POST"])
|
||||||
|
|
|
@ -17,8 +17,6 @@ from flaschengeist.plugins import AuthPlugin, before_role_updated
|
||||||
|
|
||||||
|
|
||||||
class AuthLDAP(AuthPlugin):
|
class AuthLDAP(AuthPlugin):
|
||||||
id = "auth_ldap"
|
|
||||||
|
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
app.config.update(
|
app.config.update(
|
||||||
|
|
|
@ -14,8 +14,6 @@ from flaschengeist import logger
|
||||||
|
|
||||||
|
|
||||||
class AuthPlain(AuthPlugin):
|
class AuthPlain(AuthPlugin):
|
||||||
id = "auth_plain"
|
|
||||||
|
|
||||||
def install(self):
|
def install(self):
|
||||||
plugins_installed(self.post_install)
|
plugins_installed(self.post_install)
|
||||||
|
|
||||||
|
|
|
@ -57,10 +57,9 @@ def service_debit():
|
||||||
|
|
||||||
|
|
||||||
class BalancePlugin(Plugin):
|
class BalancePlugin(Plugin):
|
||||||
"""Balance Plugin"""
|
name = "balance"
|
||||||
|
|
||||||
id = "dev.flaschengeist.balance"
|
id = "dev.flaschengeist.balance"
|
||||||
blueprint = Blueprint("balance", __name__)
|
blueprint = Blueprint(name, __name__)
|
||||||
permissions = permissions.permissions
|
permissions = permissions.permissions
|
||||||
plugin: "BalancePlugin" = LocalProxy(lambda: current_app.config["FG_PLUGINS"][BalancePlugin.name])
|
plugin: "BalancePlugin" = LocalProxy(lambda: current_app.config["FG_PLUGINS"][BalancePlugin.name])
|
||||||
models = models
|
models = models
|
||||||
|
|
|
@ -131,7 +131,7 @@ def get_balance(userid, current_session: Session):
|
||||||
|
|
||||||
Route: ``/users/<userid>/balance`` | Method: ``GET``
|
Route: ``/users/<userid>/balance`` | Method: ``GET``
|
||||||
|
|
||||||
GET-parameters: ``{from?: string, to?: string}``
|
GET-parameters: ```{from?: string, to?: string}```
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
userid: Userid of user to get balance from
|
userid: Userid of user to get balance from
|
||||||
|
@ -170,7 +170,7 @@ def get_transactions(userid, current_session: Session):
|
||||||
|
|
||||||
Route: ``/users/<userid>/balance/transactions`` | Method: ``GET``
|
Route: ``/users/<userid>/balance/transactions`` | Method: ``GET``
|
||||||
|
|
||||||
GET-parameters: ``{from?: string, to?: string, limit?: int, offset?: int}``
|
GET-parameters: ```{from?: string, to?: string, limit?: int, offset?: int}```
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
userid: Userid of user to get transactions from
|
userid: Userid of user to get transactions from
|
||||||
|
|
|
@ -12,8 +12,6 @@ from . import Plugin
|
||||||
|
|
||||||
|
|
||||||
class MailMessagePlugin(Plugin):
|
class MailMessagePlugin(Plugin):
|
||||||
id = "dev.flaschengeist.mail_plugin"
|
|
||||||
|
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.server = config["SERVER"]
|
self.server = config["SERVER"]
|
||||||
|
|
|
@ -17,9 +17,9 @@ from . import pricelist_controller, permissions
|
||||||
|
|
||||||
|
|
||||||
class PriceListPlugin(Plugin):
|
class PriceListPlugin(Plugin):
|
||||||
id = "dev.flaschengeist.pricelist"
|
name = "pricelist"
|
||||||
permissions = permissions.permissions
|
permissions = permissions.permissions
|
||||||
blueprint = Blueprint("pricelist", __name__, url_prefix="/pricelist")
|
blueprint = Blueprint(name, __name__, url_prefix="/pricelist")
|
||||||
plugin = LocalProxy(lambda: current_app.config["FG_PLUGINS"][PriceListPlugin.name])
|
plugin = LocalProxy(lambda: current_app.config["FG_PLUGINS"][PriceListPlugin.name])
|
||||||
models = models
|
models = models
|
||||||
|
|
||||||
|
|
|
@ -16,8 +16,8 @@ from . import permissions
|
||||||
|
|
||||||
|
|
||||||
class RolesPlugin(Plugin):
|
class RolesPlugin(Plugin):
|
||||||
id = "dev.flaschengeist.roles"
|
name = "roles"
|
||||||
blueprint = Blueprint("roles", __name__)
|
blueprint = Blueprint(name, __name__)
|
||||||
permissions = permissions.permissions
|
permissions = permissions.permissions
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -18,8 +18,8 @@ from flaschengeist.utils.datetime import from_iso_format
|
||||||
|
|
||||||
|
|
||||||
class UsersPlugin(Plugin):
|
class UsersPlugin(Plugin):
|
||||||
id = "dev.flaschengeist.users"
|
name = "users"
|
||||||
blueprint = Blueprint("users", __name__)
|
blueprint = Blueprint(name, __name__)
|
||||||
permissions = permissions.permissions
|
permissions = permissions.permissions
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,6 @@ _hooks_after = {}
|
||||||
|
|
||||||
def Hook(function=None, id=None):
|
def Hook(function=None, id=None):
|
||||||
"""Hook decorator
|
"""Hook decorator
|
||||||
|
|
||||||
Use to decorate functions as hooks, so plugins can hook up their custom functions.
|
Use to decorate functions as hooks, so plugins can hook up their custom functions.
|
||||||
"""
|
"""
|
||||||
# `id` passed as `arg` not `kwarg`
|
# `id` passed as `arg` not `kwarg`
|
||||||
|
@ -39,10 +38,8 @@ def Hook(function=None, id=None):
|
||||||
|
|
||||||
def HookBefore(id: str):
|
def HookBefore(id: str):
|
||||||
"""Decorator for functions to be called before a Hook-Function is called
|
"""Decorator for functions to be called before a Hook-Function is called
|
||||||
|
|
||||||
The hooked up function must accept the same arguments as the function hooked onto,
|
The hooked up function must accept the same arguments as the function hooked onto,
|
||||||
as the functions are called with the same arguments.
|
as the functions are called with the same arguments.
|
||||||
|
|
||||||
Hint: This enables you to modify the arguments!
|
Hint: This enables you to modify the arguments!
|
||||||
"""
|
"""
|
||||||
if not id or not isinstance(id, str):
|
if not id or not isinstance(id, str):
|
||||||
|
@ -57,18 +54,9 @@ def HookBefore(id: str):
|
||||||
|
|
||||||
def HookAfter(id: str):
|
def HookAfter(id: str):
|
||||||
"""Decorator for functions to be called after a Hook-Function is called
|
"""Decorator for functions to be called after a Hook-Function is called
|
||||||
|
|
||||||
As with the HookBefore, the hooked up function must accept the same
|
As with the HookBefore, the hooked up function must accept the same
|
||||||
arguments as the function hooked onto, but also receives a
|
arguments as the function hooked onto, but also receives a
|
||||||
`hook_result` kwarg containing the result of the function.
|
`hook_result` kwarg containing the result of the function.
|
||||||
|
|
||||||
Example:
|
|
||||||
```py
|
|
||||||
@HookAfter("some.id")
|
|
||||||
def my_func(hook_result):
|
|
||||||
# This function is executed after the function registered with "some.id"
|
|
||||||
print(hook_result) # This is the result of the function
|
|
||||||
```
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not id or not isinstance(id, str):
|
if not id or not isinstance(id, str):
|
||||||
|
|
|
@ -1,46 +0,0 @@
|
||||||
"""Plugin utils
|
|
||||||
|
|
||||||
Utilities for handling Flaschengeist plugins
|
|
||||||
"""
|
|
||||||
|
|
||||||
import pkg_resources
|
|
||||||
from flaschengeist import logger
|
|
||||||
from flaschengeist.plugins import Plugin
|
|
||||||
|
|
||||||
|
|
||||||
def get_plugins() -> list[type[Plugin]]:
|
|
||||||
"""Get all installed plugins for Flaschengeist
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
list of classes implementing `flaschengeist.plugins.Plugin`
|
|
||||||
"""
|
|
||||||
logger.debug("Search for plugins")
|
|
||||||
|
|
||||||
plugins = []
|
|
||||||
for entry_point in pkg_resources.iter_entry_points("flaschengeist.plugins"):
|
|
||||||
try:
|
|
||||||
logger.debug(f"Found plugin: >{entry_point.name}<")
|
|
||||||
plugin_class = entry_point.load()
|
|
||||||
if issubclass(plugin_class, Plugin):
|
|
||||||
plugins.append(plugin_class)
|
|
||||||
except TypeError:
|
|
||||||
logger.error(f"Invalid entry point for plugin {entry_point.name} found.")
|
|
||||||
return plugins
|
|
||||||
|
|
||||||
|
|
||||||
def plugin_version(plugin: type[Plugin]) -> str:
|
|
||||||
"""Get version of plugin
|
|
||||||
|
|
||||||
Returns the version of a plugin, if plugin does not set the
|
|
||||||
version property, the version of the package providing the
|
|
||||||
plugin is taken.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
plugin: Plugin or Plugin class
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Version as string
|
|
||||||
"""
|
|
||||||
if plugin.version:
|
|
||||||
return plugin.version
|
|
||||||
return pkg_resources.get_distribution(plugin.__module__.split(".", 1)[0]).version
|
|
Loading…
Reference in New Issue