feat(hooks): Some more work on the hooks functions

This commit is contained in:
Ferdinand Thiessen 2021-12-17 14:27:27 +01:00
parent cfac55efe0
commit 38ebaf0e79
4 changed files with 84 additions and 47 deletions

View File

@ -12,6 +12,7 @@ from . import logger
from .plugins import AuthPlugin
from flaschengeist.config import config, configure_app
from flaschengeist.controller import roleController
from flaschengeist.utils.hook import Hook
class CustomJSONEncoder(JSONEncoder):
@ -35,6 +36,7 @@ class CustomJSONEncoder(JSONEncoder):
return JSONEncoder.default(self, o)
@Hook("plugins.loaded")
def __load_plugins(app):
logger.debug("Search for plugins")
@ -77,23 +79,20 @@ def __load_plugins(app):
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()
installed = []
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()
installed.append(plugin)
if plugin.permissions:
roleController.create_permissions(plugin.permissions)
for plugin in installed:
plugin.post_install()
def create_app(test_config=None):

View File

@ -10,6 +10,21 @@ from flaschengeist.models.user import _Avatar, User
from flaschengeist.models.setting import _PluginSetting
from flaschengeist.utils.hook import HookBefore, HookAfter
plugins_installed = HookAfter("plugins.installed")
"""Hook decorator for when all plugins are installed
Possible use case would be to populate the database with some presets.
Args:
hook_result: void (kwargs)
"""
plugins_loaded = HookAfter("plugins.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
Args:
app: Current flask app instance (args)
hook_result: void (kwargs)
"""
before_role_updated = HookBefore("update_role")
"""Hook decorator for when roles are modified
Args:
@ -57,12 +72,6 @@ class Plugin:
"""
pass
def post_install(self):
"""Fill database or do other stuff
Called after all plugins are installed
"""
pass
def get_setting(self, name: str, **kwargs):
"""Get plugin setting from database
Args:

View File

@ -7,13 +7,16 @@ import os
import hashlib
import binascii
from werkzeug.exceptions import BadRequest
from flaschengeist.plugins import AuthPlugin
from flaschengeist.plugins import AuthPlugin, plugins_installed
from flaschengeist.models.user import User, Role, Permission
from flaschengeist.database import db
from flaschengeist import logger
class AuthPlain(AuthPlugin):
def install(self):
plugins_installed(self.post_install)
def post_install(self):
if User.query.filter(User.deleted == False).count() == 0:
logger.info("Installing admin user")

View File

@ -1,43 +1,69 @@
_hook_dict = ({}, {})
from functools import wraps
class Hook(object):
"""Decorator for Hooks
Use to decorate system hooks where plugins should be able to hook in
_hooks_before = {}
_hooks_after = {}
def Hook(function=None, id=None):
"""Hook decorator
Use to decorate functions as hooks, so plugins can hook up their custom functions.
"""
# `id` passed as `arg` not `kwarg`
if isinstance(function, str):
return Hook(id=function)
def decorator(function):
@wraps(function)
def wrapped(*args, **kwargs):
_id = id if id is not None else function.__qualname__
# Hooks before
for f in _hooks_before.get(_id, []):
f(*args, **kwargs)
# Main function
result = function(*args, **kwargs)
# Hooks after
for f in _hooks_after.get(_id, []):
f(*args, hook_result=result, **kwargs)
return result
return wrapped
# Called @Hook or we are in the second step
if callable(function):
return decorator(function)
else:
return decorator
def HookBefore(id: str):
"""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,
as the functions are called with the same arguments.
Hint: This enables you to modify the arguments!
"""
if not id or not isinstance(id, str):
raise TypeError("HookBefore requires the ID of the function to hook up")
def wrapped(function):
_hooks_before.setdefault(id, []).append(function)
return function
return wrapped
def HookAfter(id: str):
"""Decorator for functions to be called after a Hook-Function is called
As with the HookBefore, the hooked up function must accept the same
arguments as the function hooked onto, but also receives a
`hook_result` kwarg containing the result of the function.
"""
def __init__(self, function):
self.function = function
if not id or not isinstance(id, str):
raise TypeError("HookAfter requires the ID of the function to hook up")
def __call__(self, *args, **kwargs):
# Hooks before
for function in _hook_dict[0].get(self.function.__name__, []):
function(*args, **kwargs)
# Main function
ret = self.function(*args, **kwargs)
# Hooks after
for function in _hook_dict[1].get(self.function.__name__, []):
function(*args, **kwargs)
return ret
class HookBefore(object):
"""Decorator for functions to be called before a Hook-Function is called"""
def __init__(self, name):
self.name = name
def __call__(self, function):
_hook_dict[0].setdefault(self.name, []).append(function)
def wrapped(function):
_hooks_after.setdefault(id, []).append(function)
return function
class HookAfter(object):
"""Decorator for functions to be called after a Hook-Function is called"""
def __init__(self, name):
self.name = name
def __call__(self, function):
_hook_dict[1].setdefault(self.name, []).append(function)
return function
return wrapped