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 .plugins import AuthPlugin
from flaschengeist.config import config, configure_app from flaschengeist.config import config, configure_app
from flaschengeist.controller import roleController from flaschengeist.controller import roleController
from flaschengeist.utils.hook import Hook
class CustomJSONEncoder(JSONEncoder): class CustomJSONEncoder(JSONEncoder):
@ -35,6 +36,7 @@ class CustomJSONEncoder(JSONEncoder):
return JSONEncoder.default(self, o) return JSONEncoder.default(self, o)
@Hook("plugins.loaded")
def __load_plugins(app): def __load_plugins(app):
logger.debug("Search for plugins") logger.debug("Search for plugins")
@ -77,23 +79,20 @@ def __load_plugins(app):
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(): def install_all():
from flaschengeist.database import db from flaschengeist.database import db
db.create_all() db.create_all()
db.session.commit() db.session.commit()
installed = []
for name, plugin in current_app.config["FG_PLUGINS"].items(): for name, plugin in current_app.config["FG_PLUGINS"].items():
if not plugin: if not plugin:
logger.debug(f"Skip disabled plugin: {name}") logger.debug(f"Skip disabled plugin: {name}")
continue continue
logger.info(f"Install plugin {name}") logger.info(f"Install plugin {name}")
plugin.install() plugin.install()
installed.append(plugin)
if plugin.permissions: if plugin.permissions:
roleController.create_permissions(plugin.permissions) roleController.create_permissions(plugin.permissions)
for plugin in installed:
plugin.post_install()
def create_app(test_config=None): 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.models.setting import _PluginSetting
from flaschengeist.utils.hook import HookBefore, HookAfter 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") before_role_updated = HookBefore("update_role")
"""Hook decorator for when roles are modified """Hook decorator for when roles are modified
Args: Args:
@ -57,12 +72,6 @@ class Plugin:
""" """
pass 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): def get_setting(self, name: str, **kwargs):
"""Get plugin setting from database """Get plugin setting from database
Args: Args:

View File

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

View File

@ -1,43 +1,69 @@
_hook_dict = ({}, {}) from functools import wraps
class Hook(object): _hooks_before = {}
"""Decorator for Hooks _hooks_after = {}
Use to decorate system hooks where plugins should be able to hook in
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): if not id or not isinstance(id, str):
self.function = function raise TypeError("HookAfter requires the ID of the function to hook up")
def __call__(self, *args, **kwargs): def wrapped(function):
# Hooks before _hooks_after.setdefault(id, []).append(function)
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)
return function return function
return wrapped
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