mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-31 05:05:42 +00:00 
			
		
		
		
	Merge pull request #2525 from matmair/matmair/issue2519
Refactor action and barcode plugins and cleanup
This commit is contained in:
		| @@ -5,8 +5,6 @@ Main JSON interface views | |||||||
| # -*- coding: utf-8 -*- | # -*- coding: utf-8 -*- | ||||||
| from __future__ import unicode_literals | from __future__ import unicode_literals | ||||||
|  |  | ||||||
| import logging |  | ||||||
|  |  | ||||||
| from django.utils.translation import ugettext_lazy as _ | from django.utils.translation import ugettext_lazy as _ | ||||||
| from django.http import JsonResponse | from django.http import JsonResponse | ||||||
|  |  | ||||||
| @@ -21,14 +19,7 @@ from .views import AjaxView | |||||||
| from .version import inventreeVersion, inventreeApiVersion, inventreeInstanceName | from .version import inventreeVersion, inventreeApiVersion, inventreeInstanceName | ||||||
| from .status import is_worker_running | from .status import is_worker_running | ||||||
|  |  | ||||||
| from plugin.plugins import load_action_plugins | from plugin import registry | ||||||
|  |  | ||||||
|  |  | ||||||
| logger = logging.getLogger("inventree") |  | ||||||
|  |  | ||||||
|  |  | ||||||
| logger.info("Loading action plugins...") |  | ||||||
| action_plugins = load_action_plugins() |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class InfoView(AjaxView): | class InfoView(AjaxView): | ||||||
| @@ -110,10 +101,11 @@ class ActionPluginView(APIView): | |||||||
|                 'error': _("No action specified") |                 'error': _("No action specified") | ||||||
|             }) |             }) | ||||||
|  |  | ||||||
|         for plugin_class in action_plugins: |         action_plugins = registry.with_mixin('action') | ||||||
|             if plugin_class.action_name() == action: |         for plugin in action_plugins: | ||||||
|  |             if plugin.action_name() == action: | ||||||
|                 plugin = plugin_class(request.user, data=data) |                 # TODO @matmair use easier syntax once InvenTree 0.7.0 is released | ||||||
|  |                 plugin.init(request.user, data=data) | ||||||
|  |  | ||||||
|                 plugin.perform_action() |                 plugin.perform_action() | ||||||
|  |  | ||||||
|   | |||||||
| @@ -880,7 +880,7 @@ PLUGINS_ENABLED = _is_true(get_setting( | |||||||
| PLUGIN_FILE = get_plugin_file() | PLUGIN_FILE = get_plugin_file() | ||||||
|  |  | ||||||
| # Plugin Directories (local plugins will be loaded from these directories) | # Plugin Directories (local plugins will be loaded from these directories) | ||||||
| PLUGIN_DIRS = ['plugin.builtin', ] | PLUGIN_DIRS = ['plugin.builtin', 'barcodes.plugins', ] | ||||||
|  |  | ||||||
| if not TESTING: | if not TESTING: | ||||||
|     # load local deploy directory in prod |     # load local deploy directory in prod | ||||||
|   | |||||||
| @@ -13,7 +13,7 @@ from stock.models import StockItem | |||||||
| from stock.serializers import StockItemSerializer | from stock.serializers import StockItemSerializer | ||||||
|  |  | ||||||
| from barcodes.barcode import hash_barcode | from barcodes.barcode import hash_barcode | ||||||
| from plugin.plugins import load_barcode_plugins | from plugin import registry | ||||||
|  |  | ||||||
|  |  | ||||||
| class BarcodeScan(APIView): | class BarcodeScan(APIView): | ||||||
| @@ -53,18 +53,19 @@ class BarcodeScan(APIView): | |||||||
|         if 'barcode' not in data: |         if 'barcode' not in data: | ||||||
|             raise ValidationError({'barcode': _('Must provide barcode_data parameter')}) |             raise ValidationError({'barcode': _('Must provide barcode_data parameter')}) | ||||||
|  |  | ||||||
|         plugins = load_barcode_plugins() |         plugins = registry.with_mixin('barcode') | ||||||
|  |  | ||||||
|         barcode_data = data.get('barcode') |         barcode_data = data.get('barcode') | ||||||
|  |  | ||||||
|         # Look for a barcode plugin which knows how to deal with this barcode |         # Look for a barcode plugin which knows how to deal with this barcode | ||||||
|         plugin = None |         plugin = None | ||||||
|  |  | ||||||
|         for plugin_class in plugins: |         for current_plugin in plugins: | ||||||
|             plugin_instance = plugin_class(barcode_data) |             # TODO @matmair make simpler after InvenTree 0.7.0 release | ||||||
|  |             current_plugin.init(barcode_data) | ||||||
|  |  | ||||||
|             if plugin_instance.validate(): |             if current_plugin.validate(): | ||||||
|                 plugin = plugin_instance |                 plugin = current_plugin | ||||||
|                 break |                 break | ||||||
|  |  | ||||||
|         match_found = False |         match_found = False | ||||||
| @@ -160,15 +161,16 @@ class BarcodeAssign(APIView): | |||||||
|         except (ValueError, StockItem.DoesNotExist): |         except (ValueError, StockItem.DoesNotExist): | ||||||
|             raise ValidationError({'stockitem': _('No matching stock item found')}) |             raise ValidationError({'stockitem': _('No matching stock item found')}) | ||||||
|  |  | ||||||
|         plugins = load_barcode_plugins() |         plugins = registry.with_mixin('barcode') | ||||||
|  |  | ||||||
|         plugin = None |         plugin = None | ||||||
|  |  | ||||||
|         for plugin_class in plugins: |         for current_plugin in plugins: | ||||||
|             plugin_instance = plugin_class(barcode_data) |             # TODO @matmair make simpler after InvenTree 0.7.0 release | ||||||
|  |             current_plugin.init(barcode_data) | ||||||
|  |  | ||||||
|             if plugin_instance.validate(): |             if current_plugin.validate(): | ||||||
|                 plugin = plugin_instance |                 plugin = current_plugin | ||||||
|                 break |                 break | ||||||
|  |  | ||||||
|         match_found = False |         match_found = False | ||||||
|   | |||||||
| @@ -1,139 +1,20 @@ | |||||||
| # -*- coding: utf-8 -*- | # -*- coding: utf-8 -*- | ||||||
|  | import warnings | ||||||
|  |  | ||||||
| import string | import plugin.builtin.barcode.mixins as mixin | ||||||
| import hashlib | import plugin.integration | ||||||
| import logging |  | ||||||
|  |  | ||||||
|  |  | ||||||
| from stock.models import StockItem | hash_barcode = mixin.hash_barcode | ||||||
| from stock.serializers import StockItemSerializer, LocationSerializer |  | ||||||
| from part.serializers import PartSerializer |  | ||||||
|  |  | ||||||
|  |  | ||||||
| logger = logging.getLogger('inventree') | class BarcodePlugin(mixin.BarcodeMixin, plugin.integration.IntegrationPluginBase): | ||||||
|  |  | ||||||
|  |  | ||||||
| def hash_barcode(barcode_data): |  | ||||||
|     """ |     """ | ||||||
|     Calculate an MD5 hash of barcode data. |     Legacy barcode plugin definition - will be replaced | ||||||
|  |     Please use the new Integration Plugin API and the BarcodeMixin | ||||||
|     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! |  | ||||||
|     """ |     """ | ||||||
|  |     # TODO @matmair remove this with InvenTree 0.7.0 | ||||||
|     barcode_data = str(barcode_data).strip() |     def __init__(self, barcode_data=None): | ||||||
|  |         warnings.warn("using the BarcodePlugin is depreceated", DeprecationWarning) | ||||||
|     printable_chars = filter(lambda x: x in string.printable, barcode_data) |         super().__init__() | ||||||
|  |         self.init(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 |  | ||||||
|   | |||||||
| @@ -258,9 +258,9 @@ class BaseInvenTreeSetting(models.Model): | |||||||
|         plugin = kwargs.pop('plugin', None) |         plugin = kwargs.pop('plugin', None) | ||||||
|  |  | ||||||
|         if plugin: |         if plugin: | ||||||
|             from plugin import InvenTreePlugin |             from plugin import InvenTreePluginBase | ||||||
|  |  | ||||||
|             if issubclass(plugin.__class__, InvenTreePlugin): |             if issubclass(plugin.__class__, InvenTreePluginBase): | ||||||
|                 plugin = plugin.plugin_config() |                 plugin = plugin.plugin_config() | ||||||
|  |  | ||||||
|             kwargs['plugin'] = plugin |             kwargs['plugin'] = plugin | ||||||
|   | |||||||
| @@ -2,14 +2,18 @@ | |||||||
| Utility file to enable simper imports | Utility file to enable simper imports | ||||||
| """ | """ | ||||||
|  |  | ||||||
| from .registry import plugin_registry | from .registry import registry | ||||||
| from .plugin import InvenTreePlugin | from .plugin import InvenTreePluginBase | ||||||
| from .integration import IntegrationPluginBase | from .integration import IntegrationPluginBase | ||||||
| from .action import ActionPlugin | from .action import ActionPlugin | ||||||
|  |  | ||||||
|  | from .helpers import MixinNotImplementedError, MixinImplementationError | ||||||
|  |  | ||||||
| __all__ = [ | __all__ = [ | ||||||
|     'ActionPlugin', |     'ActionPlugin', | ||||||
|     'IntegrationPluginBase', |     'IntegrationPluginBase', | ||||||
|     'InvenTreePlugin', |     'InvenTreePluginBase', | ||||||
|     'plugin_registry', |     'registry', | ||||||
|  |     'MixinNotImplementedError', | ||||||
|  |     'MixinImplementationError', | ||||||
| ] | ] | ||||||
|   | |||||||
| @@ -2,69 +2,22 @@ | |||||||
| """Class for ActionPlugin""" | """Class for ActionPlugin""" | ||||||
|  |  | ||||||
| import logging | import logging | ||||||
|  | import warnings | ||||||
|  |  | ||||||
| import plugin.plugin as plugin | from plugin.builtin.action.mixins import ActionMixin | ||||||
|  | import plugin.integration | ||||||
|  |  | ||||||
|  |  | ||||||
| logger = logging.getLogger("inventree") | 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 | ||||||
|     """ |     """ | ||||||
|  |     # TODO @matmair remove this with InvenTree 0.7.0 | ||||||
|     ACTION_NAME = "" |     def __init__(self, user=None, data=None): | ||||||
|  |         warnings.warn("using the ActionPlugin is depreceated", DeprecationWarning) | ||||||
|     @classmethod |         super().__init__() | ||||||
|     def action_name(cls): |         self.init(user, data) | ||||||
|         """ |  | ||||||
|         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(), |  | ||||||
|         } |  | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ from __future__ import unicode_literals | |||||||
| from django.contrib import admin | from django.contrib import admin | ||||||
|  |  | ||||||
| import plugin.models as models | import plugin.models as models | ||||||
| import plugin.registry as registry | import plugin.registry as pl_registry | ||||||
|  |  | ||||||
|  |  | ||||||
| def plugin_update(queryset, new_status: bool): | def plugin_update(queryset, new_status: bool): | ||||||
| @@ -23,7 +23,7 @@ def plugin_update(queryset, new_status: bool): | |||||||
|  |  | ||||||
|     # Reload plugins if they changed |     # Reload plugins if they changed | ||||||
|     if apps_changed: |     if apps_changed: | ||||||
|         registry.plugin_registry.reload_plugins() |         pl_registry.reload_plugins() | ||||||
|  |  | ||||||
|  |  | ||||||
| @admin.action(description='Activate plugin(s)') | @admin.action(description='Activate plugin(s)') | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ from django.conf import settings | |||||||
|  |  | ||||||
| from maintenance_mode.core import set_maintenance_mode | from maintenance_mode.core import set_maintenance_mode | ||||||
|  |  | ||||||
| from plugin import plugin_registry | from plugin import registry | ||||||
|  |  | ||||||
|  |  | ||||||
| logger = logging.getLogger('inventree') | logger = logging.getLogger('inventree') | ||||||
| @@ -18,14 +18,13 @@ class PluginAppConfig(AppConfig): | |||||||
|     name = 'plugin' |     name = 'plugin' | ||||||
|  |  | ||||||
|     def ready(self): |     def ready(self): | ||||||
|  |  | ||||||
|         if settings.PLUGINS_ENABLED: |         if settings.PLUGINS_ENABLED: | ||||||
|             logger.info('Loading InvenTree plugins') |             logger.info('Loading InvenTree plugins') | ||||||
|  |  | ||||||
|             if not plugin_registry.is_loading: |             if not registry.is_loading: | ||||||
|                 # this is the first startup |                 # this is the first startup | ||||||
|                 plugin_registry.collect_plugins() |                 registry.collect_plugins() | ||||||
|                 plugin_registry.load_plugins() |                 registry.load_plugins() | ||||||
|  |  | ||||||
|                 # drop out of maintenance |                 # drop out of maintenance | ||||||
|                 # makes sure we did not have an error in reloading and maintenance is still active |                 # makes sure we did not have an error in reloading and maintenance is still active | ||||||
|   | |||||||
							
								
								
									
										68
									
								
								InvenTree/plugin/builtin/action/mixins.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								InvenTree/plugin/builtin/action/mixins.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,68 @@ | |||||||
|  | """ | ||||||
|  | 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', True, __class__) | ||||||
|  |  | ||||||
|  |     def action_name(self): | ||||||
|  |         """ | ||||||
|  |         Action name for this plugin. | ||||||
|  |  | ||||||
|  |         If the ACTION_NAME parameter is empty, | ||||||
|  |         uses 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(), | ||||||
|  |         } | ||||||
							
								
								
									
										0
									
								
								InvenTree/plugin/builtin/barcode/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								InvenTree/plugin/builtin/barcode/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										146
									
								
								InvenTree/plugin/builtin/barcode/mixins.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										146
									
								
								InvenTree/plugin/builtin/barcode/mixins.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,146 @@ | |||||||
|  | """ | ||||||
|  | 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 | ||||||
| @@ -11,6 +11,7 @@ from django.db.utils import OperationalError, ProgrammingError | |||||||
|  |  | ||||||
| from plugin.models import PluginConfig, PluginSetting | from plugin.models import PluginConfig, PluginSetting | ||||||
| from plugin.urls import PLUGIN_BASE | from plugin.urls import PLUGIN_BASE | ||||||
|  | from plugin.helpers import MixinImplementationError, MixinNotImplementedError | ||||||
|  |  | ||||||
|  |  | ||||||
| logger = logging.getLogger('inventree') | logger = logging.getLogger('inventree') | ||||||
| @@ -86,6 +87,9 @@ class ScheduleMixin: | |||||||
|     SCHEDULED_TASKS = {} |     SCHEDULED_TASKS = {} | ||||||
|  |  | ||||||
|     class MixinMeta: |     class MixinMeta: | ||||||
|  |         """ | ||||||
|  |         Meta options for this mixin | ||||||
|  |         """ | ||||||
|         MIXIN_NAME = 'Schedule' |         MIXIN_NAME = 'Schedule' | ||||||
|  |  | ||||||
|     def __init__(self): |     def __init__(self): | ||||||
| @@ -97,6 +101,9 @@ class ScheduleMixin: | |||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def has_scheduled_tasks(self): |     def has_scheduled_tasks(self): | ||||||
|  |         """ | ||||||
|  |         Are tasks defined for this plugin | ||||||
|  |         """ | ||||||
|         return bool(self.scheduled_tasks) |         return bool(self.scheduled_tasks) | ||||||
|  |  | ||||||
|     def validate_scheduled_tasks(self): |     def validate_scheduled_tasks(self): | ||||||
| @@ -105,31 +112,37 @@ class ScheduleMixin: | |||||||
|         """ |         """ | ||||||
|  |  | ||||||
|         if not self.has_scheduled_tasks: |         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(): |         for key, task in self.scheduled_tasks.items(): | ||||||
|  |  | ||||||
|             if 'func' not in task: |             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: |             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() |             schedule = task['schedule'].upper().strip() | ||||||
|  |  | ||||||
|             if schedule not in self.ALLOWABLE_SCHEDULE_TYPES: |             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 'minutes' is selected, it must be provided! | ||||||
|             if schedule == 'I' and 'minutes' not in task: |             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): |     def get_task_name(self, key): | ||||||
|  |         """ | ||||||
|  |         Task name for key | ||||||
|  |         """ | ||||||
|         # Generate a 'unique' task name |         # Generate a 'unique' task name | ||||||
|         slug = self.plugin_slug() |         slug = self.plugin_slug() | ||||||
|         return f"plugin.{slug}.{key}" |         return f"plugin.{slug}.{key}" | ||||||
|  |  | ||||||
|     def get_task_names(self): |     def get_task_names(self): | ||||||
|  |         """ | ||||||
|  |         All defined task names | ||||||
|  |         """ | ||||||
|         # Returns a list of all task names associated with this plugin instance |         # 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()] |         return [self.get_task_name(key) for key in self.scheduled_tasks.keys()] | ||||||
|  |  | ||||||
| @@ -191,10 +204,17 @@ class EventMixin: | |||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     def process_event(self, event, *args, **kwargs): |     def process_event(self, event, *args, **kwargs): | ||||||
|  |         """ | ||||||
|  |         Function to handle events | ||||||
|  |         Must be overridden by plugin | ||||||
|  |         """ | ||||||
|         # Default implementation does not do anything |         # Default implementation does not do anything | ||||||
|         raise NotImplementedError |         raise MixinNotImplementedError | ||||||
|  |  | ||||||
|     class MixinMeta: |     class MixinMeta: | ||||||
|  |         """ | ||||||
|  |         Meta options for this mixin | ||||||
|  |         """ | ||||||
|         MIXIN_NAME = 'Events' |         MIXIN_NAME = 'Events' | ||||||
|  |  | ||||||
|     def __init__(self): |     def __init__(self): | ||||||
| @@ -208,6 +228,9 @@ class UrlsMixin: | |||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     class MixinMeta: |     class MixinMeta: | ||||||
|  |         """ | ||||||
|  |         Meta options for this mixin | ||||||
|  |         """ | ||||||
|         MIXIN_NAME = 'URLs' |         MIXIN_NAME = 'URLs' | ||||||
|  |  | ||||||
|     def __init__(self): |     def __init__(self): | ||||||
| @@ -217,28 +240,28 @@ class UrlsMixin: | |||||||
|  |  | ||||||
|     def setup_urls(self): |     def setup_urls(self): | ||||||
|         """ |         """ | ||||||
|         setup url endpoints for this plugin |         Setup url endpoints for this plugin | ||||||
|         """ |         """ | ||||||
|         return getattr(self, 'URLS', None) |         return getattr(self, 'URLS', None) | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def base_url(self): |     def base_url(self): | ||||||
|         """ |         """ | ||||||
|         returns base url for this plugin |         Base url for this plugin | ||||||
|         """ |         """ | ||||||
|         return f'{PLUGIN_BASE}/{self.slug}/' |         return f'{PLUGIN_BASE}/{self.slug}/' | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def internal_name(self): |     def internal_name(self): | ||||||
|         """ |         """ | ||||||
|         returns the internal url pattern name |         Internal url pattern name | ||||||
|         """ |         """ | ||||||
|         return f'plugin:{self.slug}:' |         return f'plugin:{self.slug}:' | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def urlpatterns(self): |     def urlpatterns(self): | ||||||
|         """ |         """ | ||||||
|         returns the urlpatterns for this plugin |         Urlpatterns for this plugin | ||||||
|         """ |         """ | ||||||
|         if self.has_urls: |         if self.has_urls: | ||||||
|             return url(f'^{self.slug}/', include((self.urls, self.slug)), name=self.slug) |             return url(f'^{self.slug}/', include((self.urls, self.slug)), name=self.slug) | ||||||
| @@ -247,7 +270,7 @@ class UrlsMixin: | |||||||
|     @property |     @property | ||||||
|     def has_urls(self): |     def has_urls(self): | ||||||
|         """ |         """ | ||||||
|         does this plugin use custom urls |         Does this plugin use custom urls | ||||||
|         """ |         """ | ||||||
|         return bool(self.urls) |         return bool(self.urls) | ||||||
|  |  | ||||||
| @@ -262,7 +285,7 @@ class NavigationMixin: | |||||||
|  |  | ||||||
|     class MixinMeta: |     class MixinMeta: | ||||||
|         """ |         """ | ||||||
|         meta options for this mixin |         Meta options for this mixin | ||||||
|         """ |         """ | ||||||
|         MIXIN_NAME = 'Navigation Links' |         MIXIN_NAME = 'Navigation Links' | ||||||
|  |  | ||||||
| @@ -273,26 +296,28 @@ class NavigationMixin: | |||||||
|  |  | ||||||
|     def setup_navigation(self): |     def setup_navigation(self): | ||||||
|         """ |         """ | ||||||
|         setup navigation links for this plugin |         Setup navigation links for this plugin | ||||||
|         """ |         """ | ||||||
|         nav_links = getattr(self, 'NAVIGATION', None) |         nav_links = getattr(self, 'NAVIGATION', None) | ||||||
|         if nav_links: |         if nav_links: | ||||||
|             # check if needed values are configured |             # check if needed values are configured | ||||||
|             for link in nav_links: |             for link in nav_links: | ||||||
|                 if False in [a in link for a in ('link', 'name', )]: |                 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 |         return nav_links | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def has_naviation(self): |     def has_naviation(self): | ||||||
|         """ |         """ | ||||||
|         does this plugin define navigation elements |         Does this plugin define navigation elements | ||||||
|         """ |         """ | ||||||
|         return bool(self.navigation) |         return bool(self.navigation) | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def navigation_name(self): |     def navigation_name(self): | ||||||
|         """name for navigation tab""" |         """ | ||||||
|  |         Name for navigation tab | ||||||
|  |         """ | ||||||
|         name = getattr(self, 'NAVIGATION_TAB_NAME', None) |         name = getattr(self, 'NAVIGATION_TAB_NAME', None) | ||||||
|         if not name: |         if not name: | ||||||
|             name = self.human_name |             name = self.human_name | ||||||
| @@ -300,7 +325,9 @@ class NavigationMixin: | |||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def navigation_icon(self): |     def navigation_icon(self): | ||||||
|         """icon for navigation tab""" |         """ | ||||||
|  |         Icon-name for navigation tab | ||||||
|  |         """ | ||||||
|         return getattr(self, 'NAVIGATION_TAB_ICON', "fas fa-question") |         return getattr(self, 'NAVIGATION_TAB_ICON', "fas fa-question") | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -310,7 +337,9 @@ class AppMixin: | |||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     class MixinMeta: |     class MixinMeta: | ||||||
|         """meta options for this mixin""" |         """m | ||||||
|  |         Mta options for this mixin | ||||||
|  |         """ | ||||||
|         MIXIN_NAME = 'App registration' |         MIXIN_NAME = 'App registration' | ||||||
|  |  | ||||||
|     def __init__(self): |     def __init__(self): | ||||||
| @@ -320,7 +349,7 @@ class AppMixin: | |||||||
|     @property |     @property | ||||||
|     def has_app(self): |     def has_app(self): | ||||||
|         """ |         """ | ||||||
|         this plugin is always an app with this plugin |         This plugin is always an app with this plugin | ||||||
|         """ |         """ | ||||||
|         return True |         return True | ||||||
|  |  | ||||||
|   | |||||||
| @@ -17,7 +17,7 @@ from common.models import InvenTreeSetting | |||||||
| from InvenTree.ready import canAppAccessDatabase | from InvenTree.ready import canAppAccessDatabase | ||||||
| from InvenTree.tasks import offload_task | from InvenTree.tasks import offload_task | ||||||
|  |  | ||||||
| from plugin.registry import plugin_registry | from plugin.registry import registry | ||||||
|  |  | ||||||
|  |  | ||||||
| logger = logging.getLogger('inventree') | logger = logging.getLogger('inventree') | ||||||
| @@ -64,7 +64,7 @@ def register_event(event, *args, **kwargs): | |||||||
|  |  | ||||||
|         with transaction.atomic(): |         with transaction.atomic(): | ||||||
|  |  | ||||||
|             for slug, plugin in plugin_registry.plugins.items(): |             for slug, plugin in registry.plugins.items(): | ||||||
|  |  | ||||||
|                 if plugin.mixin_enabled('events'): |                 if plugin.mixin_enabled('events'): | ||||||
|  |  | ||||||
| @@ -95,7 +95,7 @@ def process_event(plugin_slug, event, *args, **kwargs): | |||||||
|  |  | ||||||
|     logger.info(f"Plugin '{plugin_slug}' is processing triggered event '{event}'") |     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) |     plugin.process_event(event, *args, **kwargs) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,26 +1,23 @@ | |||||||
| """Helpers for plugin app""" | """ | ||||||
|  | Helpers for plugin app | ||||||
|  | """ | ||||||
| import os | import os | ||||||
| import subprocess | import subprocess | ||||||
| import pathlib | import pathlib | ||||||
| import sysconfig | import sysconfig | ||||||
| import traceback | import traceback | ||||||
|  | import inspect | ||||||
|  | import pkgutil | ||||||
|  |  | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
|  | from django.core.exceptions import AppRegistryNotReady | ||||||
|  |  | ||||||
|  |  | ||||||
| # region logging / errors | # region logging / errors | ||||||
| def log_plugin_error(error, reference: str = 'general'): |  | ||||||
|     from plugin import plugin_registry |  | ||||||
|  |  | ||||||
|     # make sure the registry is set up |  | ||||||
|     if reference not in plugin_registry.errors: |  | ||||||
|         plugin_registry.errors[reference] = [] |  | ||||||
|  |  | ||||||
|     # add error to stack |  | ||||||
|     plugin_registry.errors[reference].append(error) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class IntegrationPluginError(Exception): | class IntegrationPluginError(Exception): | ||||||
|  |     """ | ||||||
|  |     Error that encapsulates another error and adds the path / reference of the raising plugin | ||||||
|  |     """ | ||||||
|     def __init__(self, path, message): |     def __init__(self, path, message): | ||||||
|         self.path = path |         self.path = path | ||||||
|         self.message = message |         self.message = message | ||||||
| @@ -29,7 +26,39 @@ class IntegrationPluginError(Exception): | |||||||
|         return self.message |         return self.message | ||||||
|  |  | ||||||
|  |  | ||||||
| def get_plugin_error(error, do_raise: bool = False, do_log: bool = False, log_name: str = ''): | 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 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 | ||||||
|  |     """ | ||||||
|     package_path = traceback.extract_tb(error.__traceback__)[-1].filename |     package_path = traceback.extract_tb(error.__traceback__)[-1].filename | ||||||
|     install_path = sysconfig.get_paths()["purelib"] |     install_path = sysconfig.get_paths()["purelib"] | ||||||
|     try: |     try: | ||||||
| @@ -53,18 +82,23 @@ def get_plugin_error(error, do_raise: bool = False, do_log: bool = False, log_na | |||||||
|         log_kwargs = {} |         log_kwargs = {} | ||||||
|         if log_name: |         if log_name: | ||||||
|             log_kwargs['reference'] = 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)) | ||||||
|  |  | ||||||
|     if do_raise: |     if do_raise: | ||||||
|         raise IntegrationPluginError(package_name, str(error)) |         raise IntegrationPluginError(package_name, str(error)) | ||||||
|  |  | ||||||
|     return package_name, str(error) |     if do_return: | ||||||
|  |         return new_error | ||||||
| # endregion | # endregion | ||||||
|  |  | ||||||
|  |  | ||||||
| # region git-helpers | # region git-helpers | ||||||
| def get_git_log(path): | 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:] |     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] |     command = ['git', 'log', '-n', '1', "--pretty=format:'%H%n%aN%n%aE%n%aI%n%f%n%G?%n%GK'", '--follow', '--', path] | ||||||
|     try: |     try: | ||||||
| @@ -79,9 +113,13 @@ def get_git_log(path): | |||||||
|  |  | ||||||
|  |  | ||||||
| class GitStatus: | class GitStatus: | ||||||
|     """class for resolving git gpg singing state""" |     """ | ||||||
|  |     Class for resolving git gpg singing state | ||||||
|  |     """ | ||||||
|     class Definition: |     class Definition: | ||||||
|         """definition of a git gpg sing state""" |         """ | ||||||
|  |         Definition of a git gpg sing state | ||||||
|  |         """ | ||||||
|         key: str = 'N' |         key: str = 'N' | ||||||
|         status: int = 2 |         status: int = 2 | ||||||
|         msg: str = '' |         msg: str = '' | ||||||
| @@ -100,3 +138,56 @@ class GitStatus: | |||||||
|     R = Definition(key='R', status=2, msg='good signature, revoked key',) |     R = Definition(key='R', status=2, msg='good signature, revoked key',) | ||||||
|     E = Definition(key='E', status=1, msg='cannot be checked',) |     E = Definition(key='E', status=1, msg='cannot be checked',) | ||||||
| # endregion | # 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 | ||||||
|   | |||||||
| @@ -1,5 +1,7 @@ | |||||||
| # -*- coding: utf-8 -*- | # -*- coding: utf-8 -*- | ||||||
| """class for IntegrationPluginBase and Mixins for it""" | """ | ||||||
|  | Class for IntegrationPluginBase and Mixin Base | ||||||
|  | """ | ||||||
|  |  | ||||||
| import logging | import logging | ||||||
| import os | import os | ||||||
| @@ -11,7 +13,7 @@ from django.urls.base import reverse | |||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.utils.translation import ugettext_lazy as _ | 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 | from plugin.helpers import get_git_log, GitStatus | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -20,7 +22,7 @@ logger = logging.getLogger("inventree") | |||||||
|  |  | ||||||
| class MixinBase: | class MixinBase: | ||||||
|     """ |     """ | ||||||
|     General base for mixins |     Base set of mixin functions and mechanisms | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     def __init__(self) -> None: |     def __init__(self) -> None: | ||||||
| @@ -65,7 +67,7 @@ class MixinBase: | |||||||
|         return mixins |         return mixins | ||||||
|  |  | ||||||
|  |  | ||||||
| class IntegrationPluginBase(MixinBase, plugin.InvenTreePlugin): | class IntegrationPluginBase(MixinBase, plugin_base.InvenTreePluginBase): | ||||||
|     """ |     """ | ||||||
|     The IntegrationPluginBase class is used to integrate with 3rd party software |     The IntegrationPluginBase class is used to integrate with 3rd party software | ||||||
|     """ |     """ | ||||||
| @@ -83,32 +85,42 @@ class IntegrationPluginBase(MixinBase, plugin.InvenTreePlugin): | |||||||
|         self.def_path = inspect.getfile(self.__class__) |         self.def_path = inspect.getfile(self.__class__) | ||||||
|         self.path = os.path.dirname(self.def_path) |         self.path = os.path.dirname(self.def_path) | ||||||
|  |  | ||||||
|         self.set_package() |         self.define_package() | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def _is_package(self): |     def _is_package(self): | ||||||
|  |         """ | ||||||
|  |         Is the plugin delivered as a package | ||||||
|  |         """ | ||||||
|         return getattr(self, 'is_package', False) |         return getattr(self, 'is_package', False) | ||||||
|  |  | ||||||
|     # region properties |     # region properties | ||||||
|     @property |     @property | ||||||
|     def slug(self): |     def slug(self): | ||||||
|  |         """ | ||||||
|  |         Slug of plugin | ||||||
|  |         """ | ||||||
|         return self.plugin_slug() |         return self.plugin_slug() | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def name(self): |     def name(self): | ||||||
|  |         """ | ||||||
|  |         Name of plugin | ||||||
|  |         """ | ||||||
|         return self.plugin_name() |         return self.plugin_name() | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def human_name(self): |     def human_name(self): | ||||||
|         """human readable name for labels etc.""" |         """ | ||||||
|         human_name = getattr(self, 'PLUGIN_TITLE', None) |         Human readable name of plugin | ||||||
|         if not human_name: |         """ | ||||||
|             human_name = self.plugin_name() |         return self.plugin_title() | ||||||
|         return human_name |  | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def description(self): |     def description(self): | ||||||
|         """description of plugin""" |         """ | ||||||
|  |         Description of plugin | ||||||
|  |         """ | ||||||
|         description = getattr(self, 'DESCRIPTION', None) |         description = getattr(self, 'DESCRIPTION', None) | ||||||
|         if not description: |         if not description: | ||||||
|             description = self.plugin_name() |             description = self.plugin_name() | ||||||
| @@ -116,7 +128,9 @@ class IntegrationPluginBase(MixinBase, plugin.InvenTreePlugin): | |||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def author(self): |     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) |         author = getattr(self, 'AUTHOR', None) | ||||||
|         if not author: |         if not author: | ||||||
|             author = self.package.get('author') |             author = self.package.get('author') | ||||||
| @@ -126,7 +140,9 @@ class IntegrationPluginBase(MixinBase, plugin.InvenTreePlugin): | |||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def pub_date(self): |     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) |         pub_date = getattr(self, 'PUBLISH_DATE', None) | ||||||
|         if not pub_date: |         if not pub_date: | ||||||
|             pub_date = self.package.get('date') |             pub_date = self.package.get('date') | ||||||
| @@ -138,42 +154,56 @@ class IntegrationPluginBase(MixinBase, plugin.InvenTreePlugin): | |||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def version(self): |     def version(self): | ||||||
|         """returns version of plugin""" |         """ | ||||||
|  |         Version of plugin | ||||||
|  |         """ | ||||||
|         version = getattr(self, 'VERSION', None) |         version = getattr(self, 'VERSION', None) | ||||||
|         return version |         return version | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def website(self): |     def website(self): | ||||||
|         """returns website of plugin""" |         """ | ||||||
|  |         Website of plugin - if set else None | ||||||
|  |         """ | ||||||
|         website = getattr(self, 'WEBSITE', None) |         website = getattr(self, 'WEBSITE', None) | ||||||
|         return website |         return website | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def license(self): |     def license(self): | ||||||
|         """returns license of plugin""" |         """ | ||||||
|  |         License of plugin | ||||||
|  |         """ | ||||||
|         license = getattr(self, 'LICENSE', None) |         license = getattr(self, 'LICENSE', None) | ||||||
|         return license |         return license | ||||||
|     # endregion |     # endregion | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def package_path(self): |     def package_path(self): | ||||||
|         """returns the path to the plugin""" |         """ | ||||||
|  |         Path to the plugin | ||||||
|  |         """ | ||||||
|         if self._is_package: |         if self._is_package: | ||||||
|             return self.__module__ |             return self.__module__ | ||||||
|         return pathlib.Path(self.def_path).relative_to(settings.BASE_DIR) |         return pathlib.Path(self.def_path).relative_to(settings.BASE_DIR) | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def settings_url(self): |     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}' |         return f'{reverse("settings")}#select-plugin-{self.slug}' | ||||||
|  |  | ||||||
|     # region mixins |     # region mixins | ||||||
|     def mixin(self, key): |     def mixin(self, key): | ||||||
|         """check if mixin is registered""" |         """ | ||||||
|  |         Check if mixin is registered | ||||||
|  |         """ | ||||||
|         return key in self._mixins |         return key in self._mixins | ||||||
|  |  | ||||||
|     def mixin_enabled(self, key): |     def mixin_enabled(self, key): | ||||||
|         """check if mixin is enabled and ready""" |         """ | ||||||
|  |         Check if mixin is registered, enabled and ready | ||||||
|  |         """ | ||||||
|         if self.mixin(key): |         if self.mixin(key): | ||||||
|             fnc_name = self._mixins.get(key) |             fnc_name = self._mixins.get(key) | ||||||
|  |  | ||||||
| @@ -186,17 +216,23 @@ class IntegrationPluginBase(MixinBase, plugin.InvenTreePlugin): | |||||||
|     # endregion |     # endregion | ||||||
|  |  | ||||||
|     # region package info |     # region package info | ||||||
|     def get_package_commit(self): |     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) |         return get_git_log(self.def_path) | ||||||
|  |  | ||||||
|     def get_package_metadata(self): |     def _get_package_metadata(self): | ||||||
|         """get package metadata for plugin""" |         """ | ||||||
|  |         Get package metadata for plugin | ||||||
|  |         """ | ||||||
|         return {} |         return {} | ||||||
|  |  | ||||||
|     def set_package(self): |     def define_package(self): | ||||||
|         """add packaging info of the plugins into plugins context""" |         """ | ||||||
|         package = self.get_package_metadata() if self._is_package else self.get_package_commit() |         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 |         # process date | ||||||
|         if package.get('date'): |         if package.get('date'): | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ load templates for loaded plugins | |||||||
| from django.template.loaders.filesystem import Loader as FilesystemLoader | from django.template.loaders.filesystem import Loader as FilesystemLoader | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
|  |  | ||||||
| from plugin import plugin_registry | from plugin import registry | ||||||
|  |  | ||||||
|  |  | ||||||
| class PluginTemplateLoader(FilesystemLoader): | class PluginTemplateLoader(FilesystemLoader): | ||||||
| @@ -12,7 +12,7 @@ class PluginTemplateLoader(FilesystemLoader): | |||||||
|     def get_dirs(self): |     def get_dirs(self): | ||||||
|         dirname = 'templates' |         dirname = 'templates' | ||||||
|         template_dirs = [] |         template_dirs = [] | ||||||
|         for plugin in plugin_registry.plugins.values(): |         for plugin in registry.plugins.values(): | ||||||
|             new_path = Path(plugin.path) / dirname |             new_path = Path(plugin.path) / dirname | ||||||
|             if Path(new_path).is_dir(): |             if Path(new_path).is_dir(): | ||||||
|                 template_dirs.append(new_path) |                 template_dirs.append(new_path) | ||||||
|   | |||||||
| @@ -3,6 +3,8 @@ Utility class to enable simpler imports | |||||||
| """ | """ | ||||||
|  |  | ||||||
| from ..builtin.integration.mixins import APICallMixin, AppMixin, SettingsMixin, EventMixin, ScheduleMixin, UrlsMixin, NavigationMixin | from ..builtin.integration.mixins import APICallMixin, AppMixin, SettingsMixin, EventMixin, ScheduleMixin, UrlsMixin, NavigationMixin | ||||||
|  | from ..builtin.action.mixins import ActionMixin | ||||||
|  | from ..builtin.barcode.mixins import BarcodeMixin | ||||||
|  |  | ||||||
| __all__ = [ | __all__ = [ | ||||||
|     'APICallMixin', |     'APICallMixin', | ||||||
| @@ -12,4 +14,6 @@ __all__ = [ | |||||||
|     'ScheduleMixin', |     'ScheduleMixin', | ||||||
|     'SettingsMixin', |     'SettingsMixin', | ||||||
|     'UrlsMixin', |     'UrlsMixin', | ||||||
|  |     'ActionMixin', | ||||||
|  |     'BarcodeMixin', | ||||||
| ] | ] | ||||||
|   | |||||||
| @@ -10,7 +10,7 @@ from django.db import models | |||||||
|  |  | ||||||
| import common.models | import common.models | ||||||
|  |  | ||||||
| from plugin import InvenTreePlugin, plugin_registry | from plugin import InvenTreePluginBase, registry | ||||||
|  |  | ||||||
|  |  | ||||||
| class PluginConfig(models.Model): | class PluginConfig(models.Model): | ||||||
| @@ -72,7 +72,7 @@ class PluginConfig(models.Model): | |||||||
|         self.__org_active = self.active |         self.__org_active = self.active | ||||||
|  |  | ||||||
|         # append settings from registry |         # 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): |         def get_plugin_meta(name): | ||||||
|             if self.plugin: |             if self.plugin: | ||||||
| @@ -95,10 +95,10 @@ class PluginConfig(models.Model): | |||||||
|  |  | ||||||
|         if not reload: |         if not reload: | ||||||
|             if self.active is False and self.__org_active is True: |             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: |             elif self.active is True and self.__org_active is False: | ||||||
|                 plugin_registry.reload_plugins() |                 registry.reload_plugins() | ||||||
|  |  | ||||||
|         return ret |         return ret | ||||||
|  |  | ||||||
| @@ -164,10 +164,10 @@ class PluginSetting(common.models.BaseInvenTreeSetting): | |||||||
|  |  | ||||||
|             if plugin: |             if plugin: | ||||||
|  |  | ||||||
|                 if issubclass(plugin.__class__, InvenTreePlugin): |                 if issubclass(plugin.__class__, InvenTreePluginBase): | ||||||
|                     plugin = plugin.plugin_config() |                     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) |         return super().get_setting_definition(key, **kwargs) | ||||||
|  |  | ||||||
| @@ -182,7 +182,7 @@ class PluginSetting(common.models.BaseInvenTreeSetting): | |||||||
|         plugin = kwargs.get('plugin', None) |         plugin = kwargs.get('plugin', None) | ||||||
|  |  | ||||||
|         if plugin: |         if plugin: | ||||||
|             if issubclass(plugin.__class__, InvenTreePlugin): |             if issubclass(plugin.__class__, InvenTreePluginBase): | ||||||
|                 plugin = plugin.plugin_config() |                 plugin = plugin.plugin_config() | ||||||
|             filters['plugin'] = plugin |             filters['plugin'] = plugin | ||||||
|  |  | ||||||
|   | |||||||
| @@ -2,14 +2,16 @@ | |||||||
| """ | """ | ||||||
| Base Class for InvenTree plugins | Base Class for InvenTree plugins | ||||||
| """ | """ | ||||||
|  | import warnings | ||||||
|  |  | ||||||
| from django.db.utils import OperationalError, ProgrammingError | from django.db.utils import OperationalError, ProgrammingError | ||||||
| from django.utils.text import slugify | from django.utils.text import slugify | ||||||
|  |  | ||||||
|  |  | ||||||
| class InvenTreePlugin(): | class InvenTreePluginBase(): | ||||||
|     """ |     """ | ||||||
|     Base class for a plugin |     Base class for a plugin | ||||||
|  |     DO NOT USE THIS DIRECTLY, USE plugin.IntegrationPluginBase | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     def __init__(self): |     def __init__(self): | ||||||
| @@ -24,11 +26,15 @@ class InvenTreePlugin(): | |||||||
|  |  | ||||||
|     def plugin_name(self): |     def plugin_name(self): | ||||||
|         """ |         """ | ||||||
|         Return the name of this plugin plugin |         Name of plugin | ||||||
|         """ |         """ | ||||||
|         return self.PLUGIN_NAME |         return self.PLUGIN_NAME | ||||||
|  |  | ||||||
|     def plugin_slug(self): |     def plugin_slug(self): | ||||||
|  |         """ | ||||||
|  |         Slug of plugin | ||||||
|  |         If not set plugin name slugified | ||||||
|  |         """ | ||||||
|  |  | ||||||
|         slug = getattr(self, 'PLUGIN_SLUG', None) |         slug = getattr(self, 'PLUGIN_SLUG', None) | ||||||
|  |  | ||||||
| @@ -38,6 +44,9 @@ class InvenTreePlugin(): | |||||||
|         return slugify(slug.lower()) |         return slugify(slug.lower()) | ||||||
|  |  | ||||||
|     def plugin_title(self): |     def plugin_title(self): | ||||||
|  |         """ | ||||||
|  |         Title of plugin | ||||||
|  |         """ | ||||||
|  |  | ||||||
|         if self.PLUGIN_TITLE: |         if self.PLUGIN_TITLE: | ||||||
|             return self.PLUGIN_TITLE |             return self.PLUGIN_TITLE | ||||||
| @@ -75,3 +84,13 @@ class InvenTreePlugin(): | |||||||
|             return cfg.active |             return cfg.active | ||||||
|         else: |         else: | ||||||
|             return False |             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__() | ||||||
|   | |||||||
| @@ -1,114 +0,0 @@ | |||||||
| # -*- coding: utf-8 -*- |  | ||||||
| """general functions for plugin handeling""" |  | ||||||
|  |  | ||||||
| import inspect |  | ||||||
| import importlib |  | ||||||
| import pkgutil |  | ||||||
| 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") |  | ||||||
|  |  | ||||||
|  |  | ||||||
| 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): |  | ||||||
|     """get all modules in a package""" |  | ||||||
|     from plugin.helpers import log_plugin_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: |  | ||||||
|             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_plugin_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, recursive: bool = False): |  | ||||||
|     """ |  | ||||||
|     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, recursive) |  | ||||||
|  |  | ||||||
|     # 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 |  | ||||||
|  |  | ||||||
|  |  | ||||||
| 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_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 |  | ||||||
|     """ |  | ||||||
|     from barcodes import plugins as BarcodePlugins |  | ||||||
|     from barcodes.barcode import BarcodePlugin |  | ||||||
|  |  | ||||||
|     return load_plugins('barcode', BarcodePlugin, BarcodePlugins) |  | ||||||
| @@ -28,9 +28,8 @@ except: | |||||||
| from maintenance_mode.core import maintenance_mode_on | from maintenance_mode.core import maintenance_mode_on | ||||||
| from maintenance_mode.core import get_maintenance_mode, set_maintenance_mode | from maintenance_mode.core import get_maintenance_mode, set_maintenance_mode | ||||||
|  |  | ||||||
| from plugin import plugins as inventree_plugins |  | ||||||
| from .integration import IntegrationPluginBase | from .integration import IntegrationPluginBase | ||||||
| from .helpers import get_plugin_error, IntegrationPluginError | from .helpers import handle_error, log_error, get_plugins, IntegrationPluginError | ||||||
|  |  | ||||||
|  |  | ||||||
| logger = logging.getLogger('inventree') | logger = logging.getLogger('inventree') | ||||||
| @@ -60,18 +59,16 @@ class PluginsRegistry: | |||||||
|         # mixins |         # mixins | ||||||
|         self.mixins_settings = {} |         self.mixins_settings = {} | ||||||
|  |  | ||||||
|     # region public plugin functions |     # region public functions | ||||||
|  |     # region loading / unloading | ||||||
|     def load_plugins(self): |     def load_plugins(self): | ||||||
|         """ |         """ | ||||||
|         Load and activate all IntegrationPlugins |         Load and activate all IntegrationPlugins | ||||||
|         """ |         """ | ||||||
|  |  | ||||||
|         if not settings.PLUGINS_ENABLED: |         if not settings.PLUGINS_ENABLED: | ||||||
|             # Plugins not enabled, do nothing |             # Plugins not enabled, do nothing | ||||||
|             return |             return | ||||||
|  |  | ||||||
|         from plugin.helpers import log_plugin_error |  | ||||||
|  |  | ||||||
|         logger.info('Start loading plugins') |         logger.info('Start loading plugins') | ||||||
|  |  | ||||||
|         # Set maintanace mode |         # Set maintanace mode | ||||||
| @@ -95,7 +92,7 @@ class PluginsRegistry: | |||||||
|                 break |                 break | ||||||
|             except IntegrationPluginError as error: |             except IntegrationPluginError as error: | ||||||
|                 logger.error(f'[PLUGIN] Encountered an error with {error.path}:\n{error.message}') |                 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 |                 blocked_plugin = error.path  # we will not try to load this app again | ||||||
|  |  | ||||||
|                 # Initialize apps without any integration plugins |                 # Initialize apps without any integration plugins | ||||||
| @@ -179,7 +176,7 @@ class PluginsRegistry: | |||||||
|  |  | ||||||
|         # Collect plugins from paths |         # Collect plugins from paths | ||||||
|         for plugin in settings.PLUGIN_DIRS: |         for plugin in settings.PLUGIN_DIRS: | ||||||
|             modules = inventree_plugins.get_plugins(importlib.import_module(plugin), IntegrationPluginBase, True) |             modules = get_plugins(importlib.import_module(plugin), IntegrationPluginBase) | ||||||
|             if modules: |             if modules: | ||||||
|                 [self.plugin_modules.append(item) for item in modules] |                 [self.plugin_modules.append(item) for item in modules] | ||||||
|  |  | ||||||
| @@ -192,12 +189,29 @@ class PluginsRegistry: | |||||||
|                     plugin.is_package = True |                     plugin.is_package = True | ||||||
|                     self.plugin_modules.append(plugin) |                     self.plugin_modules.append(plugin) | ||||||
|                 except Exception as error: |                 except Exception as error: | ||||||
|                     get_plugin_error(error, do_log=True, log_name='discovery') |                     handle_error(error, do_raise=False, log_name='discovery') | ||||||
|  |  | ||||||
|         # Log collected plugins |         # Log collected plugins | ||||||
|         logger.info(f'Collected {len(self.plugin_modules)} plugins!') |         logger.info(f'Collected {len(self.plugin_modules)} plugins!') | ||||||
|         logger.info(", ".join([a.__module__ for a in self.plugin_modules])) |         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 | ||||||
|  |         """ | ||||||
|  |         result = [] | ||||||
|  |  | ||||||
|  |         for plugin in self.plugins.values(): | ||||||
|  |             if plugin.mixin_enabled(mixin): | ||||||
|  |                 result.append(plugin) | ||||||
|  |  | ||||||
|  |         return result | ||||||
|  |     # endregion | ||||||
|  |     # endregion | ||||||
|  |  | ||||||
|  |     # region general internal loading /activating / deactivating / deloading | ||||||
|     def _init_plugins(self, disabled=None): |     def _init_plugins(self, disabled=None): | ||||||
|         """ |         """ | ||||||
|         Initialise all found plugins |         Initialise all found plugins | ||||||
| @@ -254,7 +268,7 @@ class PluginsRegistry: | |||||||
|                     plugin = plugin() |                     plugin = plugin() | ||||||
|                 except Exception as error: |                 except Exception as error: | ||||||
|                     # log error and raise it -> disable plugin |                     # log error and raise it -> disable plugin | ||||||
|                     get_plugin_error(error, do_raise=True, do_log=True, log_name='init') |                     handle_error(error, log_name='init') | ||||||
|  |  | ||||||
|                 logger.info(f'Loaded integration plugin {plugin.slug}') |                 logger.info(f'Loaded integration plugin {plugin.slug}') | ||||||
|                 plugin.is_package = was_packaged |                 plugin.is_package = was_packaged | ||||||
| @@ -290,7 +304,9 @@ class PluginsRegistry: | |||||||
|         self.deactivate_integration_app() |         self.deactivate_integration_app() | ||||||
|         self.deactivate_integration_schedule() |         self.deactivate_integration_schedule() | ||||||
|         self.deactivate_integration_settings() |         self.deactivate_integration_settings() | ||||||
|  |     # endregion | ||||||
|  |  | ||||||
|  |     # region mixin specific loading ... | ||||||
|     def activate_integration_settings(self, plugins): |     def activate_integration_settings(self, plugins): | ||||||
|  |  | ||||||
|         logger.info('Activating plugin settings') |         logger.info('Activating plugin settings') | ||||||
| @@ -536,7 +552,8 @@ class PluginsRegistry: | |||||||
|             cmd(*args, **kwargs) |             cmd(*args, **kwargs) | ||||||
|             return True, [] |             return True, [] | ||||||
|         except Exception as error: |         except Exception as error: | ||||||
|             get_plugin_error(error, do_raise=True) |             handle_error(error) | ||||||
|  |     # endregion | ||||||
|  |  | ||||||
|  |  | ||||||
| plugin_registry = PluginsRegistry() | registry = PluginsRegistry() | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ | |||||||
|  |  | ||||||
| from django.test import TestCase | from django.test import TestCase | ||||||
|  |  | ||||||
| from plugin import plugin_registry | from plugin import registry | ||||||
|  |  | ||||||
|  |  | ||||||
| class SampleApiCallerPluginTests(TestCase): | class SampleApiCallerPluginTests(TestCase): | ||||||
| @@ -11,8 +11,8 @@ class SampleApiCallerPluginTests(TestCase): | |||||||
|     def test_return(self): |     def test_return(self): | ||||||
|         """check if the external api call works""" |         """check if the external api call works""" | ||||||
|         # The plugin should be defined |         # The plugin should be defined | ||||||
|         self.assertIn('sample-api-caller', plugin_registry.plugins) |         self.assertIn('sample-api-caller', registry.plugins) | ||||||
|         plg = plugin_registry.plugins['sample-api-caller'] |         plg = registry.plugins['sample-api-caller'] | ||||||
|         self.assertTrue(plg) |         self.assertTrue(plg) | ||||||
|  |  | ||||||
|         # do an api call |         # do an api call | ||||||
|   | |||||||
| @@ -7,7 +7,7 @@ from django import template | |||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
|  |  | ||||||
| from common.models import InvenTreeSetting | from common.models import InvenTreeSetting | ||||||
| from plugin import plugin_registry | from plugin import registry | ||||||
|  |  | ||||||
|  |  | ||||||
| register = template.Library() | register = template.Library() | ||||||
| @@ -15,31 +15,41 @@ register = template.Library() | |||||||
|  |  | ||||||
| @register.simple_tag() | @register.simple_tag() | ||||||
| def plugin_list(*args, **kwargs): | def plugin_list(*args, **kwargs): | ||||||
|     """ Return a list of all installed integration plugins """ |     """ | ||||||
|     return plugin_registry.plugins |     List of all installed integration plugins | ||||||
|  |     """ | ||||||
|  |     return registry.plugins | ||||||
|  |  | ||||||
|  |  | ||||||
| @register.simple_tag() | @register.simple_tag() | ||||||
| def inactive_plugin_list(*args, **kwargs): | def inactive_plugin_list(*args, **kwargs): | ||||||
|     """ Return a list of all inactive integration plugins """ |     """ | ||||||
|     return plugin_registry.plugins_inactive |     List of all inactive integration plugins | ||||||
|  |     """ | ||||||
|  |     return registry.plugins_inactive | ||||||
|  |  | ||||||
|  |  | ||||||
| @register.simple_tag() | @register.simple_tag() | ||||||
| def plugin_settings(plugin, *args, **kwargs): | def plugin_settings(plugin, *args, **kwargs): | ||||||
|     """ Return a list of all custom settings for a plugin """ |     """ | ||||||
|     return plugin_registry.mixins_settings.get(plugin) |     List of all settings for the plugin | ||||||
|  |     """ | ||||||
|  |     return registry.mixins_settings.get(plugin) | ||||||
|  |  | ||||||
|  |  | ||||||
| @register.simple_tag() | @register.simple_tag() | ||||||
| def mixin_enabled(plugin, key, *args, **kwargs): | 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) |     return plugin.mixin_enabled(key) | ||||||
|  |  | ||||||
|  |  | ||||||
| @register.simple_tag() | @register.simple_tag() | ||||||
| def navigation_enabled(*args, **kwargs): | def navigation_enabled(*args, **kwargs): | ||||||
|     """Return if plugin navigation is enabled""" |     """ | ||||||
|  |     Is plugin navigation enabled? | ||||||
|  |     """ | ||||||
|     if djangosettings.PLUGIN_TESTING: |     if djangosettings.PLUGIN_TESTING: | ||||||
|         return True |         return True | ||||||
|     return InvenTreeSetting.get_setting('ENABLE_PLUGINS_NAVIGATION') |     return InvenTreeSetting.get_setting('ENABLE_PLUGINS_NAVIGATION') | ||||||
| @@ -47,7 +57,10 @@ def navigation_enabled(*args, **kwargs): | |||||||
|  |  | ||||||
| @register.simple_tag() | @register.simple_tag() | ||||||
| def safe_url(view_name, *args, **kwargs): | def safe_url(view_name, *args, **kwargs): | ||||||
|     """ safe lookup for urls """ |     """ | ||||||
|  |     Safe lookup fnc for URLs | ||||||
|  |     Returns None if not found | ||||||
|  |     """ | ||||||
|     try: |     try: | ||||||
|         return reverse(view_name, args=args, kwargs=kwargs) |         return reverse(view_name, args=args, kwargs=kwargs) | ||||||
|     except: |     except: | ||||||
| @@ -56,5 +69,7 @@ def safe_url(view_name, *args, **kwargs): | |||||||
|  |  | ||||||
| @register.simple_tag() | @register.simple_tag() | ||||||
| def plugin_errors(*args, **kwargs): | def plugin_errors(*args, **kwargs): | ||||||
|     """Return all plugin errors""" |     """ | ||||||
|     return plugin_registry.errors |     All plugin errors in the current session | ||||||
|  |     """ | ||||||
|  |     return registry.errors | ||||||
|   | |||||||
| @@ -64,14 +64,14 @@ class PluginDetailAPITest(InvenTreeAPITestCase): | |||||||
|         Test the PluginConfig action commands |         Test the PluginConfig action commands | ||||||
|         """ |         """ | ||||||
|         from plugin.models import PluginConfig |         from plugin.models import PluginConfig | ||||||
|         from plugin import plugin_registry |         from plugin import registry | ||||||
|  |  | ||||||
|         url = reverse('admin:plugin_pluginconfig_changelist') |         url = reverse('admin:plugin_pluginconfig_changelist') | ||||||
|         fixtures = PluginConfig.objects.all() |         fixtures = PluginConfig.objects.all() | ||||||
|  |  | ||||||
|         # check if plugins were registered -> in some test setups the startup has no db access |         # check if plugins were registered -> in some test setups the startup has no db access | ||||||
|         if not fixtures: |         if not fixtures: | ||||||
|             plugin_registry.reload_plugins() |             registry.reload_plugins() | ||||||
|             fixtures = PluginConfig.objects.all() |             fixtures = PluginConfig.objects.all() | ||||||
|  |  | ||||||
|         print([str(a) for a in fixtures]) |         print([str(a) for a in fixtures]) | ||||||
|   | |||||||
| @@ -4,20 +4,18 @@ Unit tests for plugins | |||||||
|  |  | ||||||
| from django.test import TestCase | from django.test import TestCase | ||||||
|  |  | ||||||
| import plugin.plugin |  | ||||||
| import plugin.integration |  | ||||||
| from plugin.samples.integration.sample import SampleIntegrationPlugin | from plugin.samples.integration.sample import SampleIntegrationPlugin | ||||||
| from plugin.samples.integration.another_sample import WrongIntegrationPlugin, NoIntegrationPlugin | from plugin.samples.integration.another_sample import WrongIntegrationPlugin, NoIntegrationPlugin | ||||||
| import plugin.templatetags.plugin_extras as plugin_tags | import plugin.templatetags.plugin_extras as plugin_tags | ||||||
| from plugin import plugin_registry | from plugin import registry, InvenTreePluginBase | ||||||
|  |  | ||||||
|  |  | ||||||
| class InvenTreePluginTests(TestCase): | class InvenTreePluginTests(TestCase): | ||||||
|     """ Tests for InvenTreePlugin """ |     """ Tests for InvenTreePlugin """ | ||||||
|     def setUp(self): |     def setUp(self): | ||||||
|         self.plugin = plugin.plugin.InvenTreePlugin() |         self.plugin = InvenTreePluginBase() | ||||||
|  |  | ||||||
|         class NamedPlugin(plugin.plugin.InvenTreePlugin): |         class NamedPlugin(InvenTreePluginBase): | ||||||
|             """a named plugin""" |             """a named plugin""" | ||||||
|             PLUGIN_NAME = 'abc123' |             PLUGIN_NAME = 'abc123' | ||||||
|  |  | ||||||
| @@ -34,20 +32,6 @@ class InvenTreePluginTests(TestCase): | |||||||
|         self.assertEqual(self.named_plugin.plugin_name(), 'abc123') |         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 |  | ||||||
|         # 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 |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class PluginTagTests(TestCase): | class PluginTagTests(TestCase): | ||||||
|     """ Tests for the plugin extras """ |     """ Tests for the plugin extras """ | ||||||
|  |  | ||||||
| @@ -58,17 +42,17 @@ class PluginTagTests(TestCase): | |||||||
|  |  | ||||||
|     def test_tag_plugin_list(self): |     def test_tag_plugin_list(self): | ||||||
|         """test that all plugins are listed""" |         """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): |     def test_tag_incative_plugin_list(self): | ||||||
|         """test that all inactive plugins are listed""" |         """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): |     def test_tag_plugin_settings(self): | ||||||
|         """check all plugins are listed""" |         """check all plugins are listed""" | ||||||
|         self.assertEqual( |         self.assertEqual( | ||||||
|             plugin_tags.plugin_settings(self.sample), |             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): |     def test_tag_mixin_enabled(self): | ||||||
| @@ -90,4 +74,4 @@ class PluginTagTests(TestCase): | |||||||
|  |  | ||||||
|     def test_tag_plugin_errors(self): |     def test_tag_plugin_errors(self): | ||||||
|         """test that all errors are listed""" |         """test that all errors are listed""" | ||||||
|         self.assertEqual(plugin_tags.plugin_errors(), plugin_registry.errors) |         self.assertEqual(plugin_tags.plugin_errors(), registry.errors) | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ URL lookup for plugin app | |||||||
|  |  | ||||||
| from django.conf.urls import url, include | from django.conf.urls import url, include | ||||||
|  |  | ||||||
| from plugin import plugin_registry | from plugin import registry | ||||||
|  |  | ||||||
|  |  | ||||||
| PLUGIN_BASE = 'plugin'  # Constant for links | PLUGIN_BASE = 'plugin'  # Constant for links | ||||||
| @@ -17,7 +17,7 @@ def get_plugin_urls(): | |||||||
|  |  | ||||||
|     urls = [] |     urls = [] | ||||||
|  |  | ||||||
|     for plugin in plugin_registry.plugins.values(): |     for plugin in registry.plugins.values(): | ||||||
|         if plugin.mixin_enabled('urls'): |         if plugin.mixin_enabled('urls'): | ||||||
|             urls.append(plugin.urlpatterns) |             urls.append(plugin.urlpatterns) | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user