2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-30 04:26:44 +00:00

Fixes for unit tests

This commit is contained in:
Oliver 2022-01-04 21:36:27 +11:00
parent 928b90a833
commit ac849c1566
10 changed files with 116 additions and 69 deletions

View File

@ -8,30 +8,38 @@ import plugin.registry as registry
def plugin_update(queryset, new_status: bool): def plugin_update(queryset, new_status: bool):
"""general function for bulk changing plugins""" """
General function for bulk changing plugins
"""
apps_changed = False apps_changed = False
# run through all plugins in the queryset as the save method needs to be overridden # Run through all plugins in the queryset as the save method needs to be overridden
for plugin in queryset: for plugin in queryset:
if plugin.active is not new_status: if plugin.active is not new_status:
plugin.active = new_status plugin.active = new_status
plugin.save(no_reload=True) plugin.save(no_reload=True)
apps_changed = True apps_changed = True
# reload plugins if they changed # Reload plugins if they changed
if apps_changed: if apps_changed:
registry.plugin_registry.reload_plugins() registry.plugin_registry.reload_plugins()
@admin.action(description='Activate plugin(s)') @admin.action(description='Activate plugin(s)')
def plugin_activate(modeladmin, request, queryset): def plugin_activate(modeladmin, request, queryset):
"""activate a set of plugins""" """
Activate a set of plugins
"""
plugin_update(queryset, True) plugin_update(queryset, True)
@admin.action(description='Deactivate plugin(s)') @admin.action(description='Deactivate plugin(s)')
def plugin_deactivate(modeladmin, request, queryset): def plugin_deactivate(modeladmin, request, queryset):
"""deactivate a set of plugins""" """
Deactivate a set of plugins
"""
plugin_update(queryset, False) plugin_update(queryset, False)
@ -51,7 +59,10 @@ class PluginSettingInline(admin.TabularInline):
class PluginConfigAdmin(admin.ModelAdmin): class PluginConfigAdmin(admin.ModelAdmin):
"""Custom admin with restricted id fields""" """
Custom admin with restricted id fields
"""
readonly_fields = ["key", "name", ] readonly_fields = ["key", "name", ]
list_display = ['name', 'key', '__str__', 'active', ] list_display = ['name', 'key', '__str__', 'active', ]
list_filter = ['active'] list_filter = ['active']

View File

@ -34,17 +34,9 @@ class SettingsMixin:
Return the 'value' of the setting associated with this plugin Return the 'value' of the setting associated with this plugin
""" """
# Find the plugin configuration associated with this plugin return PluginSetting.get_setting(key, plugin=self)
plugin = self.plugin_config() def set_setting(self, key, value, user=None):
if plugin:
return PluginSetting.get_setting(key, plugin=plugin, settings=self.settings)
else:
# Plugin cannot be found, return default value
return PluginSetting.get_setting_default(key, settings=self.settings)
def set_setting(self, key, value, user):
""" """
Set plugin setting value by key Set plugin setting value by key
""" """
@ -58,7 +50,7 @@ class SettingsMixin:
# Cannot find associated plugin model, return # Cannot find associated plugin model, return
return return
PluginSetting.set_setting(key, value, user, plugin=plugin, settings=self.settings) PluginSetting.set_setting(key, value, user, plugin=plugin)
class UrlsMixin: class UrlsMixin:

View File

@ -19,19 +19,27 @@ logger = logging.getLogger("inventree")
class MixinBase: class MixinBase:
"""general base for mixins""" """
General base for mixins
"""
def __init__(self) -> None: def __init__(self) -> None:
self._mixinreg = {} self._mixinreg = {}
self._mixins = {} self._mixins = {}
def add_mixin(self, key: str, fnc_enabled=True, cls=None): def add_mixin(self, key: str, fnc_enabled=True, cls=None):
"""add a mixin to the plugins registry""" """
Add a mixin to the plugins registry
"""
self._mixins[key] = fnc_enabled self._mixins[key] = fnc_enabled
self.setup_mixin(key, cls=cls) self.setup_mixin(key, cls=cls)
def setup_mixin(self, key, cls=None): def setup_mixin(self, key, cls=None):
"""define mixin details for the current mixin -> provides meta details for all active mixins""" """
Define mixin details for the current mixin -> provides meta details for all active mixins
"""
# get human name # get human name
human_name = getattr(cls.MixinMeta, 'MIXIN_NAME', key) if cls and hasattr(cls, 'MixinMeta') else key human_name = getattr(cls.MixinMeta, 'MIXIN_NAME', key) if cls and hasattr(cls, 'MixinMeta') else key
@ -43,7 +51,10 @@ class MixinBase:
@property @property
def registered_mixins(self, with_base: bool = False): def registered_mixins(self, with_base: bool = False):
"""get all registered mixins for the plugin""" """
Get all registered mixins for the plugin
"""
mixins = getattr(self, '_mixinreg', None) mixins = getattr(self, '_mixinreg', None)
if mixins: if mixins:
# filter out base # filter out base

View File

@ -18,9 +18,9 @@ class InvenTreePlugin():
# Override the plugin name for each concrete plugin instance # Override the plugin name for each concrete plugin instance
PLUGIN_NAME = '' PLUGIN_NAME = ''
PLUGIN_SLUG = '' PLUGIN_SLUG = None
PLUGIN_TITLE = '' PLUGIN_TITLE = None
def plugin_name(self): def plugin_name(self):
""" """
@ -35,11 +35,14 @@ class InvenTreePlugin():
if slug is None: if slug is None:
slug = self.plugin_name() slug = self.plugin_name()
return slugify(slug) return slugify(slug.lower())
def plugin_title(self): def plugin_title(self):
if self.PLUGIN_TITLE:
return self.PLUGIN_TITLE return self.PLUGIN_TITLE
else:
return self.plugin_name()
def plugin_config(self, raise_error=False): def plugin_config(self, raise_error=False):
""" """

View File

@ -1,7 +1,10 @@
""" """
registry for plugins Registry for loading and managing multiple plugins at run-time
holds the class and the object that contains all code to maintain plugin states
- Holds the class and the object that contains all code to maintain plugin states
- Manages setup and teardown of plugin class instances
""" """
import importlib import importlib
import pathlib import pathlib
import logging import logging
@ -34,6 +37,10 @@ logger = logging.getLogger('inventree')
class PluginsRegistry: class PluginsRegistry:
"""
The PluginsRegistry class
"""
def __init__(self) -> None: def __init__(self) -> None:
# plugin registry # plugin registry
self.plugins = {} self.plugins = {}
@ -54,11 +61,15 @@ class PluginsRegistry:
# region public plugin functions # region public plugin functions
def load_plugins(self): def load_plugins(self):
"""load and activate all IntegrationPlugins""" """
Load and activate all IntegrationPlugins
"""
from plugin.helpers import log_plugin_error from plugin.helpers import log_plugin_error
logger.info('Start loading plugins') logger.info('Start loading plugins')
# set maintanace mode
# Set maintanace mode
_maintenance = bool(get_maintenance_mode()) _maintenance = bool(get_maintenance_mode())
if not _maintenance: if not _maintenance:
set_maintenance_mode(True) set_maintenance_mode(True)
@ -68,7 +79,7 @@ class PluginsRegistry:
retry_counter = settings.PLUGIN_RETRY retry_counter = settings.PLUGIN_RETRY
while not registered_sucessfull: while not registered_sucessfull:
try: try:
# we are using the db so for migrations etc we need to try this block # We are using the db so for migrations etc we need to try this block
self._init_plugins(blocked_plugin) self._init_plugins(blocked_plugin)
self._activate_plugins() self._activate_plugins()
registered_sucessfull = True registered_sucessfull = True
@ -81,13 +92,14 @@ class PluginsRegistry:
log_plugin_error({error.path: error.message}, 'load') log_plugin_error({error.path: error.message}, 'load')
blocked_plugin = error.path # we will not try to load this app again blocked_plugin = error.path # we will not try to load this app again
# init apps without any integration plugins # Initialize apps without any integration plugins
self._clean_registry() self._clean_registry()
self._clean_installed_apps() self._clean_installed_apps()
self._activate_plugins(force_reload=True) self._activate_plugins(force_reload=True)
# we do not want to end in an endless loop # We do not want to end in an endless loop
retry_counter -= 1 retry_counter -= 1
if retry_counter <= 0: if retry_counter <= 0:
if settings.PLUGIN_TESTING: if settings.PLUGIN_TESTING:
print('[PLUGIN] Max retries, breaking loading') print('[PLUGIN] Max retries, breaking loading')
@ -98,15 +110,20 @@ class PluginsRegistry:
# now the loading will re-start up with init # now the loading will re-start up with init
# remove maintenance # Remove maintenance mode
if not _maintenance: if not _maintenance:
set_maintenance_mode(False) set_maintenance_mode(False)
logger.info('Finished loading plugins') logger.info('Finished loading plugins')
def unload_plugins(self): def unload_plugins(self):
"""unload and deactivate all IntegrationPlugins""" """
Unload and deactivate all IntegrationPlugins
"""
logger.info('Start unloading plugins') logger.info('Start unloading plugins')
# set maintanace mode
# Set maintanace mode
_maintenance = bool(get_maintenance_mode()) _maintenance = bool(get_maintenance_mode())
if not _maintenance: if not _maintenance:
set_maintenance_mode(True) set_maintenance_mode(True)
@ -123,21 +140,27 @@ class PluginsRegistry:
logger.info('Finished unloading plugins') logger.info('Finished unloading plugins')
def reload_plugins(self): def reload_plugins(self):
"""safely reload IntegrationPlugins""" """
# do not reload whe currently loading Safely reload IntegrationPlugins
"""
# Do not reload whe currently loading
if self.is_loading: if self.is_loading:
return return
logger.info('Start reloading plugins') logger.info('Start reloading plugins')
with maintenance_mode_on(): with maintenance_mode_on():
self.unload_plugins() self.unload_plugins()
self.load_plugins() self.load_plugins()
logger.info('Finished reloading plugins')
# endregion
# region general plugin managment mechanisms logger.info('Finished reloading plugins')
def collect_plugins(self): def collect_plugins(self):
"""collect integration plugins from all possible ways of loading""" """
Collect integration plugins from all possible ways of loading
"""
self.plugin_modules = [] # clear self.plugin_modules = [] # clear
# Collect plugins from paths # Collect plugins from paths
@ -146,7 +169,7 @@ class PluginsRegistry:
if modules: if modules:
[self.plugin_modules.append(item) for item in modules] [self.plugin_modules.append(item) for item in modules]
# check if not running in testing mode and apps should be loaded from hooks # Check if not running in testing mode and apps should be loaded from hooks
if (not settings.PLUGIN_TESTING) or (settings.PLUGIN_TESTING and settings.PLUGIN_TESTING_SETUP): if (not settings.PLUGIN_TESTING) or (settings.PLUGIN_TESTING and settings.PLUGIN_TESTING_SETUP):
# Collect plugins from setup entry points # Collect plugins from setup entry points
for entry in metadata.entry_points().get('inventree_plugins', []): for entry in metadata.entry_points().get('inventree_plugins', []):
@ -159,22 +182,25 @@ class PluginsRegistry:
logger.info(", ".join([a.__module__ for a in self.plugin_modules])) logger.info(", ".join([a.__module__ for a in self.plugin_modules]))
def _init_plugins(self, disabled=None): def _init_plugins(self, disabled=None):
"""initialise all found plugins """
Initialise all found plugins
:param disabled: loading path of disabled app, defaults to None :param disabled: loading path of disabled app, defaults to None
:type disabled: str, optional :type disabled: str, optional
:raises error: IntegrationPluginError :raises error: IntegrationPluginError
""" """
from plugin.models import PluginConfig from plugin.models import PluginConfig
logger.info('Starting plugin initialisation') logger.info('Starting plugin initialisation')
# Initialize integration plugins # Initialize integration plugins
for plugin in self.plugin_modules: for plugin in self.plugin_modules:
# check if package # Check if package
was_packaged = getattr(plugin, 'is_package', False) was_packaged = getattr(plugin, 'is_package', False)
# check if activated # Check if activated
# these checks only use attributes - never use plugin supplied functions -> that would lead to arbitrary code execution!! # These checks only use attributes - never use plugin supplied functions -> that would lead to arbitrary code execution!!
plug_name = plugin.PLUGIN_NAME plug_name = plugin.PLUGIN_NAME
plug_key = plugin.PLUGIN_SLUG if getattr(plugin, 'PLUGIN_SLUG', None) else plug_name plug_key = plugin.PLUGIN_SLUG if getattr(plugin, 'PLUGIN_SLUG', None) else plug_name
plug_key = slugify(plug_key) # keys are slugs! plug_key = slugify(plug_key) # keys are slugs!
@ -186,23 +212,23 @@ class PluginsRegistry:
raise error raise error
plugin_db_setting = None plugin_db_setting = None
# always activate if testing # Always activate if testing
if settings.PLUGIN_TESTING or (plugin_db_setting and plugin_db_setting.active): if settings.PLUGIN_TESTING or (plugin_db_setting and plugin_db_setting.active):
# check if the plugin was blocked -> threw an error # Check if the plugin was blocked -> threw an error
if disabled: if disabled:
# option1: package, option2: file-based # option1: package, option2: file-based
if (plugin.__name__ == disabled) or (plugin.__module__ == disabled): if (plugin.__name__ == disabled) or (plugin.__module__ == disabled):
# errors are bad so disable the plugin in the database # Errors are bad so disable the plugin in the database
if not settings.PLUGIN_TESTING: if not settings.PLUGIN_TESTING:
plugin_db_setting.active = False plugin_db_setting.active = False
# TODO save the error to the plugin # TODO save the error to the plugin
plugin_db_setting.save(no_reload=True) plugin_db_setting.save(no_reload=True)
# add to inactive plugins so it shows up in the ui # Add to inactive plugins so it shows up in the ui
self.plugins_inactive[plug_key] = plugin_db_setting self.plugins_inactive[plug_key] = plugin_db_setting
continue # continue -> the plugin is not loaded continue # continue -> the plugin is not loaded
# init package # Initialize package
# now we can be sure that an admin has activated the plugin # now we can be sure that an admin has activated the plugin
# TODO check more stuff -> as of Nov 2021 there are not many checks in place # TODO check more stuff -> as of Nov 2021 there are not many checks in place
# but we could enhance those to check signatures, run the plugin against a whitelist etc. # but we could enhance those to check signatures, run the plugin against a whitelist etc.
@ -235,7 +261,7 @@ class PluginsRegistry:
plugins = self.plugins.items() plugins = self.plugins.items()
logger.info(f'Found {len(plugins)} active plugins') logger.info(f'Found {len(plugins)} active plugins')
self.activate_integration_globalsettings(plugins) self.activate_integration_settings(plugins)
self.activate_integration_app(plugins, force_reload=force_reload) self.activate_integration_app(plugins, force_reload=force_reload)
def _deactivate_plugins(self): def _deactivate_plugins(self):
@ -243,10 +269,9 @@ class PluginsRegistry:
Run integration deactivation functions for all plugins Run integration deactivation functions for all plugins
""" """
self.deactivate_integration_app() self.deactivate_integration_app()
self.deactivate_integration_globalsettings() self.deactivate_integration_settings()
# endregion
def activate_integration_globalsettings(self, plugins): def activate_integration_settings(self, plugins):
from common.models import InvenTreeSetting from common.models import InvenTreeSetting
if settings.PLUGIN_TESTING or InvenTreeSetting.get_setting('ENABLE_PLUGINS_GLOBALSETTING'): if settings.PLUGIN_TESTING or InvenTreeSetting.get_setting('ENABLE_PLUGINS_GLOBALSETTING'):
@ -256,7 +281,7 @@ class PluginsRegistry:
plugin_setting = plugin.settings plugin_setting = plugin.settings
self.mixins_settings[slug] = plugin_setting self.mixins_settings[slug] = plugin_setting
def deactivate_integration_globalsettings(self): def deactivate_integration_settings(self):
# collect all settings # collect all settings
plugin_settings = {} plugin_settings = {}
@ -267,7 +292,6 @@ class PluginsRegistry:
# clear cache # clear cache
self.mixins_Fsettings = {} self.mixins_Fsettings = {}
# region integration_app
def activate_integration_app(self, plugins, force_reload=False): def activate_integration_app(self, plugins, force_reload=False):
"""activate AppMixin plugins - add custom apps and reload """activate AppMixin plugins - add custom apps and reload
@ -441,8 +465,6 @@ class PluginsRegistry:
return True, [] return True, []
except Exception as error: except Exception as error:
get_plugin_error(error, do_raise=True) get_plugin_error(error, do_raise=True)
# endregion
# endregion
plugin_registry = PluginsRegistry() plugin_registry = PluginsRegistry()

View File

@ -1,5 +1,5 @@
""" """
JSON serializers for Stock app JSON serializers for plugin app
""" """
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
@ -20,7 +20,8 @@ from plugin.models import PluginConfig, PluginSetting
class PluginConfigSerializer(serializers.ModelSerializer): class PluginConfigSerializer(serializers.ModelSerializer):
""" Serializer for a PluginConfig: """
Serializer for a PluginConfig:
""" """
meta = serializers.DictField(read_only=True) meta = serializers.DictField(read_only=True)
@ -73,7 +74,7 @@ class PluginConfigInstallSerializer(serializers.Serializer):
if not data.get('confirm'): if not data.get('confirm'):
raise ValidationError({'confirm': _('Installation not confirmed')}) raise ValidationError({'confirm': _('Installation not confirmed')})
if (not data.get('url')) and (not data.get('packagename')): if (not data.get('url')) and (not data.get('packagename')):
msg = _('Either packagenmae of url must be provided') msg = _('Either packagename of URL must be provided')
raise ValidationError({'url': msg, 'packagename': msg}) raise ValidationError({'url': msg, 'packagename': msg})
return data return data
@ -115,7 +116,7 @@ class PluginConfigInstallSerializer(serializers.Serializer):
ret['result'] = str(error.output, 'utf-8') ret['result'] = str(error.output, 'utf-8')
ret['error'] = True ret['error'] = True
# register plugins # Register plugins
# TODO # TODO
return ret return ret

View File

@ -19,7 +19,7 @@ class PluginDetailAPITest(InvenTreeAPITestCase):
] ]
def setUp(self): def setUp(self):
self.MSG_NO_PKG = 'Either packagenmae of url must be provided' self.MSG_NO_PKG = 'Either packagename of URL must be provided'
self.PKG_NAME = 'minimal' self.PKG_NAME = 'minimal'
super().setUp() super().setUp()

View File

@ -42,7 +42,7 @@ class SettingsMixinTest(BaseMixinDefinition, TestCase):
def test_function(self): def test_function(self):
# settings variable # settings variable
self.assertEqual(self.mixin.globalsettings, self.TEST_SETTINGS) self.assertEqual(self.mixin.settings, self.TEST_SETTINGS)
# calling settings # calling settings
# not existing # not existing

View File

@ -1,4 +1,6 @@
""" Unit tests for plugins """ """
Unit tests for plugins
"""
from django.test import TestCase from django.test import TestCase
@ -6,7 +8,6 @@ import plugin.plugin
import plugin.integration import plugin.integration
from plugin.samples.integration.sample import SampleIntegrationPlugin from plugin.samples.integration.sample import SampleIntegrationPlugin
from plugin.samples.integration.another_sample import WrongIntegrationPlugin, NoIntegrationPlugin from plugin.samples.integration.another_sample import WrongIntegrationPlugin, NoIntegrationPlugin
# from plugin.plugins import load_action_plugins, load_barcode_plugins
import plugin.templatetags.plugin_extras as plugin_tags import plugin.templatetags.plugin_extras as plugin_tags
from plugin import plugin_registry from plugin import plugin_registry

View File

@ -1,6 +1,7 @@
""" """
URL lookup for plugin app URL lookup for plugin app
""" """
from django.conf.urls import url, include from django.conf.urls import url, include
from plugin import plugin_registry from plugin import plugin_registry
@ -10,9 +11,14 @@ PLUGIN_BASE = 'plugin' # Constant for links
def get_plugin_urls(): def get_plugin_urls():
"""returns a urlpattern that can be integrated into the global urls""" """
Returns a urlpattern that can be integrated into the global urls
"""
urls = [] urls = []
for plugin in plugin_registry.plugins.values(): for plugin in plugin_registry.plugins.values():
if plugin.mixin_enabled('urls'): if plugin.mixin_enabled('urls'):
urls.append(plugin.urlpatterns) urls.append(plugin.urlpatterns)
return url(f'^{PLUGIN_BASE}/', include((urls, 'plugin'))) return url(f'^{PLUGIN_BASE}/', include((urls, 'plugin')))