From 8088bf28fe51d7e416764c7031149bbf95ba37d6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 10 Jan 2022 00:40:19 +0100 Subject: [PATCH 01/49] refactor ActionPlugin to use mixin --- InvenTree/plugin/action.py | 67 +++-------------- InvenTree/plugin/builtin/action/mixins.py | 73 +++++++++++++++++++ .../plugin/builtin/integration/mixins.py | 72 ++++++++++++++++++ InvenTree/plugin/mixins/__init__.py | 2 + 4 files changed, 157 insertions(+), 57 deletions(-) create mode 100644 InvenTree/plugin/builtin/action/mixins.py diff --git a/InvenTree/plugin/action.py b/InvenTree/plugin/action.py index 5e36c22e74..d0aee38230 100644 --- a/InvenTree/plugin/action.py +++ b/InvenTree/plugin/action.py @@ -2,69 +2,22 @@ """Class for ActionPlugin""" import logging +import warnings import plugin.plugin as plugin +from plugin.builtin.action.mixins import ActionMixin +import plugin.integration logger = logging.getLogger("inventree") -class ActionPlugin(plugin.InvenTreePlugin): +class ActionPlugin(ActionMixin, plugin.integration.IntegrationPluginBase): """ - The ActionPlugin class is used to perform custom actions + Legacy action definition - will be replaced + Please use the new Integration Plugin API and the Action mixin """ - - ACTION_NAME = "" - - @classmethod - def action_name(cls): - """ - Return the action name for this plugin. - If the ACTION_NAME parameter is empty, - look at the PLUGIN_NAME instead. - """ - action = cls.ACTION_NAME - - if not action: - action = cls.PLUGIN_NAME - - return action - - def __init__(self, user, data=None): - """ - An action plugin takes a user reference, and an optional dataset (dict) - """ - plugin.InvenTreePlugin.__init__(self) - - self.user = user - self.data = data - - def perform_action(self): - """ - Override this method to perform the action! - """ - - def get_result(self): - """ - Result of the action? - """ - - # Re-implement this for cutsom actions - return False - - def get_info(self): - """ - Extra info? Can be a string / dict / etc - """ - return None - - def get_response(self): - """ - Return a response. Default implementation is a simple response - which can be overridden. - """ - return { - "action": self.action_name(), - "result": self.get_result(), - "info": self.get_info(), - } + def __init__(self, user=None, data=None): + warnings.warn("using the ActionPlugin is depreceated", DeprecationWarning) + super().__init__() + self.init(user, data) diff --git a/InvenTree/plugin/builtin/action/mixins.py b/InvenTree/plugin/builtin/action/mixins.py new file mode 100644 index 0000000000..1f9b34c661 --- /dev/null +++ b/InvenTree/plugin/builtin/action/mixins.py @@ -0,0 +1,73 @@ +""" +Plugin mixin classes for action plugin +""" + +class ActionMixin: + """ + Mixin that enables custom actions + """ + ACTION_NAME = "" + + class MixinMeta: + """ + meta options for this mixin + """ + MIXIN_NAME = 'Actions' + + def __init__(self): + super().__init__() + self.add_mixin('action', 'has_action', __class__) + + @property + def has_action(self): + """ + Does this plugin have everything needed for an action? + """ + return True + + def action_name(self): + """ + Return the action name for this plugin. + If the ACTION_NAME parameter is empty, + look at the PLUGIN_NAME instead. + """ + if self.ACTION_NAME: + return self.ACTION_NAME + return self.name + + def init(self, user, data=None): + """ + An action plugin takes a user reference, and an optional dataset (dict) + """ + self.user = user + self.data = data + + def perform_action(self): + """ + Override this method to perform the action! + """ + + def get_result(self): + """ + Result of the action? + """ + + # Re-implement this for cutsom actions + return False + + def get_info(self): + """ + Extra info? Can be a string / dict / etc + """ + return None + + def get_response(self): + """ + Return a response. Default implementation is a simple response + which can be overridden. + """ + return { + "action": self.action_name(), + "result": self.get_result(), + "info": self.get_info(), + } diff --git a/InvenTree/plugin/builtin/integration/mixins.py b/InvenTree/plugin/builtin/integration/mixins.py index c6198ed7a1..14997fd452 100644 --- a/InvenTree/plugin/builtin/integration/mixins.py +++ b/InvenTree/plugin/builtin/integration/mixins.py @@ -301,3 +301,75 @@ class AppMixin: this plugin is always an app with this plugin """ return True + + +class ActionMixin: + """ + Mixin that enables custom actions + """ + ACTION_NAME = "" + + class MixinMeta: + """ + meta options for this mixin + """ + MIXIN_NAME = 'Action' + + def __init__(self): + super().__init__() + self.add_mixin('action', 'has_action', __class__) + + @property + def has_action(self): + """ + Does this plugin have everything needed for an action? + """ + return True + + @property + def action_name(self): + """ + Return the action name for this plugin. + If the ACTION_NAME parameter is empty, + look at the PLUGIN_NAME instead. + """ + if self.ACTION_NAME: + return self.ACTION_NAME + return self.name + + def init(self, user, data=None): + """ + An action plugin takes a user reference, and an optional dataset (dict) + """ + self.user = user + self.data = data + + def perform_action(self): + """ + Override this method to perform the action! + """ + + def get_result(self): + """ + Result of the action? + """ + + # Re-implement this for custom actions + return False + + def get_info(self): + """ + Extra info? Can be a string / dict / etc + """ + return None + + def get_response(self): + """ + Return a response. Default implementation is a simple response + which can be overridden. + """ + return { + "action": self.action_name(), + "result": self.get_result(), + "info": self.get_info(), + } diff --git a/InvenTree/plugin/mixins/__init__.py b/InvenTree/plugin/mixins/__init__.py index e9c910bb9e..389258a99a 100644 --- a/InvenTree/plugin/mixins/__init__.py +++ b/InvenTree/plugin/mixins/__init__.py @@ -3,6 +3,7 @@ Utility class to enable simpler imports """ from ..builtin.integration.mixins import AppMixin, SettingsMixin, ScheduleMixin, UrlsMixin, NavigationMixin +from ..builtin.action.mixins import ActionMixin __all__ = [ 'AppMixin', @@ -10,4 +11,5 @@ __all__ = [ 'ScheduleMixin', 'SettingsMixin', 'UrlsMixin', + 'ActionMixin', ] From bcb0f62e42580b30b98b7e703bd2f926b0787b51 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 10 Jan 2022 00:59:24 +0100 Subject: [PATCH 02/49] remove old loading mechanism --- InvenTree/InvenTree/api.py | 17 ++++------------- InvenTree/plugin/plugins.py | 11 ----------- InvenTree/plugin/registry.py | 10 ++++++++++ 3 files changed, 14 insertions(+), 24 deletions(-) diff --git a/InvenTree/InvenTree/api.py b/InvenTree/InvenTree/api.py index 8249a093aa..069fd9492d 100644 --- a/InvenTree/InvenTree/api.py +++ b/InvenTree/InvenTree/api.py @@ -5,8 +5,6 @@ Main JSON interface views # -*- coding: utf-8 -*- from __future__ import unicode_literals -import logging - from django.utils.translation import ugettext_lazy as _ from django.http import JsonResponse @@ -21,15 +19,7 @@ from .views import AjaxView from .version import inventreeVersion, inventreeApiVersion, inventreeInstanceName from .status import is_worker_running -from plugin.plugins import load_action_plugins - - -logger = logging.getLogger("inventree") - - -logger.info("Loading action plugins...") -action_plugins = load_action_plugins() - +from plugin import plugin_registry class InfoView(AjaxView): """ Simple JSON endpoint for InvenTree information. @@ -110,10 +100,11 @@ class ActionPluginView(APIView): 'error': _("No action specified") }) + action_plugins = plugin_registry.with_mixin('action') for plugin_class in action_plugins: if plugin_class.action_name() == action: - - plugin = plugin_class(request.user, data=data) + # TODO @matmair use easier syntax once InvenTree 0.7.0 is released + plugin = plugin_class.init(request.user, data=data) plugin.perform_action() diff --git a/InvenTree/plugin/plugins.py b/InvenTree/plugin/plugins.py index e2be1e6427..f0150495cb 100644 --- a/InvenTree/plugin/plugins.py +++ b/InvenTree/plugin/plugins.py @@ -8,10 +8,6 @@ import logging from django.core.exceptions import AppRegistryNotReady -# Action plugins -import plugin.builtin.action as action -from plugin.action import ActionPlugin - logger = logging.getLogger("inventree") @@ -97,13 +93,6 @@ def load_plugins(name: str, cls, module): return plugins -def load_action_plugins(): - """ - Return a list of all registered action plugins - """ - return load_plugins('action', ActionPlugin, action) - - def load_barcode_plugins(): """ Return a list of all registered barcode plugins diff --git a/InvenTree/plugin/registry.py b/InvenTree/plugin/registry.py index b7e37d22ba..907f1e179f 100644 --- a/InvenTree/plugin/registry.py +++ b/InvenTree/plugin/registry.py @@ -183,7 +183,17 @@ class PluginsRegistry: # Log collected plugins logger.info(f'Collected {len(self.plugin_modules)} plugins!') logger.info(", ".join([a.__module__ for a in self.plugin_modules])) + def with_mixin(self, mixin: str): + """ + Returns reference to all plugins that have a specified mixin enabled + """ + result = [] + for plugin in self.plugins.items(): + if plugin.mixin_enabled(mixin): + result.append(plugin) + + return result def _init_plugins(self, disabled=None): """ Initialise all found plugins From 6affc7550bc218e0429c75384f051541d440673e Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 10 Jan 2022 01:00:12 +0100 Subject: [PATCH 03/49] add depreciation TODO --- InvenTree/plugin/action.py | 1 + 1 file changed, 1 insertion(+) diff --git a/InvenTree/plugin/action.py b/InvenTree/plugin/action.py index d0aee38230..6800ed2076 100644 --- a/InvenTree/plugin/action.py +++ b/InvenTree/plugin/action.py @@ -17,6 +17,7 @@ class ActionPlugin(ActionMixin, plugin.integration.IntegrationPluginBase): Legacy action definition - will be replaced Please use the new Integration Plugin API and the Action mixin """ + # TODO @matmair remove this with InvenTree 0.7.0 def __init__(self, user=None, data=None): warnings.warn("using the ActionPlugin is depreceated", DeprecationWarning) super().__init__() From bb559deb5d80d70e79937513ce6c2894485db259 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 10 Jan 2022 01:00:45 +0100 Subject: [PATCH 04/49] add docstrings --- InvenTree/plugin/registry.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/InvenTree/plugin/registry.py b/InvenTree/plugin/registry.py index 907f1e179f..fc0113d51a 100644 --- a/InvenTree/plugin/registry.py +++ b/InvenTree/plugin/registry.py @@ -59,7 +59,8 @@ class PluginsRegistry: # mixins self.mixins_settings = {} - # region public plugin functions + # region public functions + # region loading / unloading def load_plugins(self): """ Load and activate all IntegrationPlugins @@ -183,6 +184,8 @@ class PluginsRegistry: # Log collected plugins logger.info(f'Collected {len(self.plugin_modules)} plugins!') logger.info(", ".join([a.__module__ for a in self.plugin_modules])) + # endregion + # region registry functions def with_mixin(self, mixin: str): """ Returns reference to all plugins that have a specified mixin enabled @@ -194,6 +197,10 @@ class PluginsRegistry: result.append(plugin) return result + # endregion + # endregion + + # region general internal loading /activating / deactivating / deloading def _init_plugins(self, disabled=None): """ Initialise all found plugins @@ -286,7 +293,9 @@ class PluginsRegistry: self.deactivate_integration_app() self.deactivate_integration_schedule() self.deactivate_integration_settings() + # endregion + # region mixin specific loading ... def activate_integration_settings(self, plugins): logger.info('Activating plugin settings') @@ -533,6 +542,6 @@ class PluginsRegistry: return True, [] except Exception as error: get_plugin_error(error, do_raise=True) - + # endregion plugin_registry = PluginsRegistry() From 13ff94b6b2e0abfd3cd587d1fdcfbe736f018589 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 10 Jan 2022 01:05:32 +0100 Subject: [PATCH 05/49] remove dead code --- InvenTree/plugin/test_plugin.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/InvenTree/plugin/test_plugin.py b/InvenTree/plugin/test_plugin.py index 2013ad43c8..18c0f8bc85 100644 --- a/InvenTree/plugin/test_plugin.py +++ b/InvenTree/plugin/test_plugin.py @@ -40,9 +40,7 @@ class PluginIntegrationTests(TestCase): def test_plugin_loading(self): """check if plugins load as expected""" # plugin_names_barcode = [a().plugin_name() for a in load_barcode_plugins()] # TODO refactor barcode plugin to support standard loading - # plugin_names_action = [a().plugin_name() for a in load_action_plugins()] # TODO refactor action plugin to support standard loading - # self.assertEqual(plugin_names_action, '') # self.assertEqual(plugin_names_barcode, '') # TODO remove test once loading is moved From 4fc3e85a53f60a0712f659d27bb0313ee653cde8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 10 Jan 2022 01:06:05 +0100 Subject: [PATCH 06/49] use new loading mechanism for barcodes too --- InvenTree/barcodes/api.py | 6 +++--- InvenTree/plugin/plugins.py | 35 --------------------------------- InvenTree/plugin/test_plugin.py | 12 ----------- 3 files changed, 3 insertions(+), 50 deletions(-) diff --git a/InvenTree/barcodes/api.py b/InvenTree/barcodes/api.py index 4b853ab438..1f71b06e63 100644 --- a/InvenTree/barcodes/api.py +++ b/InvenTree/barcodes/api.py @@ -13,7 +13,7 @@ from stock.models import StockItem from stock.serializers import StockItemSerializer from barcodes.barcode import hash_barcode -from plugin.plugins import load_barcode_plugins +from plugin import plugin_registry class BarcodeScan(APIView): @@ -53,7 +53,7 @@ class BarcodeScan(APIView): if 'barcode' not in data: raise ValidationError({'barcode': _('Must provide barcode_data parameter')}) - plugins = load_barcode_plugins() + plugins = plugin_registry.with_mixin('barcode') barcode_data = data.get('barcode') @@ -160,7 +160,7 @@ class BarcodeAssign(APIView): except (ValueError, StockItem.DoesNotExist): raise ValidationError({'stockitem': _('No matching stock item found')}) - plugins = load_barcode_plugins() + plugins = plugin_registry.with_mixin('barcode') plugin = None diff --git a/InvenTree/plugin/plugins.py b/InvenTree/plugin/plugins.py index f0150495cb..ae315ebf32 100644 --- a/InvenTree/plugin/plugins.py +++ b/InvenTree/plugin/plugins.py @@ -4,14 +4,10 @@ import inspect import importlib import pkgutil -import logging from django.core.exceptions import AppRegistryNotReady -logger = logging.getLogger("inventree") - - def iter_namespace(pkg): """get all modules in a package""" return pkgutil.iter_modules(pkg.__path__, pkg.__name__ + ".") @@ -70,34 +66,3 @@ def get_plugins(pkg, baseclass, recursive: bool = False): plugins.append(plugin) return plugins - - -def load_plugins(name: str, cls, module): - """general function to load a plugin class - - :param name: name of the plugin for logs - :type name: str - :param module: module from which the plugins should be loaded - :return: class of the to-be-loaded plugin - """ - logger.debug("Loading %s plugins", name) - - plugins = get_plugins(module, cls) - - if len(plugins) > 0: - logger.info("Discovered %i %s plugins:", len(plugins), name) - - for plugin in plugins: - logger.debug(" - %s", plugin.PLUGIN_NAME) - - return plugins - - -def load_barcode_plugins(): - """ - Return a list of all registered barcode plugins - """ - from barcodes import plugins as BarcodePlugins - from barcodes.barcode import BarcodePlugin - - return load_plugins('barcode', BarcodePlugin, BarcodePlugins) diff --git a/InvenTree/plugin/test_plugin.py b/InvenTree/plugin/test_plugin.py index 18c0f8bc85..3fa7a55f41 100644 --- a/InvenTree/plugin/test_plugin.py +++ b/InvenTree/plugin/test_plugin.py @@ -34,18 +34,6 @@ class InvenTreePluginTests(TestCase): self.assertEqual(self.named_plugin.plugin_name(), 'abc123') -class PluginIntegrationTests(TestCase): - """ Tests for general plugin functions """ - - def test_plugin_loading(self): - """check if plugins load as expected""" - # plugin_names_barcode = [a().plugin_name() for a in load_barcode_plugins()] # TODO refactor barcode plugin to support standard loading - - # self.assertEqual(plugin_names_barcode, '') - - # TODO remove test once loading is moved - - class PluginTagTests(TestCase): """ Tests for the plugin extras """ From 20e712a28763a07533867cb36053a8e8e2cef6ae Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 10 Jan 2022 01:23:48 +0100 Subject: [PATCH 07/49] update barcodes to use mixin --- InvenTree/barcodes/barcode.py | 144 ++---------------- InvenTree/plugin/builtin/barcode/__init__.py | 0 InvenTree/plugin/builtin/barcode/mixins.py | 147 +++++++++++++++++++ InvenTree/plugin/mixins/__init__.py | 2 + 4 files changed, 161 insertions(+), 132 deletions(-) create mode 100644 InvenTree/plugin/builtin/barcode/__init__.py create mode 100644 InvenTree/plugin/builtin/barcode/mixins.py diff --git a/InvenTree/barcodes/barcode.py b/InvenTree/barcodes/barcode.py index 51f8a1ffa1..bca27590e8 100644 --- a/InvenTree/barcodes/barcode.py +++ b/InvenTree/barcodes/barcode.py @@ -1,139 +1,19 @@ # -*- coding: utf-8 -*- +import warnings -import string -import hashlib -import logging +import plugin.builtin.barcode.mixins as mixin +import plugin.integration -from stock.models import StockItem -from stock.serializers import StockItemSerializer, LocationSerializer -from part.serializers import PartSerializer +hash_barcode = mixin.hash_barcode - -logger = logging.getLogger('inventree') - - -def hash_barcode(barcode_data): +class BarcodePlugin(mixin.BarcodeMixin, plugin.integration.IntegrationPluginBase): """ - Calculate an MD5 hash of barcode data. - - HACK: Remove any 'non printable' characters from the hash, - as it seems browers will remove special control characters... - - TODO: Work out a way around this! + Legacy barcode plugin definition - will be replaced + Please use the new Integration Plugin API and the BarcodeMixin """ - - barcode_data = str(barcode_data).strip() - - printable_chars = filter(lambda x: x in string.printable, barcode_data) - - barcode_data = ''.join(list(printable_chars)) - - hash = hashlib.md5(str(barcode_data).encode()) - return str(hash.hexdigest()) - - -class BarcodePlugin: - """ - Base class for barcode handling. - Custom barcode plugins should extend this class as necessary. - """ - - # Override the barcode plugin name for each sub-class - PLUGIN_NAME = "" - - @property - def name(self): - return self.PLUGIN_NAME - - def __init__(self, barcode_data): - """ - Initialize the BarcodePlugin instance - - Args: - barcode_data - The raw barcode data - """ - - self.data = barcode_data - - def getStockItem(self): - """ - Attempt to retrieve a StockItem associated with this barcode. - Default implementation returns None - """ - - return None - - def getStockItemByHash(self): - """ - Attempt to retrieve a StockItem associated with this barcode, - based on the barcode hash. - """ - - try: - item = StockItem.objects.get(uid=self.hash()) - return item - except StockItem.DoesNotExist: - return None - - def renderStockItem(self, item): - """ - Render a stock item to JSON response - """ - - serializer = StockItemSerializer(item, part_detail=True, location_detail=True, supplier_part_detail=True) - return serializer.data - - def getStockLocation(self): - """ - Attempt to retrieve a StockLocation associated with this barcode. - Default implementation returns None - """ - - return None - - def renderStockLocation(self, loc): - """ - Render a stock location to a JSON response - """ - - serializer = LocationSerializer(loc) - return serializer.data - - def getPart(self): - """ - Attempt to retrieve a Part associated with this barcode. - Default implementation returns None - """ - - return None - - def renderPart(self, part): - """ - Render a part to JSON response - """ - - serializer = PartSerializer(part) - return serializer.data - - def hash(self): - """ - Calculate a hash for the barcode data. - This is supposed to uniquely identify the barcode contents, - at least within the bardcode sub-type. - - The default implementation simply returns an MD5 hash of the barcode data, - encoded to a string. - - This may be sufficient for most applications, but can obviously be overridden - by a subclass. - - """ - - return hash_barcode(self.data) - - def validate(self): - """ - Default implementation returns False - """ - return False + # TODO @matmair remove this with InvenTree 0.7.0 + def __init__(self, barcode_data=None): + warnings.warn("using the BarcodePlugin is depreceated", DeprecationWarning) + super().__init__() + self.init(barcode_data) diff --git a/InvenTree/plugin/builtin/barcode/__init__.py b/InvenTree/plugin/builtin/barcode/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/InvenTree/plugin/builtin/barcode/mixins.py b/InvenTree/plugin/builtin/barcode/mixins.py new file mode 100644 index 0000000000..904c260275 --- /dev/null +++ b/InvenTree/plugin/builtin/barcode/mixins.py @@ -0,0 +1,147 @@ +""" +Plugin mixin classes for barcode plugin +""" +import string +import hashlib + +from stock.models import StockItem +from stock.serializers import StockItemSerializer, LocationSerializer +from part.serializers import PartSerializer + + +def hash_barcode(barcode_data): + """ + Calculate an MD5 hash of barcode data. + + HACK: Remove any 'non printable' characters from the hash, + as it seems browers will remove special control characters... + + TODO: Work out a way around this! + """ + + barcode_data = str(barcode_data).strip() + + printable_chars = filter(lambda x: x in string.printable, barcode_data) + + barcode_data = ''.join(list(printable_chars)) + + hash = hashlib.md5(str(barcode_data).encode()) + return str(hash.hexdigest()) + + +class BarcodeMixin: + """ + Mixin that enables barcode handeling + Custom barcode plugins should use and extend this mixin as necessary. + """ + ACTION_NAME = "" + + class MixinMeta: + """ + meta options for this mixin + """ + MIXIN_NAME = 'Barcode' + + def __init__(self): + super().__init__() + self.add_mixin('barcode', 'has_barcode', __class__) + + @property + def has_barcode(self): + """ + Does this plugin have everything needed to process a barcode + """ + return True + + def init(self, barcode_data): + """ + Initialize the BarcodePlugin instance + + Args: + barcode_data - The raw barcode data + """ + + self.data = barcode_data + + + def getStockItem(self): + """ + Attempt to retrieve a StockItem associated with this barcode. + Default implementation returns None + """ + + return None + + def getStockItemByHash(self): + """ + Attempt to retrieve a StockItem associated with this barcode, + based on the barcode hash. + """ + + try: + item = StockItem.objects.get(uid=self.hash()) + return item + except StockItem.DoesNotExist: + return None + + def renderStockItem(self, item): + """ + Render a stock item to JSON response + """ + + serializer = StockItemSerializer(item, part_detail=True, location_detail=True, supplier_part_detail=True) + return serializer.data + + def getStockLocation(self): + """ + Attempt to retrieve a StockLocation associated with this barcode. + Default implementation returns None + """ + + return None + + def renderStockLocation(self, loc): + """ + Render a stock location to a JSON response + """ + + serializer = LocationSerializer(loc) + return serializer.data + + def getPart(self): + """ + Attempt to retrieve a Part associated with this barcode. + Default implementation returns None + """ + + return None + + def renderPart(self, part): + """ + Render a part to JSON response + """ + + serializer = PartSerializer(part) + return serializer.data + + def hash(self): + """ + Calculate a hash for the barcode data. + This is supposed to uniquely identify the barcode contents, + at least within the bardcode sub-type. + + The default implementation simply returns an MD5 hash of the barcode data, + encoded to a string. + + This may be sufficient for most applications, but can obviously be overridden + by a subclass. + + """ + + return hash_barcode(self.data) + + def validate(self): + """ + Default implementation returns False + """ + return False diff --git a/InvenTree/plugin/mixins/__init__.py b/InvenTree/plugin/mixins/__init__.py index 389258a99a..03b6f48971 100644 --- a/InvenTree/plugin/mixins/__init__.py +++ b/InvenTree/plugin/mixins/__init__.py @@ -4,6 +4,7 @@ Utility class to enable simpler imports from ..builtin.integration.mixins import AppMixin, SettingsMixin, ScheduleMixin, UrlsMixin, NavigationMixin from ..builtin.action.mixins import ActionMixin +from ..builtin.barcode.mixins import BarcodeMixin __all__ = [ 'AppMixin', @@ -12,4 +13,5 @@ __all__ = [ 'SettingsMixin', 'UrlsMixin', 'ActionMixin', + 'BarcodeMixin', ] From 632632c8ad83eb4d80b5a90e99b56a80798af6a3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 10 Jan 2022 01:28:44 +0100 Subject: [PATCH 08/49] fix lookup function --- InvenTree/plugin/registry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/plugin/registry.py b/InvenTree/plugin/registry.py index fc0113d51a..171b565a3f 100644 --- a/InvenTree/plugin/registry.py +++ b/InvenTree/plugin/registry.py @@ -192,7 +192,7 @@ class PluginsRegistry: """ result = [] - for plugin in self.plugins.items(): + for plugin in self.plugins.values(): if plugin.mixin_enabled(mixin): result.append(plugin) From d03c636067f90cadc5d029c85d376a3b7f3bec56 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 10 Jan 2022 02:10:08 +0100 Subject: [PATCH 09/49] fix plugin init sequence --- InvenTree/barcodes/api.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/InvenTree/barcodes/api.py b/InvenTree/barcodes/api.py index 1f71b06e63..c0e6373861 100644 --- a/InvenTree/barcodes/api.py +++ b/InvenTree/barcodes/api.py @@ -60,11 +60,12 @@ class BarcodeScan(APIView): # Look for a barcode plugin which knows how to deal with this barcode plugin = None - for plugin_class in plugins: - plugin_instance = plugin_class(barcode_data) + for current_plugin in plugins: + # TODO @matmair make simpler after InvenTree 0.7.0 release + current_plugin.init(barcode_data) - if plugin_instance.validate(): - plugin = plugin_instance + if current_plugin.validate(): + plugin = current_plugin break match_found = False @@ -164,11 +165,12 @@ class BarcodeAssign(APIView): plugin = None - for plugin_class in plugins: - plugin_instance = plugin_class(barcode_data) + for current_plugin in plugins: + # TODO @matmair make simpler after InvenTree 0.7.0 release + current_plugin.init(barcode_data) - if plugin_instance.validate(): - plugin = plugin_instance + if current_plugin.validate(): + plugin = current_plugin break match_found = False From e80a71eb46a442c2634e36a5d71c5a825e4a23e2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 10 Jan 2022 02:10:24 +0100 Subject: [PATCH 10/49] load plugins too --- InvenTree/InvenTree/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 89e60a597e..44796efb02 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -874,7 +874,7 @@ MARKDOWNIFY_BLEACH = False MAINTENANCE_MODE_RETRY_AFTER = 60 # Plugin Directories (local plugins will be loaded from these directories) -PLUGIN_DIRS = ['plugin.builtin', ] +PLUGIN_DIRS = ['plugin.builtin', 'barcodes.plugins', ] if not TESTING: # load local deploy directory in prod From 7d160039b105eca16667e658517715b366cfafff Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 10 Jan 2022 02:15:58 +0100 Subject: [PATCH 11/49] PEP fixes --- InvenTree/InvenTree/api.py | 1 + InvenTree/barcodes/barcode.py | 1 + InvenTree/plugin/builtin/action/mixins.py | 1 + InvenTree/plugin/builtin/barcode/mixins.py | 1 - InvenTree/plugin/registry.py | 2 ++ 5 files changed, 5 insertions(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/api.py b/InvenTree/InvenTree/api.py index 069fd9492d..18b11211ca 100644 --- a/InvenTree/InvenTree/api.py +++ b/InvenTree/InvenTree/api.py @@ -21,6 +21,7 @@ from .status import is_worker_running from plugin import plugin_registry + class InfoView(AjaxView): """ Simple JSON endpoint for InvenTree information. Use to confirm that the server is running, etc. diff --git a/InvenTree/barcodes/barcode.py b/InvenTree/barcodes/barcode.py index bca27590e8..030552c866 100644 --- a/InvenTree/barcodes/barcode.py +++ b/InvenTree/barcodes/barcode.py @@ -7,6 +7,7 @@ import plugin.integration hash_barcode = mixin.hash_barcode + class BarcodePlugin(mixin.BarcodeMixin, plugin.integration.IntegrationPluginBase): """ Legacy barcode plugin definition - will be replaced diff --git a/InvenTree/plugin/builtin/action/mixins.py b/InvenTree/plugin/builtin/action/mixins.py index 1f9b34c661..0be19be2ca 100644 --- a/InvenTree/plugin/builtin/action/mixins.py +++ b/InvenTree/plugin/builtin/action/mixins.py @@ -2,6 +2,7 @@ Plugin mixin classes for action plugin """ + class ActionMixin: """ Mixin that enables custom actions diff --git a/InvenTree/plugin/builtin/barcode/mixins.py b/InvenTree/plugin/builtin/barcode/mixins.py index 904c260275..b86130f71e 100644 --- a/InvenTree/plugin/builtin/barcode/mixins.py +++ b/InvenTree/plugin/builtin/barcode/mixins.py @@ -63,7 +63,6 @@ class BarcodeMixin: self.data = barcode_data - def getStockItem(self): """ Attempt to retrieve a StockItem associated with this barcode. diff --git a/InvenTree/plugin/registry.py b/InvenTree/plugin/registry.py index 171b565a3f..8c527a9c52 100644 --- a/InvenTree/plugin/registry.py +++ b/InvenTree/plugin/registry.py @@ -185,6 +185,7 @@ class PluginsRegistry: logger.info(f'Collected {len(self.plugin_modules)} plugins!') logger.info(", ".join([a.__module__ for a in self.plugin_modules])) # endregion + # region registry functions def with_mixin(self, mixin: str): """ @@ -544,4 +545,5 @@ class PluginsRegistry: get_plugin_error(error, do_raise=True) # endregion + plugin_registry = PluginsRegistry() From 18cb676ce3a9bec67d72d6014ddd67469ac1c19e Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 10 Jan 2022 02:17:50 +0100 Subject: [PATCH 12/49] another PEP fix --- InvenTree/plugin/registry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/plugin/registry.py b/InvenTree/plugin/registry.py index 8c527a9c52..513407c1ff 100644 --- a/InvenTree/plugin/registry.py +++ b/InvenTree/plugin/registry.py @@ -185,7 +185,7 @@ class PluginsRegistry: logger.info(f'Collected {len(self.plugin_modules)} plugins!') logger.info(", ".join([a.__module__ for a in self.plugin_modules])) # endregion - + # region registry functions def with_mixin(self, mixin: str): """ From 2bc4d57ffac001cc1cd7edf7bd73df2ca66fd18f Mon Sep 17 00:00:00 2001 From: Matthias Mair <66015116+matmair@users.noreply.github.com> Date: Mon, 10 Jan 2022 02:49:34 +0100 Subject: [PATCH 13/49] remove double code --- .../plugin/builtin/integration/mixins.py | 72 ------------------- 1 file changed, 72 deletions(-) diff --git a/InvenTree/plugin/builtin/integration/mixins.py b/InvenTree/plugin/builtin/integration/mixins.py index 14997fd452..c6198ed7a1 100644 --- a/InvenTree/plugin/builtin/integration/mixins.py +++ b/InvenTree/plugin/builtin/integration/mixins.py @@ -301,75 +301,3 @@ class AppMixin: this plugin is always an app with this plugin """ return True - - -class ActionMixin: - """ - Mixin that enables custom actions - """ - ACTION_NAME = "" - - class MixinMeta: - """ - meta options for this mixin - """ - MIXIN_NAME = 'Action' - - def __init__(self): - super().__init__() - self.add_mixin('action', 'has_action', __class__) - - @property - def has_action(self): - """ - Does this plugin have everything needed for an action? - """ - return True - - @property - def action_name(self): - """ - Return the action name for this plugin. - If the ACTION_NAME parameter is empty, - look at the PLUGIN_NAME instead. - """ - if self.ACTION_NAME: - return self.ACTION_NAME - return self.name - - def init(self, user, data=None): - """ - An action plugin takes a user reference, and an optional dataset (dict) - """ - self.user = user - self.data = data - - def perform_action(self): - """ - Override this method to perform the action! - """ - - def get_result(self): - """ - Result of the action? - """ - - # Re-implement this for custom actions - return False - - def get_info(self): - """ - Extra info? Can be a string / dict / etc - """ - return None - - def get_response(self): - """ - Return a response. Default implementation is a simple response - which can be overridden. - """ - return { - "action": self.action_name(), - "result": self.get_result(), - "info": self.get_info(), - } From 0fecf3c0bc6bd0659cb1647c4bb143ecc629f0af Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 10 Jan 2022 23:50:00 +0100 Subject: [PATCH 14/49] fix assingment --- InvenTree/InvenTree/api.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/InvenTree/InvenTree/api.py b/InvenTree/InvenTree/api.py index 18b11211ca..0d3cc39610 100644 --- a/InvenTree/InvenTree/api.py +++ b/InvenTree/InvenTree/api.py @@ -102,10 +102,10 @@ class ActionPluginView(APIView): }) action_plugins = plugin_registry.with_mixin('action') - for plugin_class in action_plugins: - if plugin_class.action_name() == action: + for plugin in action_plugins: + if plugin.action_name() == action: # TODO @matmair use easier syntax once InvenTree 0.7.0 is released - plugin = plugin_class.init(request.user, data=data) + plugin.init(request.user, data=data) plugin.perform_action() From 588936f6f4e251f226789b8f97c357a7c7d4b3fc Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 10 Jan 2022 23:53:34 +0100 Subject: [PATCH 15/49] PEP fix --- InvenTree/plugin/builtin/integration/mixins.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/InvenTree/plugin/builtin/integration/mixins.py b/InvenTree/plugin/builtin/integration/mixins.py index cfe165c889..cf15acdef9 100644 --- a/InvenTree/plugin/builtin/integration/mixins.py +++ b/InvenTree/plugin/builtin/integration/mixins.py @@ -3,8 +3,6 @@ Plugin mixin classes """ import logging -import json -import requests from django.conf.urls import url, include from django.db.utils import OperationalError, ProgrammingError From c3d7a26f5d476dfeede9c1f69977304081263c87 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 11 Jan 2022 00:30:17 +0100 Subject: [PATCH 16/49] readd api call (dont know how that happened) --- .../plugin/builtin/integration/mixins.py | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/InvenTree/plugin/builtin/integration/mixins.py b/InvenTree/plugin/builtin/integration/mixins.py index cf15acdef9..d118823831 100644 --- a/InvenTree/plugin/builtin/integration/mixins.py +++ b/InvenTree/plugin/builtin/integration/mixins.py @@ -3,6 +3,8 @@ Plugin mixin classes """ import logging +import json +import requests from django.conf.urls import url, include from django.db.utils import OperationalError, ProgrammingError @@ -321,3 +323,107 @@ class AppMixin: this plugin is always an app with this plugin """ return True + + +class APICallMixin: + """ + Mixin that enables easier API calls for a plugin + Steps to set up: + 1. Add this mixin before (left of) SettingsMixin and PluginBase + 2. Add two settings for the required url and token/passowrd (use `SettingsMixin`) + 3. Save the references to keys of the settings in `API_URL_SETTING` and `API_TOKEN_SETTING` + 4. (Optional) Set `API_TOKEN` to the name required for the token by the external API - Defaults to `Bearer` + 5. (Optional) Override the `api_url` property method if the setting needs to be extended + 6. (Optional) Override `api_headers` to add extra headers (by default the token and Content-Type are contained) + 7. Access the API in you plugin code via `api_call` + Example: + ``` + from plugin import IntegrationPluginBase + from plugin.mixins import APICallMixin, SettingsMixin + class SampleApiCallerPlugin(APICallMixin, SettingsMixin, IntegrationPluginBase): + ''' + A small api call sample + ''' + PLUGIN_NAME = "Sample API Caller" + SETTINGS = { + 'API_TOKEN': { + 'name': 'API Token', + 'protected': True, + }, + 'API_URL': { + 'name': 'External URL', + 'description': 'Where is your API located?', + 'default': 'reqres.in', + }, + } + API_URL_SETTING = 'API_URL' + API_TOKEN_SETTING = 'API_TOKEN' + def get_external_url(self): + ''' + returns data from the sample endpoint + ''' + return self.api_call('api/users/2') + ``` + """ + API_METHOD = 'https' + API_URL_SETTING = None + API_TOKEN_SETTING = None + + API_TOKEN = 'Bearer' + + class MixinMeta: + """meta options for this mixin""" + MIXIN_NAME = 'API calls' + + def __init__(self): + super().__init__() + self.add_mixin('api_call', 'has_api_call', __class__) + + @property + def has_api_call(self): + """Is the mixin ready to call external APIs?""" + if not bool(self.API_URL_SETTING): + raise ValueError("API_URL_SETTING must be defined") + if not bool(self.API_TOKEN_SETTING): + raise ValueError("API_TOKEN_SETTING must be defined") + return True + + @property + def api_url(self): + return f'{self.API_METHOD}://{self.get_setting(self.API_URL_SETTING)}' + + @property + def api_headers(self): + return { + self.API_TOKEN: self.get_setting(self.API_TOKEN_SETTING), + 'Content-Type': 'application/json' + } + + def api_build_url_args(self, arguments): + groups = [] + for key, val in arguments.items(): + groups.append(f'{key}={",".join([str(a) for a in val])}') + return f'?{"&".join(groups)}' + + def api_call(self, endpoint, method: str = 'GET', url_args=None, data=None, headers=None, simple_response: bool = True): + if url_args: + endpoint += self.api_build_url_args(url_args) + + if headers is None: + headers = self.api_headers + + # build kwargs for call + kwargs = { + 'url': f'{self.api_url}/{endpoint}', + 'headers': headers, + } + if data: + kwargs['data'] = json.dumps(data) + + # run command + response = requests.request(method, **kwargs) + + # return + if simple_response: + return response.json() + return response From dd2547e117717d1bae4fe1445410685c0f91110f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 11 Jan 2022 00:32:56 +0100 Subject: [PATCH 17/49] readd spaces --- InvenTree/plugin/builtin/integration/mixins.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/InvenTree/plugin/builtin/integration/mixins.py b/InvenTree/plugin/builtin/integration/mixins.py index d118823831..586fc8a666 100644 --- a/InvenTree/plugin/builtin/integration/mixins.py +++ b/InvenTree/plugin/builtin/integration/mixins.py @@ -328,6 +328,7 @@ class AppMixin: class APICallMixin: """ Mixin that enables easier API calls for a plugin + Steps to set up: 1. Add this mixin before (left of) SettingsMixin and PluginBase 2. Add two settings for the required url and token/passowrd (use `SettingsMixin`) @@ -336,15 +337,19 @@ class APICallMixin: 5. (Optional) Override the `api_url` property method if the setting needs to be extended 6. (Optional) Override `api_headers` to add extra headers (by default the token and Content-Type are contained) 7. Access the API in you plugin code via `api_call` + Example: ``` from plugin import IntegrationPluginBase from plugin.mixins import APICallMixin, SettingsMixin + + class SampleApiCallerPlugin(APICallMixin, SettingsMixin, IntegrationPluginBase): ''' A small api call sample ''' PLUGIN_NAME = "Sample API Caller" + SETTINGS = { 'API_TOKEN': { 'name': 'API Token', @@ -358,6 +363,7 @@ class APICallMixin: } API_URL_SETTING = 'API_URL' API_TOKEN_SETTING = 'API_TOKEN' + def get_external_url(self): ''' returns data from the sample endpoint From a3410a30d509fde9edde62648e69b7be41d1992f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 11 Jan 2022 00:35:01 +0100 Subject: [PATCH 18/49] also load nuiltin actions --- InvenTree/InvenTree/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 44796efb02..c140bdecb9 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -874,7 +874,7 @@ MARKDOWNIFY_BLEACH = False MAINTENANCE_MODE_RETRY_AFTER = 60 # Plugin Directories (local plugins will be loaded from these directories) -PLUGIN_DIRS = ['plugin.builtin', 'barcodes.plugins', ] +PLUGIN_DIRS = ['plugin.builtin', 'barcodes.acion', 'barcodes.plugins', ] if not TESTING: # load local deploy directory in prod From 0283214034ac4debdd73298e5f3c6419f0aac222 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 11 Jan 2022 00:43:18 +0100 Subject: [PATCH 19/49] add custom errors for plugin --- InvenTree/plugin/__init__.py | 4 ++++ InvenTree/plugin/builtin/integration/mixins.py | 15 ++++++++------- InvenTree/plugin/helpers.py | 15 +++++++++++++++ 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/InvenTree/plugin/__init__.py b/InvenTree/plugin/__init__.py index 86f65919c4..3c220044b5 100644 --- a/InvenTree/plugin/__init__.py +++ b/InvenTree/plugin/__init__.py @@ -7,9 +7,13 @@ from .plugin import InvenTreePlugin from .integration import IntegrationPluginBase from .action import ActionPlugin +from .helpers import MixinNotImplementedError, MixinImplementationError + __all__ = [ 'ActionPlugin', 'IntegrationPluginBase', 'InvenTreePlugin', 'plugin_registry', + 'MixinNotImplementedError', + 'MixinImplementationError', ] diff --git a/InvenTree/plugin/builtin/integration/mixins.py b/InvenTree/plugin/builtin/integration/mixins.py index 586fc8a666..684e100d81 100644 --- a/InvenTree/plugin/builtin/integration/mixins.py +++ b/InvenTree/plugin/builtin/integration/mixins.py @@ -11,6 +11,7 @@ from django.db.utils import OperationalError, ProgrammingError from plugin.models import PluginConfig, PluginSetting from plugin.urls import PLUGIN_BASE +from plugin.helpers import MixinImplementationError, MixinNotImplementedError logger = logging.getLogger('inventree') @@ -105,24 +106,24 @@ class ScheduleMixin: """ if not self.has_scheduled_tasks: - raise ValueError("SCHEDULED_TASKS not defined") + raise MixinImplementationError("SCHEDULED_TASKS not defined") for key, task in self.scheduled_tasks.items(): if 'func' not in task: - raise ValueError(f"Task '{key}' is missing 'func' parameter") + raise MixinImplementationError(f"Task '{key}' is missing 'func' parameter") if 'schedule' not in task: - raise ValueError(f"Task '{key}' is missing 'schedule' parameter") + raise MixinImplementationError(f"Task '{key}' is missing 'schedule' parameter") schedule = task['schedule'].upper().strip() if schedule not in self.ALLOWABLE_SCHEDULE_TYPES: - raise ValueError(f"Task '{key}': Schedule '{schedule}' is not a valid option") + raise MixinImplementationError(f"Task '{key}': Schedule '{schedule}' is not a valid option") # If 'minutes' is selected, it must be provided! if schedule == 'I' and 'minutes' not in task: - raise ValueError(f"Task '{key}' is missing 'minutes' parameter") + raise MixinImplementationError(f"Task '{key}' is missing 'minutes' parameter") def get_task_name(self, key): # Generate a 'unique' task name @@ -192,7 +193,7 @@ class EventMixin: def process_event(self, event, *args, **kwargs): # Default implementation does not do anything - raise NotImplementedError + raise MixinNotImplementedError class MixinMeta: MIXIN_NAME = 'Events' @@ -280,7 +281,7 @@ class NavigationMixin: # check if needed values are configured for link in nav_links: if False in [a in link for a in ('link', 'name', )]: - raise NotImplementedError('Wrong Link definition', link) + raise MixinNotImplementedError('Wrong Link definition', link) return nav_links @property diff --git a/InvenTree/plugin/helpers.py b/InvenTree/plugin/helpers.py index fb46df8927..e6e1e9134d 100644 --- a/InvenTree/plugin/helpers.py +++ b/InvenTree/plugin/helpers.py @@ -29,6 +29,21 @@ class IntegrationPluginError(Exception): return self.message +class MixinImplementationError(ValueError): + """ + Error if mixin was implemented wrong in plugin + Mostly raised if constant is missing + """ + pass + + +class MixinNotImplementedError(NotImplementedError): + """ + Error if necessary mixin function was not overwritten + """ + pass + + def get_plugin_error(error, do_raise: bool = False, do_log: bool = False, log_name: str = ''): package_path = traceback.extract_tb(error.__traceback__)[-1].filename install_path = sysconfig.get_paths()["purelib"] From f53e66d4c3475002a50824d642db4ed257a089be Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 11 Jan 2022 01:00:41 +0100 Subject: [PATCH 20/49] Rename registry to make it clearer --- InvenTree/InvenTree/api.py | 4 ++-- InvenTree/barcodes/api.py | 6 +++--- InvenTree/plugin/__init__.py | 4 ++-- InvenTree/plugin/admin.py | 4 ++-- InvenTree/plugin/apps.py | 8 ++++---- InvenTree/plugin/events.py | 6 +++--- InvenTree/plugin/helpers.py | 8 ++++---- InvenTree/plugin/loader.py | 4 ++-- InvenTree/plugin/registry.py | 2 +- .../plugin/samples/integration/test_api_caller.py | 6 +++--- InvenTree/plugin/templatetags/plugin_extras.py | 10 +++++----- InvenTree/plugin/test_api.py | 4 ++-- InvenTree/plugin/test_plugin.py | 10 +++++----- InvenTree/plugin/urls.py | 4 ++-- 14 files changed, 40 insertions(+), 40 deletions(-) diff --git a/InvenTree/InvenTree/api.py b/InvenTree/InvenTree/api.py index 0d3cc39610..7c8f71ea9a 100644 --- a/InvenTree/InvenTree/api.py +++ b/InvenTree/InvenTree/api.py @@ -19,7 +19,7 @@ from .views import AjaxView from .version import inventreeVersion, inventreeApiVersion, inventreeInstanceName from .status import is_worker_running -from plugin import plugin_registry +from plugin import registry class InfoView(AjaxView): @@ -101,7 +101,7 @@ class ActionPluginView(APIView): 'error': _("No action specified") }) - action_plugins = plugin_registry.with_mixin('action') + action_plugins = registry.with_mixin('action') for plugin in action_plugins: if plugin.action_name() == action: # TODO @matmair use easier syntax once InvenTree 0.7.0 is released diff --git a/InvenTree/barcodes/api.py b/InvenTree/barcodes/api.py index c0e6373861..355fcab0e1 100644 --- a/InvenTree/barcodes/api.py +++ b/InvenTree/barcodes/api.py @@ -13,7 +13,7 @@ from stock.models import StockItem from stock.serializers import StockItemSerializer from barcodes.barcode import hash_barcode -from plugin import plugin_registry +from plugin import registry class BarcodeScan(APIView): @@ -53,7 +53,7 @@ class BarcodeScan(APIView): if 'barcode' not in data: raise ValidationError({'barcode': _('Must provide barcode_data parameter')}) - plugins = plugin_registry.with_mixin('barcode') + plugins = registry.with_mixin('barcode') barcode_data = data.get('barcode') @@ -161,7 +161,7 @@ class BarcodeAssign(APIView): except (ValueError, StockItem.DoesNotExist): raise ValidationError({'stockitem': _('No matching stock item found')}) - plugins = plugin_registry.with_mixin('barcode') + plugins = registry.with_mixin('barcode') plugin = None diff --git a/InvenTree/plugin/__init__.py b/InvenTree/plugin/__init__.py index 3c220044b5..98d0d94b3a 100644 --- a/InvenTree/plugin/__init__.py +++ b/InvenTree/plugin/__init__.py @@ -2,7 +2,7 @@ Utility file to enable simper imports """ -from .registry import plugin_registry +from .registry import registry from .plugin import InvenTreePlugin from .integration import IntegrationPluginBase from .action import ActionPlugin @@ -13,7 +13,7 @@ __all__ = [ 'ActionPlugin', 'IntegrationPluginBase', 'InvenTreePlugin', - 'plugin_registry', + 'registry', 'MixinNotImplementedError', 'MixinImplementationError', ] diff --git a/InvenTree/plugin/admin.py b/InvenTree/plugin/admin.py index b20aef8057..790c3c6c5d 100644 --- a/InvenTree/plugin/admin.py +++ b/InvenTree/plugin/admin.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals from django.contrib import admin import plugin.models as models -import plugin.registry as registry +import plugin.registry as pl_registry def plugin_update(queryset, new_status: bool): @@ -23,7 +23,7 @@ def plugin_update(queryset, new_status: bool): # Reload plugins if they changed if apps_changed: - registry.plugin_registry.reload_plugins() + pl_registry.registry.reload_plugins() @admin.action(description='Activate plugin(s)') diff --git a/InvenTree/plugin/apps.py b/InvenTree/plugin/apps.py index cca9dee91c..1b0e521488 100644 --- a/InvenTree/plugin/apps.py +++ b/InvenTree/plugin/apps.py @@ -4,17 +4,17 @@ from __future__ import unicode_literals from django.apps import AppConfig from maintenance_mode.core import set_maintenance_mode -from plugin import plugin_registry +from plugin import registry class PluginAppConfig(AppConfig): name = 'plugin' def ready(self): - if not plugin_registry.is_loading: + if not registry.is_loading: # this is the first startup - plugin_registry.collect_plugins() - plugin_registry.load_plugins() + registry.collect_plugins() + registry.load_plugins() # drop out of maintenance # makes sure we did not have an error in reloading and maintenance is still active diff --git a/InvenTree/plugin/events.py b/InvenTree/plugin/events.py index f777621a45..709a408d71 100644 --- a/InvenTree/plugin/events.py +++ b/InvenTree/plugin/events.py @@ -17,7 +17,7 @@ from common.models import InvenTreeSetting from InvenTree.ready import canAppAccessDatabase from InvenTree.tasks import offload_task -from plugin.registry import plugin_registry +from plugin.registry import registry logger = logging.getLogger('inventree') @@ -60,7 +60,7 @@ def register_event(event, *args, **kwargs): with transaction.atomic(): - for slug, plugin in plugin_registry.plugins.items(): + for slug, plugin in registry.plugins.items(): if plugin.mixin_enabled('events'): @@ -91,7 +91,7 @@ def process_event(plugin_slug, event, *args, **kwargs): logger.info(f"Plugin '{plugin_slug}' is processing triggered event '{event}'") - plugin = plugin_registry.plugins[plugin_slug] + plugin = registry.plugins[plugin_slug] plugin.process_event(event, *args, **kwargs) diff --git a/InvenTree/plugin/helpers.py b/InvenTree/plugin/helpers.py index e6e1e9134d..c44976dcb4 100644 --- a/InvenTree/plugin/helpers.py +++ b/InvenTree/plugin/helpers.py @@ -10,14 +10,14 @@ from django.conf import settings # region logging / errors def log_plugin_error(error, reference: str = 'general'): - from plugin import plugin_registry + from plugin import registry # make sure the registry is set up - if reference not in plugin_registry.errors: - plugin_registry.errors[reference] = [] + if reference not in registry.errors: + registry.errors[reference] = [] # add error to stack - plugin_registry.errors[reference].append(error) + registry.errors[reference].append(error) class IntegrationPluginError(Exception): diff --git a/InvenTree/plugin/loader.py b/InvenTree/plugin/loader.py index 2d17f8c36f..aaba9fe060 100644 --- a/InvenTree/plugin/loader.py +++ b/InvenTree/plugin/loader.py @@ -4,7 +4,7 @@ load templates for loaded plugins from django.template.loaders.filesystem import Loader as FilesystemLoader from pathlib import Path -from plugin import plugin_registry +from plugin import registry class PluginTemplateLoader(FilesystemLoader): @@ -12,7 +12,7 @@ class PluginTemplateLoader(FilesystemLoader): def get_dirs(self): dirname = 'templates' template_dirs = [] - for plugin in plugin_registry.plugins.values(): + for plugin in registry.plugins.values(): new_path = Path(plugin.path) / dirname if Path(new_path).is_dir(): template_dirs.append(new_path) diff --git a/InvenTree/plugin/registry.py b/InvenTree/plugin/registry.py index 28cd6f97a2..9e8fd1fe99 100644 --- a/InvenTree/plugin/registry.py +++ b/InvenTree/plugin/registry.py @@ -547,4 +547,4 @@ class PluginsRegistry: # endregion -plugin_registry = PluginsRegistry() +registry = PluginsRegistry() diff --git a/InvenTree/plugin/samples/integration/test_api_caller.py b/InvenTree/plugin/samples/integration/test_api_caller.py index e15edfad94..32ee07bbd3 100644 --- a/InvenTree/plugin/samples/integration/test_api_caller.py +++ b/InvenTree/plugin/samples/integration/test_api_caller.py @@ -2,7 +2,7 @@ from django.test import TestCase -from plugin import plugin_registry +from plugin import registry class SampleApiCallerPluginTests(TestCase): @@ -11,8 +11,8 @@ class SampleApiCallerPluginTests(TestCase): def test_return(self): """check if the external api call works""" # The plugin should be defined - self.assertIn('sample-api-caller', plugin_registry.plugins) - plg = plugin_registry.plugins['sample-api-caller'] + self.assertIn('sample-api-caller', registry.plugins) + plg = registry.plugins['sample-api-caller'] self.assertTrue(plg) # do an api call diff --git a/InvenTree/plugin/templatetags/plugin_extras.py b/InvenTree/plugin/templatetags/plugin_extras.py index 7852133ebb..e1043a9aa0 100644 --- a/InvenTree/plugin/templatetags/plugin_extras.py +++ b/InvenTree/plugin/templatetags/plugin_extras.py @@ -7,7 +7,7 @@ from django import template from django.urls import reverse from common.models import InvenTreeSetting -from plugin import plugin_registry +from plugin import registry register = template.Library() @@ -16,19 +16,19 @@ register = template.Library() @register.simple_tag() def plugin_list(*args, **kwargs): """ Return a list of all installed integration plugins """ - return plugin_registry.plugins + return registry.plugins @register.simple_tag() def inactive_plugin_list(*args, **kwargs): """ Return a list of all inactive integration plugins """ - return plugin_registry.plugins_inactive + return registry.plugins_inactive @register.simple_tag() def plugin_settings(plugin, *args, **kwargs): """ Return a list of all custom settings for a plugin """ - return plugin_registry.mixins_settings.get(plugin) + return registry.mixins_settings.get(plugin) @register.simple_tag() @@ -57,4 +57,4 @@ def safe_url(view_name, *args, **kwargs): @register.simple_tag() def plugin_errors(*args, **kwargs): """Return all plugin errors""" - return plugin_registry.errors + return registry.errors diff --git a/InvenTree/plugin/test_api.py b/InvenTree/plugin/test_api.py index fdc97e407b..67ca5a69ad 100644 --- a/InvenTree/plugin/test_api.py +++ b/InvenTree/plugin/test_api.py @@ -64,14 +64,14 @@ class PluginDetailAPITest(InvenTreeAPITestCase): Test the PluginConfig action commands """ from plugin.models import PluginConfig - from plugin import plugin_registry + from plugin import registry url = reverse('admin:plugin_pluginconfig_changelist') fixtures = PluginConfig.objects.all() # check if plugins were registered -> in some test setups the startup has no db access if not fixtures: - plugin_registry.reload_plugins() + registry.reload_plugins() fixtures = PluginConfig.objects.all() print([str(a) for a in fixtures]) diff --git a/InvenTree/plugin/test_plugin.py b/InvenTree/plugin/test_plugin.py index 3fa7a55f41..b067326f69 100644 --- a/InvenTree/plugin/test_plugin.py +++ b/InvenTree/plugin/test_plugin.py @@ -9,7 +9,7 @@ import plugin.integration from plugin.samples.integration.sample import SampleIntegrationPlugin from plugin.samples.integration.another_sample import WrongIntegrationPlugin, NoIntegrationPlugin import plugin.templatetags.plugin_extras as plugin_tags -from plugin import plugin_registry +from plugin import registry class InvenTreePluginTests(TestCase): @@ -44,17 +44,17 @@ class PluginTagTests(TestCase): def test_tag_plugin_list(self): """test that all plugins are listed""" - self.assertEqual(plugin_tags.plugin_list(), plugin_registry.plugins) + self.assertEqual(plugin_tags.plugin_list(), registry.plugins) def test_tag_incative_plugin_list(self): """test that all inactive plugins are listed""" - self.assertEqual(plugin_tags.inactive_plugin_list(), plugin_registry.plugins_inactive) + self.assertEqual(plugin_tags.inactive_plugin_list(), registry.plugins_inactive) def test_tag_plugin_settings(self): """check all plugins are listed""" self.assertEqual( plugin_tags.plugin_settings(self.sample), - plugin_registry.mixins_settings.get(self.sample) + registry.mixins_settings.get(self.sample) ) def test_tag_mixin_enabled(self): @@ -76,4 +76,4 @@ class PluginTagTests(TestCase): def test_tag_plugin_errors(self): """test that all errors are listed""" - self.assertEqual(plugin_tags.plugin_errors(), plugin_registry.errors) + self.assertEqual(plugin_tags.plugin_errors(), registry.errors) diff --git a/InvenTree/plugin/urls.py b/InvenTree/plugin/urls.py index 1457aaf6f1..d41a84e2f1 100644 --- a/InvenTree/plugin/urls.py +++ b/InvenTree/plugin/urls.py @@ -4,7 +4,7 @@ URL lookup for plugin app from django.conf.urls import url, include -from plugin import plugin_registry +from plugin import registry PLUGIN_BASE = 'plugin' # Constant for links @@ -17,7 +17,7 @@ def get_plugin_urls(): urls = [] - for plugin in plugin_registry.plugins.values(): + for plugin in registry.plugins.values(): if plugin.mixin_enabled('urls'): urls.append(plugin.urlpatterns) From 3ae84617d080e352a799f5e560528d1353f83734 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 11 Jan 2022 01:01:51 +0100 Subject: [PATCH 21/49] change default values for plugin hadler --- InvenTree/plugin/helpers.py | 10 ++++++++-- InvenTree/plugin/registry.py | 8 ++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/InvenTree/plugin/helpers.py b/InvenTree/plugin/helpers.py index c44976dcb4..927fd44a7c 100644 --- a/InvenTree/plugin/helpers.py +++ b/InvenTree/plugin/helpers.py @@ -44,7 +44,10 @@ class MixinNotImplementedError(NotImplementedError): pass -def get_plugin_error(error, do_raise: bool = False, do_log: bool = False, log_name: str = ''): +def handle_plugin_error(error, do_raise: bool = True, do_log: bool = True, do_return: bool = False, log_name: str = ''): + """ + Handles an error and casts it as an IntegrationPluginError + """ package_path = traceback.extract_tb(error.__traceback__)[-1].filename install_path = sysconfig.get_paths()["purelib"] try: @@ -70,10 +73,13 @@ def get_plugin_error(error, do_raise: bool = False, do_log: bool = False, log_na log_kwargs['reference'] = log_name log_plugin_error({package_name: str(error)}, **log_kwargs) + new_error = IntegrationPluginError(package_name, str(error)) + if do_raise: raise IntegrationPluginError(package_name, str(error)) - return package_name, str(error) + if do_return: + return new_error # endregion diff --git a/InvenTree/plugin/registry.py b/InvenTree/plugin/registry.py index 9e8fd1fe99..bae20c23d9 100644 --- a/InvenTree/plugin/registry.py +++ b/InvenTree/plugin/registry.py @@ -30,7 +30,7 @@ from maintenance_mode.core import get_maintenance_mode, set_maintenance_mode from plugin import plugins as inventree_plugins from .integration import IntegrationPluginBase -from .helpers import get_plugin_error, IntegrationPluginError +from .helpers import handle_plugin_error, IntegrationPluginError logger = logging.getLogger('inventree') @@ -180,7 +180,7 @@ class PluginsRegistry: plugin.is_package = True self.plugin_modules.append(plugin) except Exception as error: - get_plugin_error(error, do_log=True, log_name='discovery') + handle_plugin_error(error, do_raise=False, log_name='discovery') # Log collected plugins logger.info(f'Collected {len(self.plugin_modules)} plugins!') @@ -259,7 +259,7 @@ class PluginsRegistry: plugin = plugin() except Exception as error: # log error and raise it -> disable plugin - get_plugin_error(error, do_raise=True, do_log=True, log_name='init') + handle_plugin_error(error, log_name='init') logger.info(f'Loaded integration plugin {plugin.slug}') plugin.is_package = was_packaged @@ -543,7 +543,7 @@ class PluginsRegistry: cmd(*args, **kwargs) return True, [] except Exception as error: - get_plugin_error(error, do_raise=True) + handle_plugin_error(error) # endregion From 3ff4ed67c3e3604290a033988fa37d717ef5a5d4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 11 Jan 2022 01:08:09 +0100 Subject: [PATCH 22/49] docstrings --- InvenTree/plugin/builtin/action/mixins.py | 12 +--- .../plugin/builtin/integration/mixins.py | 52 ++++++++++---- InvenTree/plugin/helpers.py | 18 ++++- InvenTree/plugin/integration.py | 67 ++++++++++++++----- 4 files changed, 110 insertions(+), 39 deletions(-) diff --git a/InvenTree/plugin/builtin/action/mixins.py b/InvenTree/plugin/builtin/action/mixins.py index 0be19be2ca..ba98738c84 100644 --- a/InvenTree/plugin/builtin/action/mixins.py +++ b/InvenTree/plugin/builtin/action/mixins.py @@ -19,18 +19,12 @@ class ActionMixin: super().__init__() self.add_mixin('action', 'has_action', __class__) - @property - def has_action(self): - """ - Does this plugin have everything needed for an action? - """ - return True - def action_name(self): """ - Return the action name for this plugin. + Action name for this plugin. + If the ACTION_NAME parameter is empty, - look at the PLUGIN_NAME instead. + uses the PLUGIN_NAME instead. """ if self.ACTION_NAME: return self.ACTION_NAME diff --git a/InvenTree/plugin/builtin/integration/mixins.py b/InvenTree/plugin/builtin/integration/mixins.py index 684e100d81..a2087ee879 100644 --- a/InvenTree/plugin/builtin/integration/mixins.py +++ b/InvenTree/plugin/builtin/integration/mixins.py @@ -87,6 +87,9 @@ class ScheduleMixin: SCHEDULED_TASKS = {} class MixinMeta: + """ + Meta options for this mixin + """ MIXIN_NAME = 'Schedule' def __init__(self): @@ -98,6 +101,9 @@ class ScheduleMixin: @property def has_scheduled_tasks(self): + """ + Are tasks defined for this plugin + """ return bool(self.scheduled_tasks) def validate_scheduled_tasks(self): @@ -126,11 +132,17 @@ class ScheduleMixin: raise MixinImplementationError(f"Task '{key}' is missing 'minutes' parameter") def get_task_name(self, key): + """ + Task name for key + """ # Generate a 'unique' task name slug = self.plugin_slug() return f"plugin.{slug}.{key}" def get_task_names(self): + """ + All defined task names + """ # Returns a list of all task names associated with this plugin instance return [self.get_task_name(key) for key in self.scheduled_tasks.keys()] @@ -192,10 +204,17 @@ class EventMixin: """ def process_event(self, event, *args, **kwargs): + """ + Function to handle events + Must be overridden by plugin + """ # Default implementation does not do anything raise MixinNotImplementedError class MixinMeta: + """ + Meta options for this mixin + """ MIXIN_NAME = 'Events' def __init__(self): @@ -209,6 +228,9 @@ class UrlsMixin: """ class MixinMeta: + """ + Meta options for this mixin + """ MIXIN_NAME = 'URLs' def __init__(self): @@ -218,28 +240,28 @@ class UrlsMixin: def setup_urls(self): """ - setup url endpoints for this plugin + Setup url endpoints for this plugin """ return getattr(self, 'URLS', None) @property def base_url(self): """ - returns base url for this plugin + Base url for this plugin """ return f'{PLUGIN_BASE}/{self.slug}/' @property def internal_name(self): """ - returns the internal url pattern name + Internal url pattern name """ return f'plugin:{self.slug}:' @property def urlpatterns(self): """ - returns the urlpatterns for this plugin + Urlpatterns for this plugin """ if self.has_urls: return url(f'^{self.slug}/', include((self.urls, self.slug)), name=self.slug) @@ -248,7 +270,7 @@ class UrlsMixin: @property def has_urls(self): """ - does this plugin use custom urls + Does this plugin use custom urls """ return bool(self.urls) @@ -263,7 +285,7 @@ class NavigationMixin: class MixinMeta: """ - meta options for this mixin + Meta options for this mixin """ MIXIN_NAME = 'Navigation Links' @@ -274,7 +296,7 @@ class NavigationMixin: def setup_navigation(self): """ - setup navigation links for this plugin + Setup navigation links for this plugin """ nav_links = getattr(self, 'NAVIGATION', None) if nav_links: @@ -287,13 +309,15 @@ class NavigationMixin: @property def has_naviation(self): """ - does this plugin define navigation elements + Does this plugin define navigation elements """ return bool(self.navigation) @property def navigation_name(self): - """name for navigation tab""" + """ + Name for navigation tab + """ name = getattr(self, 'NAVIGATION_TAB_NAME', None) if not name: name = self.human_name @@ -301,7 +325,9 @@ class NavigationMixin: @property def navigation_icon(self): - """icon for navigation tab""" + """ + Icon-name for navigation tab + """ return getattr(self, 'NAVIGATION_TAB_ICON', "fas fa-question") @@ -311,7 +337,9 @@ class AppMixin: """ class MixinMeta: - """meta options for this mixin""" + """m + Mta options for this mixin + """ MIXIN_NAME = 'App registration' def __init__(self): @@ -321,7 +349,7 @@ class AppMixin: @property def has_app(self): """ - this plugin is always an app with this plugin + This plugin is always an app with this plugin """ return True diff --git a/InvenTree/plugin/helpers.py b/InvenTree/plugin/helpers.py index 927fd44a7c..e7c7f4a3a8 100644 --- a/InvenTree/plugin/helpers.py +++ b/InvenTree/plugin/helpers.py @@ -10,6 +10,9 @@ from django.conf import settings # region logging / errors def log_plugin_error(error, reference: str = 'general'): + """ + Log an plugin error + """ from plugin import registry # make sure the registry is set up @@ -21,6 +24,9 @@ def log_plugin_error(error, reference: str = 'general'): class IntegrationPluginError(Exception): + """ + Error that encapsulates another error and adds the path / reference of the raising plugin + """ def __init__(self, path, message): self.path = path self.message = message @@ -85,7 +91,9 @@ def handle_plugin_error(error, do_raise: bool = True, do_log: bool = True, do_re # region git-helpers def get_git_log(path): - """get dict with info of the last commit to file named in path""" + """ + Get dict with info of the last commit to file named in path + """ path = path.replace(os.path.dirname(settings.BASE_DIR), '')[1:] command = ['git', 'log', '-n', '1', "--pretty=format:'%H%n%aN%n%aE%n%aI%n%f%n%G?%n%GK'", '--follow', '--', path] try: @@ -100,9 +108,13 @@ def get_git_log(path): class GitStatus: - """class for resolving git gpg singing state""" + """ + Class for resolving git gpg singing state + """ class Definition: - """definition of a git gpg sing state""" + """ + Definition of a git gpg sing state + """ key: str = 'N' status: int = 2 msg: str = '' diff --git a/InvenTree/plugin/integration.py b/InvenTree/plugin/integration.py index c00b81419d..9936fed1fa 100644 --- a/InvenTree/plugin/integration.py +++ b/InvenTree/plugin/integration.py @@ -20,7 +20,7 @@ logger = logging.getLogger("inventree") class MixinBase: """ - General base for mixins + Base set of mixin functions and mechanisms """ def __init__(self) -> None: @@ -87,20 +87,31 @@ class IntegrationPluginBase(MixinBase, plugin.InvenTreePlugin): @property def _is_package(self): + """ + Is the plugin delivered as a package + """ return getattr(self, 'is_package', False) # region properties @property def slug(self): + """ + Slug of plugin + """ return self.plugin_slug() @property def name(self): + """ + Name of plugin + """ return self.plugin_name() @property def human_name(self): - """human readable name for labels etc.""" + """ + Human readable name of plugin + """ human_name = getattr(self, 'PLUGIN_TITLE', None) if not human_name: human_name = self.plugin_name() @@ -108,7 +119,9 @@ class IntegrationPluginBase(MixinBase, plugin.InvenTreePlugin): @property def description(self): - """description of plugin""" + """ + Description of plugin + """ description = getattr(self, 'DESCRIPTION', None) if not description: description = self.plugin_name() @@ -116,7 +129,9 @@ class IntegrationPluginBase(MixinBase, plugin.InvenTreePlugin): @property def author(self): - """returns author of plugin - either from plugin settings or git""" + """ + Author of plugin - either from plugin settings or git + """ author = getattr(self, 'AUTHOR', None) if not author: author = self.package.get('author') @@ -126,7 +141,9 @@ class IntegrationPluginBase(MixinBase, plugin.InvenTreePlugin): @property def pub_date(self): - """returns publishing date of plugin - either from plugin settings or git""" + """ + Publishing date of plugin - either from plugin settings or git + """ pub_date = getattr(self, 'PUBLISH_DATE', None) if not pub_date: pub_date = self.package.get('date') @@ -138,42 +155,56 @@ class IntegrationPluginBase(MixinBase, plugin.InvenTreePlugin): @property def version(self): - """returns version of plugin""" + """ + Version of plugin + """ version = getattr(self, 'VERSION', None) return version @property def website(self): - """returns website of plugin""" + """ + Website of plugin - if set else None + """ website = getattr(self, 'WEBSITE', None) return website @property def license(self): - """returns license of plugin""" + """ + License of plugin + """ license = getattr(self, 'LICENSE', None) return license # endregion @property def package_path(self): - """returns the path to the plugin""" + """ + Path to the plugin + """ if self._is_package: return self.__module__ return pathlib.Path(self.def_path).relative_to(settings.BASE_DIR) @property def settings_url(self): - """returns url to the settings panel""" + """ + URL to the settings panel for this plugin + """ return f'{reverse("settings")}#select-plugin-{self.slug}' # region mixins def mixin(self, key): - """check if mixin is registered""" + """ + Check if mixin is registered + """ return key in self._mixins def mixin_enabled(self, key): - """check if mixin is enabled and ready""" + """ + Check if mixin is registered, enabled and ready + """ if self.mixin(key): fnc_name = self._mixins.get(key) @@ -187,15 +218,21 @@ class IntegrationPluginBase(MixinBase, plugin.InvenTreePlugin): # region package info def get_package_commit(self): - """get last git commit for plugin""" + """ + Get last git commit for the plugin + """ return get_git_log(self.def_path) def get_package_metadata(self): - """get package metadata for plugin""" + """ + Get package metadata for plugin + """ return {} def set_package(self): - """add packaging info of the plugins into plugins context""" + """ + Add package info of the plugin into plugins context + """ package = self.get_package_metadata() if self._is_package else self.get_package_commit() # process date From f612f579926db6f110254c1fd4b6b0e89781de4b Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 11 Jan 2022 01:08:35 +0100 Subject: [PATCH 23/49] make registration simpler --- InvenTree/plugin/builtin/action/mixins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/plugin/builtin/action/mixins.py b/InvenTree/plugin/builtin/action/mixins.py index ba98738c84..18a1876659 100644 --- a/InvenTree/plugin/builtin/action/mixins.py +++ b/InvenTree/plugin/builtin/action/mixins.py @@ -17,7 +17,7 @@ class ActionMixin: def __init__(self): super().__init__() - self.add_mixin('action', 'has_action', __class__) + self.add_mixin('action', True, __class__) def action_name(self): """ From 960181182787a2daa19c6d48c599d6697cee23e1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 11 Jan 2022 01:08:51 +0100 Subject: [PATCH 24/49] docstrings --- .../plugin/templatetags/plugin_extras.py | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/InvenTree/plugin/templatetags/plugin_extras.py b/InvenTree/plugin/templatetags/plugin_extras.py index e1043a9aa0..f9557c84ff 100644 --- a/InvenTree/plugin/templatetags/plugin_extras.py +++ b/InvenTree/plugin/templatetags/plugin_extras.py @@ -15,31 +15,41 @@ register = template.Library() @register.simple_tag() def plugin_list(*args, **kwargs): - """ Return a list of all installed integration plugins """ + """ + List of all installed integration plugins + """ return registry.plugins @register.simple_tag() def inactive_plugin_list(*args, **kwargs): - """ Return a list of all inactive integration plugins """ + """ + List of all inactive integration plugins + """ return registry.plugins_inactive @register.simple_tag() def plugin_settings(plugin, *args, **kwargs): - """ Return a list of all custom settings for a plugin """ + """ + List of all settings for the plugin + """ return registry.mixins_settings.get(plugin) @register.simple_tag() def mixin_enabled(plugin, key, *args, **kwargs): - """ Return if the mixin is existant and configured in the plugin """ + """ + Is the mixin registerd and configured in the plugin? + """ return plugin.mixin_enabled(key) @register.simple_tag() def navigation_enabled(*args, **kwargs): - """Return if plugin navigation is enabled""" + """ + Is plugin navigation enabled? + """ if djangosettings.PLUGIN_TESTING: return True return InvenTreeSetting.get_setting('ENABLE_PLUGINS_NAVIGATION') @@ -47,7 +57,10 @@ def navigation_enabled(*args, **kwargs): @register.simple_tag() def safe_url(view_name, *args, **kwargs): - """ safe lookup for urls """ + """ + Safe lookup fnc for URLs + Returns None if not found + """ try: return reverse(view_name, args=args, kwargs=kwargs) except: @@ -56,5 +69,7 @@ def safe_url(view_name, *args, **kwargs): @register.simple_tag() def plugin_errors(*args, **kwargs): - """Return all plugin errors""" + """ + All plugin errors in the current session + """ return registry.errors From 0b9a6a11236ca23306df73818c2955016226de45 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 11 Jan 2022 01:09:04 +0100 Subject: [PATCH 25/49] registry reanme fix --- InvenTree/plugin/models.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/InvenTree/plugin/models.py b/InvenTree/plugin/models.py index 8b81eb2062..8eef92acea 100644 --- a/InvenTree/plugin/models.py +++ b/InvenTree/plugin/models.py @@ -10,7 +10,7 @@ from django.db import models import common.models -from plugin import InvenTreePlugin, plugin_registry +from plugin import InvenTreePlugin, registry class PluginConfig(models.Model): @@ -72,7 +72,7 @@ class PluginConfig(models.Model): self.__org_active = self.active # append settings from registry - self.plugin = plugin_registry.plugins.get(self.key, None) + self.plugin = registry.plugins.get(self.key, None) def get_plugin_meta(name): if self.plugin: @@ -95,10 +95,10 @@ class PluginConfig(models.Model): if not reload: if self.active is False and self.__org_active is True: - plugin_registry.reload_plugins() + registry.reload_plugins() elif self.active is True and self.__org_active is False: - plugin_registry.reload_plugins() + registry.reload_plugins() return ret @@ -167,7 +167,7 @@ class PluginSetting(common.models.BaseInvenTreeSetting): if issubclass(plugin.__class__, InvenTreePlugin): plugin = plugin.plugin_config() - kwargs['settings'] = plugin_registry.mixins_settings.get(plugin.key, {}) + kwargs['settings'] = registry.mixins_settings.get(plugin.key, {}) return super().get_setting_definition(key, **kwargs) From 5424ee38e7cfd4b44640c432d6ba4f2e6ee8b2d3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 11 Jan 2022 01:10:34 +0100 Subject: [PATCH 26/49] rename to makr internal functions as internal so --- InvenTree/plugin/integration.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/InvenTree/plugin/integration.py b/InvenTree/plugin/integration.py index 9936fed1fa..825472fe7c 100644 --- a/InvenTree/plugin/integration.py +++ b/InvenTree/plugin/integration.py @@ -83,7 +83,7 @@ class IntegrationPluginBase(MixinBase, plugin.InvenTreePlugin): self.def_path = inspect.getfile(self.__class__) self.path = os.path.dirname(self.def_path) - self.set_package() + self.define_package() @property def _is_package(self): @@ -217,23 +217,23 @@ class IntegrationPluginBase(MixinBase, plugin.InvenTreePlugin): # endregion # region package info - def get_package_commit(self): + def _get_package_commit(self): """ Get last git commit for the plugin """ return get_git_log(self.def_path) - def get_package_metadata(self): + def _get_package_metadata(self): """ Get package metadata for plugin """ return {} - def set_package(self): + def define_package(self): """ Add package info of the plugin into plugins context """ - package = self.get_package_metadata() if self._is_package else self.get_package_commit() + package = self._get_package_metadata() if self._is_package else self._get_package_commit() # process date if package.get('date'): From 56772ccd02a31cd8835a99731385f9dabb8fd32f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 11 Jan 2022 01:10:59 +0100 Subject: [PATCH 27/49] docstring fix --- InvenTree/plugin/integration.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/InvenTree/plugin/integration.py b/InvenTree/plugin/integration.py index 825472fe7c..348344f1ba 100644 --- a/InvenTree/plugin/integration.py +++ b/InvenTree/plugin/integration.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- -"""class for IntegrationPluginBase and Mixins for it""" +""" +Class for IntegrationPluginBase and Mixin Base +""" import logging import os From 67a501f438823e2beeff91009dd93203320ea778 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 11 Jan 2022 01:12:19 +0100 Subject: [PATCH 28/49] rename --- InvenTree/plugin/helpers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/plugin/helpers.py b/InvenTree/plugin/helpers.py index e7c7f4a3a8..83fb0c48e0 100644 --- a/InvenTree/plugin/helpers.py +++ b/InvenTree/plugin/helpers.py @@ -9,7 +9,7 @@ from django.conf import settings # region logging / errors -def log_plugin_error(error, reference: str = 'general'): +def log_error(error, reference: str = 'general'): """ Log an plugin error """ @@ -77,7 +77,7 @@ def handle_plugin_error(error, do_raise: bool = True, do_log: bool = True, do_re log_kwargs = {} if log_name: log_kwargs['reference'] = log_name - log_plugin_error({package_name: str(error)}, **log_kwargs) + log_error({package_name: str(error)}, **log_kwargs) new_error = IntegrationPluginError(package_name, str(error)) From 9bb6bb294c1fbf36ed5acf914073e3aed1b9e096 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 11 Jan 2022 01:12:50 +0100 Subject: [PATCH 29/49] rename --- InvenTree/plugin/helpers.py | 2 +- InvenTree/plugin/plugins.py | 4 ++-- InvenTree/plugin/registry.py | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/InvenTree/plugin/helpers.py b/InvenTree/plugin/helpers.py index 83fb0c48e0..3a42cdfd5d 100644 --- a/InvenTree/plugin/helpers.py +++ b/InvenTree/plugin/helpers.py @@ -50,7 +50,7 @@ class MixinNotImplementedError(NotImplementedError): pass -def handle_plugin_error(error, do_raise: bool = True, do_log: bool = True, do_return: bool = False, log_name: str = ''): +def handle_error(error, do_raise: bool = True, do_log: bool = True, do_return: bool = False, log_name: str = ''): """ Handles an error and casts it as an IntegrationPluginError """ diff --git a/InvenTree/plugin/plugins.py b/InvenTree/plugin/plugins.py index ae315ebf32..65ca8e29d9 100644 --- a/InvenTree/plugin/plugins.py +++ b/InvenTree/plugin/plugins.py @@ -15,7 +15,7 @@ def iter_namespace(pkg): def get_modules(pkg, recursive: bool = False): """get all modules in a package""" - from plugin.helpers import log_plugin_error + from plugin.helpers import log_error if not recursive: return [importlib.import_module(name) for finder, name, ispkg in iter_namespace(pkg)] @@ -35,7 +35,7 @@ def get_modules(pkg, recursive: bool = False): # this 'protects' against malformed plugin modules by more or less silently failing # log to stack - log_plugin_error({name: str(error)}, 'discovery') + log_error({name: str(error)}, 'discovery') return [v for k, v in context.items()] diff --git a/InvenTree/plugin/registry.py b/InvenTree/plugin/registry.py index bae20c23d9..a65614cf86 100644 --- a/InvenTree/plugin/registry.py +++ b/InvenTree/plugin/registry.py @@ -30,7 +30,7 @@ from maintenance_mode.core import get_maintenance_mode, set_maintenance_mode from plugin import plugins as inventree_plugins from .integration import IntegrationPluginBase -from .helpers import handle_plugin_error, IntegrationPluginError +from .helpers import handle_error, IntegrationPluginError logger = logging.getLogger('inventree') @@ -67,7 +67,7 @@ class PluginsRegistry: Load and activate all IntegrationPlugins """ - from plugin.helpers import log_plugin_error + from plugin.helpers import log_error logger.info('Start loading plugins') @@ -91,7 +91,7 @@ class PluginsRegistry: break except IntegrationPluginError as error: logger.error(f'[PLUGIN] Encountered an error with {error.path}:\n{error.message}') - log_plugin_error({error.path: error.message}, 'load') + log_error({error.path: error.message}, 'load') blocked_plugin = error.path # we will not try to load this app again # Initialize apps without any integration plugins @@ -180,7 +180,7 @@ class PluginsRegistry: plugin.is_package = True self.plugin_modules.append(plugin) except Exception as error: - handle_plugin_error(error, do_raise=False, log_name='discovery') + handle_error(error, do_raise=False, log_name='discovery') # Log collected plugins logger.info(f'Collected {len(self.plugin_modules)} plugins!') @@ -259,7 +259,7 @@ class PluginsRegistry: plugin = plugin() except Exception as error: # log error and raise it -> disable plugin - handle_plugin_error(error, log_name='init') + handle_error(error, log_name='init') logger.info(f'Loaded integration plugin {plugin.slug}') plugin.is_package = was_packaged @@ -543,7 +543,7 @@ class PluginsRegistry: cmd(*args, **kwargs) return True, [] except Exception as error: - handle_plugin_error(error) + handle_error(error) # endregion From 9b02e3bdb7dd5fe47ce4702da4ee817ce09b6279 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 11 Jan 2022 01:13:36 +0100 Subject: [PATCH 30/49] reafactor --- InvenTree/plugin/helpers.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/InvenTree/plugin/helpers.py b/InvenTree/plugin/helpers.py index 3a42cdfd5d..e3af46c0e6 100644 --- a/InvenTree/plugin/helpers.py +++ b/InvenTree/plugin/helpers.py @@ -9,20 +9,6 @@ from django.conf import settings # region logging / errors -def log_error(error, reference: str = 'general'): - """ - Log an plugin error - """ - from plugin import registry - - # make sure the registry is set up - if reference not in registry.errors: - registry.errors[reference] = [] - - # add error to stack - registry.errors[reference].append(error) - - class IntegrationPluginError(Exception): """ Error that encapsulates another error and adds the path / reference of the raising plugin @@ -50,6 +36,20 @@ class MixinNotImplementedError(NotImplementedError): pass +def log_error(error, reference: str = 'general'): + """ + Log an plugin error + """ + from plugin import registry + + # make sure the registry is set up + if reference not in registry.errors: + registry.errors[reference] = [] + + # add error to stack + registry.errors[reference].append(error) + + def handle_error(error, do_raise: bool = True, do_log: bool = True, do_return: bool = False, log_name: str = ''): """ Handles an error and casts it as an IntegrationPluginError From fa6a20712f939f28bfed1820f7ff5cace3af33fb Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 11 Jan 2022 01:13:52 +0100 Subject: [PATCH 31/49] docstring --- InvenTree/plugin/helpers.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/InvenTree/plugin/helpers.py b/InvenTree/plugin/helpers.py index e3af46c0e6..ff0c4c4c4a 100644 --- a/InvenTree/plugin/helpers.py +++ b/InvenTree/plugin/helpers.py @@ -1,4 +1,6 @@ -"""Helpers for plugin app""" +""" +Helpers for plugin app +""" import os import subprocess import pathlib From d215af45f1eb9771f1923165476608cbe9ee0274 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 11 Jan 2022 01:18:51 +0100 Subject: [PATCH 32/49] simplify code --- InvenTree/plugin/integration.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/InvenTree/plugin/integration.py b/InvenTree/plugin/integration.py index 348344f1ba..c96bcb7f6e 100644 --- a/InvenTree/plugin/integration.py +++ b/InvenTree/plugin/integration.py @@ -114,10 +114,7 @@ class IntegrationPluginBase(MixinBase, plugin.InvenTreePlugin): """ Human readable name of plugin """ - human_name = getattr(self, 'PLUGIN_TITLE', None) - if not human_name: - human_name = self.plugin_name() - return human_name + return self.plugin_title() @property def description(self): From 0f7c277e69cc195bfa8fece39445c5c7d2828860 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 11 Jan 2022 01:19:03 +0100 Subject: [PATCH 33/49] docstrings --- InvenTree/plugin/plugin.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/InvenTree/plugin/plugin.py b/InvenTree/plugin/plugin.py index 8842553ba7..9ec01d42ca 100644 --- a/InvenTree/plugin/plugin.py +++ b/InvenTree/plugin/plugin.py @@ -24,11 +24,16 @@ class InvenTreePlugin(): def plugin_name(self): """ - Return the name of this plugin plugin + Name of plugin """ return self.PLUGIN_NAME def plugin_slug(self): + """ + Slug of plugin + If not set plugin name slugified + """ + slug = getattr(self, 'PLUGIN_SLUG', None) @@ -38,6 +43,9 @@ class InvenTreePlugin(): return slugify(slug.lower()) def plugin_title(self): + """ + Title of plugin + """ if self.PLUGIN_TITLE: return self.PLUGIN_TITLE From 86142856f7b8c5ddf310a7f22f153082258ae73e Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 11 Jan 2022 01:25:21 +0100 Subject: [PATCH 34/49] PEP fixes --- InvenTree/plugin/__init__.py | 2 +- InvenTree/plugin/helpers.py | 2 +- InvenTree/plugin/plugin.py | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/InvenTree/plugin/__init__.py b/InvenTree/plugin/__init__.py index 98d0d94b3a..4aef9e3cb9 100644 --- a/InvenTree/plugin/__init__.py +++ b/InvenTree/plugin/__init__.py @@ -3,7 +3,7 @@ Utility file to enable simper imports """ from .registry import registry -from .plugin import InvenTreePlugin +from .plugin import AAInvenTreePlugin as InvenTreePlugin from .integration import IntegrationPluginBase from .action import ActionPlugin diff --git a/InvenTree/plugin/helpers.py b/InvenTree/plugin/helpers.py index ff0c4c4c4a..3b9164fc8f 100644 --- a/InvenTree/plugin/helpers.py +++ b/InvenTree/plugin/helpers.py @@ -26,7 +26,7 @@ class IntegrationPluginError(Exception): class MixinImplementationError(ValueError): """ Error if mixin was implemented wrong in plugin - Mostly raised if constant is missing + Mostly raised if constant is missing """ pass diff --git a/InvenTree/plugin/plugin.py b/InvenTree/plugin/plugin.py index 9ec01d42ca..4295c5741f 100644 --- a/InvenTree/plugin/plugin.py +++ b/InvenTree/plugin/plugin.py @@ -7,7 +7,7 @@ from django.db.utils import OperationalError, ProgrammingError from django.utils.text import slugify -class InvenTreePlugin(): +class AAInvenTreePlugin(): """ Base class for a plugin """ @@ -34,7 +34,6 @@ class InvenTreePlugin(): If not set plugin name slugified """ - slug = getattr(self, 'PLUGIN_SLUG', None) if slug is None: From 52d90cef465fbd5ed635a573450d40ba1e8dbf07 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 11 Jan 2022 01:29:57 +0100 Subject: [PATCH 35/49] fix path --- InvenTree/InvenTree/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index c140bdecb9..25f04078bc 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -874,7 +874,7 @@ MARKDOWNIFY_BLEACH = False MAINTENANCE_MODE_RETRY_AFTER = 60 # Plugin Directories (local plugins will be loaded from these directories) -PLUGIN_DIRS = ['plugin.builtin', 'barcodes.acion', 'barcodes.plugins', ] +PLUGIN_DIRS = ['plugin.builtin', 'plugin.builtin.acion', 'barcodes.plugins', ] if not TESTING: # load local deploy directory in prod From 2a7ad931730201a8ae64d64e0be561d9c429a2c1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 11 Jan 2022 01:41:33 +0100 Subject: [PATCH 36/49] move invenTreePlugin to new class to enable depreceation --- InvenTree/common/models.py | 4 ++-- InvenTree/plugin/__init__.py | 4 ++-- InvenTree/plugin/integration.py | 4 ++-- InvenTree/plugin/models.py | 6 +++--- InvenTree/plugin/plugin.py | 14 +++++++++++++- InvenTree/plugin/test_plugin.py | 7 +++---- 6 files changed, 25 insertions(+), 14 deletions(-) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 50c95966cc..f0d19b3532 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -250,9 +250,9 @@ class BaseInvenTreeSetting(models.Model): plugin = kwargs.pop('plugin', None) if plugin: - from plugin import InvenTreePlugin + from plugin import InvenTreePluginBase - if issubclass(plugin.__class__, InvenTreePlugin): + if issubclass(plugin.__class__, InvenTreePluginBase): plugin = plugin.plugin_config() kwargs['plugin'] = plugin diff --git a/InvenTree/plugin/__init__.py b/InvenTree/plugin/__init__.py index 4aef9e3cb9..b8e40e4271 100644 --- a/InvenTree/plugin/__init__.py +++ b/InvenTree/plugin/__init__.py @@ -3,7 +3,7 @@ Utility file to enable simper imports """ from .registry import registry -from .plugin import AAInvenTreePlugin as InvenTreePlugin +from .plugin import InvenTreePluginBase from .integration import IntegrationPluginBase from .action import ActionPlugin @@ -12,7 +12,7 @@ from .helpers import MixinNotImplementedError, MixinImplementationError __all__ = [ 'ActionPlugin', 'IntegrationPluginBase', - 'InvenTreePlugin', + 'InvenTreePluginBase', 'registry', 'MixinNotImplementedError', 'MixinImplementationError', diff --git a/InvenTree/plugin/integration.py b/InvenTree/plugin/integration.py index c96bcb7f6e..33afae852c 100644 --- a/InvenTree/plugin/integration.py +++ b/InvenTree/plugin/integration.py @@ -13,7 +13,7 @@ from django.urls.base import reverse from django.conf import settings from django.utils.translation import ugettext_lazy as _ -import plugin.plugin as plugin +import plugin.plugin as plugin_base from plugin.helpers import get_git_log, GitStatus @@ -67,7 +67,7 @@ class MixinBase: return mixins -class IntegrationPluginBase(MixinBase, plugin.InvenTreePlugin): +class IntegrationPluginBase(MixinBase, plugin_base.InvenTreePluginBase): """ The IntegrationPluginBase class is used to integrate with 3rd party software """ diff --git a/InvenTree/plugin/models.py b/InvenTree/plugin/models.py index 8eef92acea..30735e7510 100644 --- a/InvenTree/plugin/models.py +++ b/InvenTree/plugin/models.py @@ -10,7 +10,7 @@ from django.db import models import common.models -from plugin import InvenTreePlugin, registry +from plugin import InvenTreePluginBase, registry class PluginConfig(models.Model): @@ -164,7 +164,7 @@ class PluginSetting(common.models.BaseInvenTreeSetting): if plugin: - if issubclass(plugin.__class__, InvenTreePlugin): + if issubclass(plugin.__class__, InvenTreePluginBase): plugin = plugin.plugin_config() kwargs['settings'] = registry.mixins_settings.get(plugin.key, {}) @@ -182,7 +182,7 @@ class PluginSetting(common.models.BaseInvenTreeSetting): plugin = kwargs.get('plugin', None) if plugin: - if issubclass(plugin.__class__, InvenTreePlugin): + if issubclass(plugin.__class__, InvenTreePluginBase): plugin = plugin.plugin_config() filters['plugin'] = plugin diff --git a/InvenTree/plugin/plugin.py b/InvenTree/plugin/plugin.py index 4295c5741f..9abbcc041e 100644 --- a/InvenTree/plugin/plugin.py +++ b/InvenTree/plugin/plugin.py @@ -2,14 +2,16 @@ """ Base Class for InvenTree plugins """ +import warnings from django.db.utils import OperationalError, ProgrammingError from django.utils.text import slugify -class AAInvenTreePlugin(): +class InvenTreePluginBase(): """ Base class for a plugin + DO NOT USE THIS DIRECTLY, USE plugin.IntegrationPluginBase """ def __init__(self): @@ -82,3 +84,13 @@ class AAInvenTreePlugin(): return cfg.active else: return False + + +# TODO @matmair remove after InvenTree 0.7.0 release +class InvenTreePlugin(InvenTreePluginBase): + """ + This is here for leagcy reasons and will be removed in the next major release + """ + def __init__(self): + warnings.warn("Using the InvenTreePlugin is depreceated", DeprecationWarning) + super().__init__() diff --git a/InvenTree/plugin/test_plugin.py b/InvenTree/plugin/test_plugin.py index b067326f69..e73664c668 100644 --- a/InvenTree/plugin/test_plugin.py +++ b/InvenTree/plugin/test_plugin.py @@ -4,20 +4,19 @@ Unit tests for plugins from django.test import TestCase -import plugin.plugin import plugin.integration from plugin.samples.integration.sample import SampleIntegrationPlugin from plugin.samples.integration.another_sample import WrongIntegrationPlugin, NoIntegrationPlugin import plugin.templatetags.plugin_extras as plugin_tags -from plugin import registry +from plugin import registry, InvenTreePluginBase class InvenTreePluginTests(TestCase): """ Tests for InvenTreePlugin """ def setUp(self): - self.plugin = plugin.plugin.InvenTreePlugin() + self.plugin = InvenTreePluginBase() - class NamedPlugin(plugin.plugin.InvenTreePlugin): + class NamedPlugin(InvenTreePluginBase): """a named plugin""" PLUGIN_NAME = 'abc123' From 208bcb7707b76ba58c8f64d703af5d0d494091b2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 11 Jan 2022 01:41:56 +0100 Subject: [PATCH 37/49] and remove actions again --- InvenTree/InvenTree/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 25f04078bc..44796efb02 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -874,7 +874,7 @@ MARKDOWNIFY_BLEACH = False MAINTENANCE_MODE_RETRY_AFTER = 60 # Plugin Directories (local plugins will be loaded from these directories) -PLUGIN_DIRS = ['plugin.builtin', 'plugin.builtin.acion', 'barcodes.plugins', ] +PLUGIN_DIRS = ['plugin.builtin', 'barcodes.plugins', ] if not TESTING: # load local deploy directory in prod From 7c51d0e399b6d369b4b37ed70c18dd68f25a1785 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 11 Jan 2022 01:44:55 +0100 Subject: [PATCH 38/49] PEP fix --- InvenTree/plugin/test_plugin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/InvenTree/plugin/test_plugin.py b/InvenTree/plugin/test_plugin.py index e73664c668..f88b6e6176 100644 --- a/InvenTree/plugin/test_plugin.py +++ b/InvenTree/plugin/test_plugin.py @@ -4,7 +4,6 @@ Unit tests for plugins from django.test import TestCase -import plugin.integration from plugin.samples.integration.sample import SampleIntegrationPlugin from plugin.samples.integration.another_sample import WrongIntegrationPlugin, NoIntegrationPlugin import plugin.templatetags.plugin_extras as plugin_tags From a31ff85c89790144de360499d92519952f6a48e1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 11 Jan 2022 01:45:20 +0100 Subject: [PATCH 39/49] rename to make clearer --- InvenTree/plugin/action.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/plugin/action.py b/InvenTree/plugin/action.py index 6800ed2076..fb3b9b90fb 100644 --- a/InvenTree/plugin/action.py +++ b/InvenTree/plugin/action.py @@ -4,7 +4,7 @@ import logging import warnings -import plugin.plugin as plugin +import plugin.plugin as plugin_base from plugin.builtin.action.mixins import ActionMixin import plugin.integration @@ -12,7 +12,7 @@ import plugin.integration logger = logging.getLogger("inventree") -class ActionPlugin(ActionMixin, plugin.integration.IntegrationPluginBase): +class ActionPlugin(ActionMixin, plugin_base.integration.IntegrationPluginBase): """ Legacy action definition - will be replaced Please use the new Integration Plugin API and the Action mixin From cd9e9a367baf6078ce09cd4b8d8acfa6060a895f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 11 Jan 2022 01:49:42 +0100 Subject: [PATCH 40/49] PEP fix --- InvenTree/plugin/action.py | 1 - 1 file changed, 1 deletion(-) diff --git a/InvenTree/plugin/action.py b/InvenTree/plugin/action.py index fb3b9b90fb..250b28a1b8 100644 --- a/InvenTree/plugin/action.py +++ b/InvenTree/plugin/action.py @@ -6,7 +6,6 @@ import warnings import plugin.plugin as plugin_base from plugin.builtin.action.mixins import ActionMixin -import plugin.integration logger = logging.getLogger("inventree") From db197a98b69784e52c8f9470032fab70c699ab5f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 11 Jan 2022 01:51:19 +0100 Subject: [PATCH 41/49] PEP fix --- InvenTree/plugin/action.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/plugin/action.py b/InvenTree/plugin/action.py index 250b28a1b8..9586355aea 100644 --- a/InvenTree/plugin/action.py +++ b/InvenTree/plugin/action.py @@ -4,14 +4,14 @@ import logging import warnings -import plugin.plugin as plugin_base from plugin.builtin.action.mixins import ActionMixin +import plugin.integration logger = logging.getLogger("inventree") -class ActionPlugin(ActionMixin, plugin_base.integration.IntegrationPluginBase): +class ActionPlugin(ActionMixin, plugin.integration.IntegrationPluginBase): """ Legacy action definition - will be replaced Please use the new Integration Plugin API and the Action mixin From 164ee5fa5c1e305702ae3d373fd5605b03379d91 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 12 Jan 2022 00:08:58 +0100 Subject: [PATCH 42/49] fix reference --- InvenTree/plugin/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/plugin/admin.py b/InvenTree/plugin/admin.py index 790c3c6c5d..ba5148e6e3 100644 --- a/InvenTree/plugin/admin.py +++ b/InvenTree/plugin/admin.py @@ -23,7 +23,7 @@ def plugin_update(queryset, new_status: bool): # Reload plugins if they changed if apps_changed: - pl_registry.registry.reload_plugins() + pl_registry.reload_plugins() @admin.action(description='Activate plugin(s)') From 4f74ae3fcedd69678738d6d7316ccdbc300ef8dd Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 12 Jan 2022 00:22:06 +0100 Subject: [PATCH 43/49] readd import --- InvenTree/plugin/registry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/plugin/registry.py b/InvenTree/plugin/registry.py index d50233782d..151868f77c 100644 --- a/InvenTree/plugin/registry.py +++ b/InvenTree/plugin/registry.py @@ -30,7 +30,7 @@ from maintenance_mode.core import get_maintenance_mode, set_maintenance_mode from plugin import plugins as inventree_plugins from .integration import IntegrationPluginBase -from .helpers import handle_error, IntegrationPluginError +from .helpers import handle_error, log_error, IntegrationPluginError logger = logging.getLogger('inventree') From 3dab1ab73674caa355a9ac9ccb798682b0b3a292 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 12 Jan 2022 00:56:18 +0100 Subject: [PATCH 44/49] remove unneeded branches --- InvenTree/plugin/plugins.py | 15 +++------------ InvenTree/plugin/registry.py | 2 +- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/InvenTree/plugin/plugins.py b/InvenTree/plugin/plugins.py index 65ca8e29d9..0d51442d45 100644 --- a/InvenTree/plugin/plugins.py +++ b/InvenTree/plugin/plugins.py @@ -2,24 +2,15 @@ """general functions for plugin handeling""" import inspect -import importlib import pkgutil from django.core.exceptions import AppRegistryNotReady -def iter_namespace(pkg): - """get all modules in a package""" - return pkgutil.iter_modules(pkg.__path__, pkg.__name__ + ".") - - -def get_modules(pkg, recursive: bool = False): +def get_modules(pkg): """get all modules in a package""" from plugin.helpers import log_error - if not recursive: - return [importlib.import_module(name) for finder, name, ispkg in iter_namespace(pkg)] - context = {} for loader, name, ispkg in pkgutil.walk_packages(pkg.__path__): try: @@ -45,7 +36,7 @@ def get_classes(module): return inspect.getmembers(module, inspect.isclass) -def get_plugins(pkg, baseclass, recursive: bool = False): +def get_plugins(pkg, baseclass): """ Return a list of all modules under a given package. @@ -55,7 +46,7 @@ def get_plugins(pkg, baseclass, recursive: bool = False): plugins = [] - modules = get_modules(pkg, recursive) + modules = get_modules(pkg) # Iterate through each module in the package for mod in modules: diff --git a/InvenTree/plugin/registry.py b/InvenTree/plugin/registry.py index 151868f77c..6c43296c76 100644 --- a/InvenTree/plugin/registry.py +++ b/InvenTree/plugin/registry.py @@ -177,7 +177,7 @@ class PluginsRegistry: # Collect plugins from paths for plugin in settings.PLUGIN_DIRS: - modules = inventree_plugins.get_plugins(importlib.import_module(plugin), IntegrationPluginBase, True) + modules = inventree_plugins.get_plugins(importlib.import_module(plugin), IntegrationPluginBase) if modules: [self.plugin_modules.append(item) for item in modules] From 678b89e09398720ce09f51ff9374ea9cab80cf79 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 12 Jan 2022 00:59:09 +0100 Subject: [PATCH 45/49] consolidate helpers --- InvenTree/plugin/helpers.py | 56 ++++++++++++++++++++++++++++++++++ InvenTree/plugin/plugins.py | 59 ------------------------------------ InvenTree/plugin/registry.py | 5 ++- 3 files changed, 58 insertions(+), 62 deletions(-) delete mode 100644 InvenTree/plugin/plugins.py diff --git a/InvenTree/plugin/helpers.py b/InvenTree/plugin/helpers.py index 3b9164fc8f..31eac195c3 100644 --- a/InvenTree/plugin/helpers.py +++ b/InvenTree/plugin/helpers.py @@ -6,8 +6,11 @@ import subprocess import pathlib import sysconfig import traceback +import inspect +import pkgutil from django.conf import settings +from django.core.exceptions import AppRegistryNotReady # region logging / errors @@ -135,3 +138,56 @@ class GitStatus: R = Definition(key='R', status=2, msg='good signature, revoked key',) E = Definition(key='E', status=1, msg='cannot be checked',) # endregion + + +# region plugin finders +def get_modules(pkg): + """get all modules in a package""" + + context = {} + for loader, name, ispkg in pkgutil.walk_packages(pkg.__path__): + try: + module = loader.find_module(name).load_module(name) + pkg_names = getattr(module, '__all__', None) + for k, v in vars(module).items(): + if not k.startswith('_') and (pkg_names is None or k in pkg_names): + context[k] = v + context[name] = module + except AppRegistryNotReady: + pass + except Exception as error: + # this 'protects' against malformed plugin modules by more or less silently failing + + # log to stack + log_error({name: str(error)}, 'discovery') + + return [v for k, v in context.items()] + + +def get_classes(module): + """get all classes in a given module""" + return inspect.getmembers(module, inspect.isclass) + + +def get_plugins(pkg, baseclass): + """ + Return a list of all modules under a given package. + + - Modules must be a subclass of the provided 'baseclass' + - Modules must have a non-empty PLUGIN_NAME parameter + """ + + plugins = [] + + modules = get_modules(pkg) + + # Iterate through each module in the package + for mod in modules: + # Iterate through each class in the module + for item in get_classes(mod): + plugin = item[1] + if issubclass(plugin, baseclass) and plugin.PLUGIN_NAME: + plugins.append(plugin) + + return plugins +# endregion \ No newline at end of file diff --git a/InvenTree/plugin/plugins.py b/InvenTree/plugin/plugins.py deleted file mode 100644 index 0d51442d45..0000000000 --- a/InvenTree/plugin/plugins.py +++ /dev/null @@ -1,59 +0,0 @@ -# -*- coding: utf-8 -*- -"""general functions for plugin handeling""" - -import inspect -import pkgutil - -from django.core.exceptions import AppRegistryNotReady - - -def get_modules(pkg): - """get all modules in a package""" - from plugin.helpers import log_error - - context = {} - for loader, name, ispkg in pkgutil.walk_packages(pkg.__path__): - try: - module = loader.find_module(name).load_module(name) - pkg_names = getattr(module, '__all__', None) - for k, v in vars(module).items(): - if not k.startswith('_') and (pkg_names is None or k in pkg_names): - context[k] = v - context[name] = module - except AppRegistryNotReady: - pass - except Exception as error: - # this 'protects' against malformed plugin modules by more or less silently failing - - # log to stack - log_error({name: str(error)}, 'discovery') - - return [v for k, v in context.items()] - - -def get_classes(module): - """get all classes in a given module""" - return inspect.getmembers(module, inspect.isclass) - - -def get_plugins(pkg, baseclass): - """ - Return a list of all modules under a given package. - - - Modules must be a subclass of the provided 'baseclass' - - Modules must have a non-empty PLUGIN_NAME parameter - """ - - plugins = [] - - modules = get_modules(pkg) - - # Iterate through each module in the package - for mod in modules: - # Iterate through each class in the module - for item in get_classes(mod): - plugin = item[1] - if issubclass(plugin, baseclass) and plugin.PLUGIN_NAME: - plugins.append(plugin) - - return plugins diff --git a/InvenTree/plugin/registry.py b/InvenTree/plugin/registry.py index 6c43296c76..e31f3c6529 100644 --- a/InvenTree/plugin/registry.py +++ b/InvenTree/plugin/registry.py @@ -28,9 +28,8 @@ except: from maintenance_mode.core import maintenance_mode_on from maintenance_mode.core import get_maintenance_mode, set_maintenance_mode -from plugin import plugins as inventree_plugins from .integration import IntegrationPluginBase -from .helpers import handle_error, log_error, IntegrationPluginError +from .helpers import handle_error, log_error, get_plugins, IntegrationPluginError logger = logging.getLogger('inventree') @@ -177,7 +176,7 @@ class PluginsRegistry: # Collect plugins from paths for plugin in settings.PLUGIN_DIRS: - modules = inventree_plugins.get_plugins(importlib.import_module(plugin), IntegrationPluginBase) + modules = get_plugins(importlib.import_module(plugin), IntegrationPluginBase) if modules: [self.plugin_modules.append(item) for item in modules] From 8690326a8f210c6330ca465b31e67837fcba7a67 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 12 Jan 2022 01:04:25 +0100 Subject: [PATCH 46/49] PEP fix --- InvenTree/plugin/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/plugin/helpers.py b/InvenTree/plugin/helpers.py index 31eac195c3..c56f5a9631 100644 --- a/InvenTree/plugin/helpers.py +++ b/InvenTree/plugin/helpers.py @@ -190,4 +190,4 @@ def get_plugins(pkg, baseclass): plugins.append(plugin) return plugins -# endregion \ No newline at end of file +# endregion From 7ce55f4195bd0450d9d93791a66ea41217ce0434 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 12 Jan 2022 11:57:33 +1100 Subject: [PATCH 47/49] Add try/except blocks for calls to conert_money --- InvenTree/part/models.py | 9 ++++++++- InvenTree/part/views.py | 13 +++++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 604d384a67..fde9c80720 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -29,6 +29,8 @@ from markdownx.models import MarkdownxField from django_cleanup import cleanup +from djmoney.contrib.exchange.exceptions import MissingRate + from mptt.models import TreeForeignKey, MPTTModel from mptt.exceptions import InvalidMove from mptt.managers import TreeManager @@ -1832,9 +1834,14 @@ class Part(MPTTModel): def get_purchase_price(self, quantity): currency = currency_code_default() - prices = [convert_money(item.purchase_price, currency).amount for item in self.stock_items.all() if item.purchase_price] + try: + prices = [convert_money(item.purchase_price, currency).amount for item in self.stock_items.all() if item.purchase_price] + except MissingRate: + prices = None + if prices: return min(prices) * quantity, max(prices) * quantity + return None @transaction.atomic diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 4304d2a95d..dc6b8a9632 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -20,6 +20,7 @@ from django.contrib import messages from moneyed import CURRENCIES from djmoney.contrib.exchange.models import convert_money +from djmoney.contrib.exchange.exceptions import MissingRate from PIL import Image @@ -425,7 +426,11 @@ class PartDetail(InvenTreeRoleMixin, DetailView): continue # convert purchase price to current currency - only one currency in the graph - price = convert_money(stock_item.purchase_price, default_currency) + try: + price = convert_money(stock_item.purchase_price, default_currency) + except MissingRate: + continue + line = { 'price': price.amount, 'qty': stock_item.quantity @@ -487,7 +492,11 @@ class PartDetail(InvenTreeRoleMixin, DetailView): if None in [sale_item.purchase_price, sale_item.quantity]: continue - price = convert_money(sale_item.purchase_price, default_currency) + try: + price = convert_money(sale_item.purchase_price, default_currency) + except MissingRate: + continue + line = { 'price': price.amount if price else 0, 'qty': sale_item.quantity, From 684de692518901f47ef796570e0a78e7b7e891fe Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 12 Jan 2022 13:06:50 +1100 Subject: [PATCH 48/49] Adds custom error handler page for 500 --- InvenTree/templates/500.html | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 InvenTree/templates/500.html diff --git a/InvenTree/templates/500.html b/InvenTree/templates/500.html new file mode 100644 index 0000000000..3fab6c0a17 --- /dev/null +++ b/InvenTree/templates/500.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block page_title %} +InvenTree | {% trans "Internal Server Error" %} +{% endblock %} + +{% block content %} + +
+

{% trans "Internal Server Error" %}

+ +
+ {% trans "The InvenTree server raised an internal error" %}
+ {% trans "Refer to the error log in the admin interface for further details" %} +
+
+ +{% endblock %} \ No newline at end of file From 5df8377213710c821c5d9af3856380129ce72715 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 12 Jan 2022 15:12:53 +1100 Subject: [PATCH 49/49] Fix "actions" column for part purchase order table --- InvenTree/templates/js/translated/part.js | 1 + 1 file changed, 1 insertion(+) diff --git a/InvenTree/templates/js/translated/part.js b/InvenTree/templates/js/translated/part.js index 020c5d1a6c..093aec388b 100644 --- a/InvenTree/templates/js/translated/part.js +++ b/InvenTree/templates/js/translated/part.js @@ -929,6 +929,7 @@ function loadPartPurchaseOrderTable(table, part_id, options={}) { { field: 'actions', title: '', + switchable: false, formatter: function(value, row) { if (row.received >= row.quantity) {