diff --git a/InvenTree/InvenTree/api.py b/InvenTree/InvenTree/api.py index 8249a093aa..7c8f71ea9a 100644 --- a/InvenTree/InvenTree/api.py +++ b/InvenTree/InvenTree/api.py @@ -5,8 +5,6 @@ Main JSON interface views # -*- coding: utf-8 -*- from __future__ import unicode_literals -import logging - from django.utils.translation import ugettext_lazy as _ from django.http import JsonResponse @@ -21,14 +19,7 @@ from .views import AjaxView from .version import inventreeVersion, inventreeApiVersion, inventreeInstanceName from .status import is_worker_running -from plugin.plugins import load_action_plugins - - -logger = logging.getLogger("inventree") - - -logger.info("Loading action plugins...") -action_plugins = load_action_plugins() +from plugin import registry class InfoView(AjaxView): @@ -110,10 +101,11 @@ class ActionPluginView(APIView): 'error': _("No action specified") }) - for plugin_class in action_plugins: - if plugin_class.action_name() == action: - - plugin = plugin_class(request.user, data=data) + action_plugins = registry.with_mixin('action') + for plugin in action_plugins: + if plugin.action_name() == action: + # TODO @matmair use easier syntax once InvenTree 0.7.0 is released + plugin.init(request.user, data=data) plugin.perform_action() diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 2f80844bf1..f2654a49cc 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -880,7 +880,7 @@ PLUGINS_ENABLED = _is_true(get_setting( PLUGIN_FILE = get_plugin_file() # Plugin Directories (local plugins will be loaded from these directories) -PLUGIN_DIRS = ['plugin.builtin', ] +PLUGIN_DIRS = ['plugin.builtin', 'barcodes.plugins', ] if not TESTING: # load local deploy directory in prod diff --git a/InvenTree/barcodes/api.py b/InvenTree/barcodes/api.py index 4b853ab438..355fcab0e1 100644 --- a/InvenTree/barcodes/api.py +++ b/InvenTree/barcodes/api.py @@ -13,7 +13,7 @@ from stock.models import StockItem from stock.serializers import StockItemSerializer from barcodes.barcode import hash_barcode -from plugin.plugins import load_barcode_plugins +from plugin import registry class BarcodeScan(APIView): @@ -53,18 +53,19 @@ class BarcodeScan(APIView): if 'barcode' not in data: raise ValidationError({'barcode': _('Must provide barcode_data parameter')}) - plugins = load_barcode_plugins() + plugins = registry.with_mixin('barcode') barcode_data = data.get('barcode') # Look for a barcode plugin which knows how to deal with this barcode plugin = None - for plugin_class in plugins: - plugin_instance = plugin_class(barcode_data) + for current_plugin in plugins: + # TODO @matmair make simpler after InvenTree 0.7.0 release + current_plugin.init(barcode_data) - if plugin_instance.validate(): - plugin = plugin_instance + if current_plugin.validate(): + plugin = current_plugin break match_found = False @@ -160,15 +161,16 @@ class BarcodeAssign(APIView): except (ValueError, StockItem.DoesNotExist): raise ValidationError({'stockitem': _('No matching stock item found')}) - plugins = load_barcode_plugins() + plugins = registry.with_mixin('barcode') plugin = None - for plugin_class in plugins: - plugin_instance = plugin_class(barcode_data) + for current_plugin in plugins: + # TODO @matmair make simpler after InvenTree 0.7.0 release + current_plugin.init(barcode_data) - if plugin_instance.validate(): - plugin = plugin_instance + if current_plugin.validate(): + plugin = current_plugin break match_found = False diff --git a/InvenTree/barcodes/barcode.py b/InvenTree/barcodes/barcode.py index 51f8a1ffa1..030552c866 100644 --- a/InvenTree/barcodes/barcode.py +++ b/InvenTree/barcodes/barcode.py @@ -1,139 +1,20 @@ # -*- coding: utf-8 -*- +import warnings -import string -import hashlib -import logging +import plugin.builtin.barcode.mixins as mixin +import plugin.integration -from stock.models import StockItem -from stock.serializers import StockItemSerializer, LocationSerializer -from part.serializers import PartSerializer +hash_barcode = mixin.hash_barcode -logger = logging.getLogger('inventree') - - -def hash_barcode(barcode_data): +class BarcodePlugin(mixin.BarcodeMixin, plugin.integration.IntegrationPluginBase): """ - Calculate an MD5 hash of barcode data. - - HACK: Remove any 'non printable' characters from the hash, - as it seems browers will remove special control characters... - - TODO: Work out a way around this! + Legacy barcode plugin definition - will be replaced + Please use the new Integration Plugin API and the BarcodeMixin """ - - barcode_data = str(barcode_data).strip() - - printable_chars = filter(lambda x: x in string.printable, barcode_data) - - barcode_data = ''.join(list(printable_chars)) - - hash = hashlib.md5(str(barcode_data).encode()) - return str(hash.hexdigest()) - - -class BarcodePlugin: - """ - Base class for barcode handling. - Custom barcode plugins should extend this class as necessary. - """ - - # Override the barcode plugin name for each sub-class - PLUGIN_NAME = "" - - @property - def name(self): - return self.PLUGIN_NAME - - def __init__(self, barcode_data): - """ - Initialize the BarcodePlugin instance - - Args: - barcode_data - The raw barcode data - """ - - self.data = barcode_data - - def getStockItem(self): - """ - Attempt to retrieve a StockItem associated with this barcode. - Default implementation returns None - """ - - return None - - def getStockItemByHash(self): - """ - Attempt to retrieve a StockItem associated with this barcode, - based on the barcode hash. - """ - - try: - item = StockItem.objects.get(uid=self.hash()) - return item - except StockItem.DoesNotExist: - return None - - def renderStockItem(self, item): - """ - Render a stock item to JSON response - """ - - serializer = StockItemSerializer(item, part_detail=True, location_detail=True, supplier_part_detail=True) - return serializer.data - - def getStockLocation(self): - """ - Attempt to retrieve a StockLocation associated with this barcode. - Default implementation returns None - """ - - return None - - def renderStockLocation(self, loc): - """ - Render a stock location to a JSON response - """ - - serializer = LocationSerializer(loc) - return serializer.data - - def getPart(self): - """ - Attempt to retrieve a Part associated with this barcode. - Default implementation returns None - """ - - return None - - def renderPart(self, part): - """ - Render a part to JSON response - """ - - serializer = PartSerializer(part) - return serializer.data - - def hash(self): - """ - Calculate a hash for the barcode data. - This is supposed to uniquely identify the barcode contents, - at least within the bardcode sub-type. - - The default implementation simply returns an MD5 hash of the barcode data, - encoded to a string. - - This may be sufficient for most applications, but can obviously be overridden - by a subclass. - - """ - - return hash_barcode(self.data) - - def validate(self): - """ - Default implementation returns False - """ - return False + # TODO @matmair remove this with InvenTree 0.7.0 + def __init__(self, barcode_data=None): + warnings.warn("using the BarcodePlugin is depreceated", DeprecationWarning) + super().__init__() + self.init(barcode_data) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 7cf3098e68..738ecc0a2f 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -263,9 +263,9 @@ class BaseInvenTreeSetting(models.Model): plugin = kwargs.pop('plugin', None) if plugin: - from plugin import InvenTreePlugin + from plugin import InvenTreePluginBase - if issubclass(plugin.__class__, InvenTreePlugin): + if issubclass(plugin.__class__, InvenTreePluginBase): plugin = plugin.plugin_config() kwargs['plugin'] = plugin diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 604d384a67..fde9c80720 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -29,6 +29,8 @@ from markdownx.models import MarkdownxField from django_cleanup import cleanup +from djmoney.contrib.exchange.exceptions import MissingRate + from mptt.models import TreeForeignKey, MPTTModel from mptt.exceptions import InvalidMove from mptt.managers import TreeManager @@ -1832,9 +1834,14 @@ class Part(MPTTModel): def get_purchase_price(self, quantity): currency = currency_code_default() - prices = [convert_money(item.purchase_price, currency).amount for item in self.stock_items.all() if item.purchase_price] + try: + prices = [convert_money(item.purchase_price, currency).amount for item in self.stock_items.all() if item.purchase_price] + except MissingRate: + prices = None + if prices: return min(prices) * quantity, max(prices) * quantity + return None @transaction.atomic diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 4304d2a95d..dc6b8a9632 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -20,6 +20,7 @@ from django.contrib import messages from moneyed import CURRENCIES from djmoney.contrib.exchange.models import convert_money +from djmoney.contrib.exchange.exceptions import MissingRate from PIL import Image @@ -425,7 +426,11 @@ class PartDetail(InvenTreeRoleMixin, DetailView): continue # convert purchase price to current currency - only one currency in the graph - price = convert_money(stock_item.purchase_price, default_currency) + try: + price = convert_money(stock_item.purchase_price, default_currency) + except MissingRate: + continue + line = { 'price': price.amount, 'qty': stock_item.quantity @@ -487,7 +492,11 @@ class PartDetail(InvenTreeRoleMixin, DetailView): if None in [sale_item.purchase_price, sale_item.quantity]: continue - price = convert_money(sale_item.purchase_price, default_currency) + try: + price = convert_money(sale_item.purchase_price, default_currency) + except MissingRate: + continue + line = { 'price': price.amount if price else 0, 'qty': sale_item.quantity, diff --git a/InvenTree/plugin/__init__.py b/InvenTree/plugin/__init__.py index 86f65919c4..b8e40e4271 100644 --- a/InvenTree/plugin/__init__.py +++ b/InvenTree/plugin/__init__.py @@ -2,14 +2,18 @@ Utility file to enable simper imports """ -from .registry import plugin_registry -from .plugin import InvenTreePlugin +from .registry import registry +from .plugin import InvenTreePluginBase from .integration import IntegrationPluginBase from .action import ActionPlugin +from .helpers import MixinNotImplementedError, MixinImplementationError + __all__ = [ 'ActionPlugin', 'IntegrationPluginBase', - 'InvenTreePlugin', - 'plugin_registry', + 'InvenTreePluginBase', + 'registry', + 'MixinNotImplementedError', + 'MixinImplementationError', ] diff --git a/InvenTree/plugin/action.py b/InvenTree/plugin/action.py index 5e36c22e74..9586355aea 100644 --- a/InvenTree/plugin/action.py +++ b/InvenTree/plugin/action.py @@ -2,69 +2,22 @@ """Class for ActionPlugin""" import logging +import warnings -import plugin.plugin as plugin +from plugin.builtin.action.mixins import ActionMixin +import plugin.integration logger = logging.getLogger("inventree") -class ActionPlugin(plugin.InvenTreePlugin): +class ActionPlugin(ActionMixin, plugin.integration.IntegrationPluginBase): """ - The ActionPlugin class is used to perform custom actions + Legacy action definition - will be replaced + Please use the new Integration Plugin API and the Action mixin """ - - ACTION_NAME = "" - - @classmethod - def action_name(cls): - """ - Return the action name for this plugin. - If the ACTION_NAME parameter is empty, - look at the PLUGIN_NAME instead. - """ - action = cls.ACTION_NAME - - if not action: - action = cls.PLUGIN_NAME - - return action - - def __init__(self, user, data=None): - """ - An action plugin takes a user reference, and an optional dataset (dict) - """ - plugin.InvenTreePlugin.__init__(self) - - self.user = user - self.data = data - - def perform_action(self): - """ - Override this method to perform the action! - """ - - def get_result(self): - """ - Result of the action? - """ - - # Re-implement this for cutsom actions - return False - - def get_info(self): - """ - Extra info? Can be a string / dict / etc - """ - return None - - def get_response(self): - """ - Return a response. Default implementation is a simple response - which can be overridden. - """ - return { - "action": self.action_name(), - "result": self.get_result(), - "info": self.get_info(), - } + # TODO @matmair remove this with InvenTree 0.7.0 + def __init__(self, user=None, data=None): + warnings.warn("using the ActionPlugin is depreceated", DeprecationWarning) + super().__init__() + self.init(user, data) diff --git a/InvenTree/plugin/admin.py b/InvenTree/plugin/admin.py index b20aef8057..ba5148e6e3 100644 --- a/InvenTree/plugin/admin.py +++ b/InvenTree/plugin/admin.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals from django.contrib import admin import plugin.models as models -import plugin.registry as registry +import plugin.registry as pl_registry def plugin_update(queryset, new_status: bool): @@ -23,7 +23,7 @@ def plugin_update(queryset, new_status: bool): # Reload plugins if they changed if apps_changed: - registry.plugin_registry.reload_plugins() + pl_registry.reload_plugins() @admin.action(description='Activate plugin(s)') diff --git a/InvenTree/plugin/apps.py b/InvenTree/plugin/apps.py index 47b5e9f024..c297b84ca0 100644 --- a/InvenTree/plugin/apps.py +++ b/InvenTree/plugin/apps.py @@ -8,7 +8,7 @@ from django.conf import settings from maintenance_mode.core import set_maintenance_mode -from plugin import plugin_registry +from plugin import registry logger = logging.getLogger('inventree') @@ -18,14 +18,13 @@ class PluginAppConfig(AppConfig): name = 'plugin' def ready(self): - if settings.PLUGINS_ENABLED: logger.info('Loading InvenTree plugins') - if not plugin_registry.is_loading: + if not registry.is_loading: # this is the first startup - plugin_registry.collect_plugins() - plugin_registry.load_plugins() + registry.collect_plugins() + registry.load_plugins() # drop out of maintenance # makes sure we did not have an error in reloading and maintenance is still active diff --git a/InvenTree/plugin/builtin/action/mixins.py b/InvenTree/plugin/builtin/action/mixins.py new file mode 100644 index 0000000000..18a1876659 --- /dev/null +++ b/InvenTree/plugin/builtin/action/mixins.py @@ -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(), + } diff --git a/InvenTree/plugin/builtin/barcode/__init__.py b/InvenTree/plugin/builtin/barcode/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/InvenTree/plugin/builtin/barcode/mixins.py b/InvenTree/plugin/builtin/barcode/mixins.py new file mode 100644 index 0000000000..b86130f71e --- /dev/null +++ b/InvenTree/plugin/builtin/barcode/mixins.py @@ -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 diff --git a/InvenTree/plugin/builtin/integration/mixins.py b/InvenTree/plugin/builtin/integration/mixins.py index 586fc8a666..a2087ee879 100644 --- a/InvenTree/plugin/builtin/integration/mixins.py +++ b/InvenTree/plugin/builtin/integration/mixins.py @@ -11,6 +11,7 @@ from django.db.utils import OperationalError, ProgrammingError from plugin.models import PluginConfig, PluginSetting from plugin.urls import PLUGIN_BASE +from plugin.helpers import MixinImplementationError, MixinNotImplementedError logger = logging.getLogger('inventree') @@ -86,6 +87,9 @@ class ScheduleMixin: SCHEDULED_TASKS = {} class MixinMeta: + """ + Meta options for this mixin + """ MIXIN_NAME = 'Schedule' def __init__(self): @@ -97,6 +101,9 @@ class ScheduleMixin: @property def has_scheduled_tasks(self): + """ + Are tasks defined for this plugin + """ return bool(self.scheduled_tasks) def validate_scheduled_tasks(self): @@ -105,31 +112,37 @@ class ScheduleMixin: """ if not self.has_scheduled_tasks: - raise ValueError("SCHEDULED_TASKS not defined") + raise MixinImplementationError("SCHEDULED_TASKS not defined") for key, task in self.scheduled_tasks.items(): if 'func' not in task: - raise ValueError(f"Task '{key}' is missing 'func' parameter") + raise MixinImplementationError(f"Task '{key}' is missing 'func' parameter") if 'schedule' not in task: - raise ValueError(f"Task '{key}' is missing 'schedule' parameter") + raise MixinImplementationError(f"Task '{key}' is missing 'schedule' parameter") schedule = task['schedule'].upper().strip() if schedule not in self.ALLOWABLE_SCHEDULE_TYPES: - raise ValueError(f"Task '{key}': Schedule '{schedule}' is not a valid option") + raise MixinImplementationError(f"Task '{key}': Schedule '{schedule}' is not a valid option") # If 'minutes' is selected, it must be provided! if schedule == 'I' and 'minutes' not in task: - raise ValueError(f"Task '{key}' is missing 'minutes' parameter") + raise MixinImplementationError(f"Task '{key}' is missing 'minutes' parameter") def get_task_name(self, key): + """ + Task name for key + """ # Generate a 'unique' task name slug = self.plugin_slug() return f"plugin.{slug}.{key}" def get_task_names(self): + """ + All defined task names + """ # Returns a list of all task names associated with this plugin instance return [self.get_task_name(key) for key in self.scheduled_tasks.keys()] @@ -191,10 +204,17 @@ class EventMixin: """ def process_event(self, event, *args, **kwargs): + """ + Function to handle events + Must be overridden by plugin + """ # Default implementation does not do anything - raise NotImplementedError + raise MixinNotImplementedError class MixinMeta: + """ + Meta options for this mixin + """ MIXIN_NAME = 'Events' def __init__(self): @@ -208,6 +228,9 @@ class UrlsMixin: """ class MixinMeta: + """ + Meta options for this mixin + """ MIXIN_NAME = 'URLs' def __init__(self): @@ -217,28 +240,28 @@ class UrlsMixin: def setup_urls(self): """ - setup url endpoints for this plugin + Setup url endpoints for this plugin """ return getattr(self, 'URLS', None) @property def base_url(self): """ - returns base url for this plugin + Base url for this plugin """ return f'{PLUGIN_BASE}/{self.slug}/' @property def internal_name(self): """ - returns the internal url pattern name + Internal url pattern name """ return f'plugin:{self.slug}:' @property def urlpatterns(self): """ - returns the urlpatterns for this plugin + Urlpatterns for this plugin """ if self.has_urls: return url(f'^{self.slug}/', include((self.urls, self.slug)), name=self.slug) @@ -247,7 +270,7 @@ class UrlsMixin: @property def has_urls(self): """ - does this plugin use custom urls + Does this plugin use custom urls """ return bool(self.urls) @@ -262,7 +285,7 @@ class NavigationMixin: class MixinMeta: """ - meta options for this mixin + Meta options for this mixin """ MIXIN_NAME = 'Navigation Links' @@ -273,26 +296,28 @@ class NavigationMixin: def setup_navigation(self): """ - setup navigation links for this plugin + Setup navigation links for this plugin """ nav_links = getattr(self, 'NAVIGATION', None) if nav_links: # check if needed values are configured for link in nav_links: if False in [a in link for a in ('link', 'name', )]: - raise NotImplementedError('Wrong Link definition', link) + raise MixinNotImplementedError('Wrong Link definition', link) return nav_links @property def has_naviation(self): """ - does this plugin define navigation elements + Does this plugin define navigation elements """ return bool(self.navigation) @property def navigation_name(self): - """name for navigation tab""" + """ + Name for navigation tab + """ name = getattr(self, 'NAVIGATION_TAB_NAME', None) if not name: name = self.human_name @@ -300,7 +325,9 @@ class NavigationMixin: @property def navigation_icon(self): - """icon for navigation tab""" + """ + Icon-name for navigation tab + """ return getattr(self, 'NAVIGATION_TAB_ICON', "fas fa-question") @@ -310,7 +337,9 @@ class AppMixin: """ class MixinMeta: - """meta options for this mixin""" + """m + Mta options for this mixin + """ MIXIN_NAME = 'App registration' def __init__(self): @@ -320,7 +349,7 @@ class AppMixin: @property def has_app(self): """ - this plugin is always an app with this plugin + This plugin is always an app with this plugin """ return True diff --git a/InvenTree/plugin/events.py b/InvenTree/plugin/events.py index b714d123b9..049c8626c5 100644 --- a/InvenTree/plugin/events.py +++ b/InvenTree/plugin/events.py @@ -17,7 +17,7 @@ from common.models import InvenTreeSetting from InvenTree.ready import canAppAccessDatabase from InvenTree.tasks import offload_task -from plugin.registry import plugin_registry +from plugin.registry import registry logger = logging.getLogger('inventree') @@ -64,7 +64,7 @@ def register_event(event, *args, **kwargs): with transaction.atomic(): - for slug, plugin in plugin_registry.plugins.items(): + for slug, plugin in registry.plugins.items(): if plugin.mixin_enabled('events'): @@ -95,7 +95,7 @@ def process_event(plugin_slug, event, *args, **kwargs): logger.info(f"Plugin '{plugin_slug}' is processing triggered event '{event}'") - plugin = plugin_registry.plugins[plugin_slug] + plugin = registry.plugins[plugin_slug] plugin.process_event(event, *args, **kwargs) diff --git a/InvenTree/plugin/helpers.py b/InvenTree/plugin/helpers.py index fb46df8927..c56f5a9631 100644 --- a/InvenTree/plugin/helpers.py +++ b/InvenTree/plugin/helpers.py @@ -1,26 +1,23 @@ -"""Helpers for plugin app""" +""" +Helpers for plugin app +""" import os import subprocess import pathlib import sysconfig import traceback +import inspect +import pkgutil from django.conf import settings +from django.core.exceptions import AppRegistryNotReady # region logging / errors -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): + """ + Error that encapsulates another error and adds the path / reference of the raising plugin + """ def __init__(self, path, message): self.path = path self.message = message @@ -29,7 +26,39 @@ class IntegrationPluginError(Exception): 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 install_path = sysconfig.get_paths()["purelib"] try: @@ -53,18 +82,23 @@ def get_plugin_error(error, do_raise: bool = False, do_log: bool = False, log_na log_kwargs = {} if log_name: log_kwargs['reference'] = log_name - log_plugin_error({package_name: str(error)}, **log_kwargs) + log_error({package_name: str(error)}, **log_kwargs) + + new_error = IntegrationPluginError(package_name, str(error)) if do_raise: raise IntegrationPluginError(package_name, str(error)) - return package_name, str(error) + if do_return: + return new_error # endregion # region git-helpers def get_git_log(path): - """get dict with info of the last commit to file named in path""" + """ + Get dict with info of the last commit to file named in path + """ path = path.replace(os.path.dirname(settings.BASE_DIR), '')[1:] command = ['git', 'log', '-n', '1', "--pretty=format:'%H%n%aN%n%aE%n%aI%n%f%n%G?%n%GK'", '--follow', '--', path] try: @@ -79,9 +113,13 @@ def get_git_log(path): class GitStatus: - """class for resolving git gpg singing state""" + """ + Class for resolving git gpg singing state + """ class Definition: - """definition of a git gpg sing state""" + """ + Definition of a git gpg sing state + """ key: str = 'N' status: int = 2 msg: str = '' @@ -100,3 +138,56 @@ class GitStatus: R = Definition(key='R', status=2, msg='good signature, revoked key',) E = Definition(key='E', status=1, msg='cannot be checked',) # endregion + + +# region plugin finders +def get_modules(pkg): + """get all modules in a package""" + + context = {} + for loader, name, ispkg in pkgutil.walk_packages(pkg.__path__): + try: + module = loader.find_module(name).load_module(name) + pkg_names = getattr(module, '__all__', None) + for k, v in vars(module).items(): + if not k.startswith('_') and (pkg_names is None or k in pkg_names): + context[k] = v + context[name] = module + except AppRegistryNotReady: + pass + except Exception as error: + # this 'protects' against malformed plugin modules by more or less silently failing + + # log to stack + log_error({name: str(error)}, 'discovery') + + return [v for k, v in context.items()] + + +def get_classes(module): + """get all classes in a given module""" + return inspect.getmembers(module, inspect.isclass) + + +def get_plugins(pkg, baseclass): + """ + Return a list of all modules under a given package. + + - Modules must be a subclass of the provided 'baseclass' + - Modules must have a non-empty PLUGIN_NAME parameter + """ + + plugins = [] + + modules = get_modules(pkg) + + # Iterate through each module in the package + for mod in modules: + # Iterate through each class in the module + for item in get_classes(mod): + plugin = item[1] + if issubclass(plugin, baseclass) and plugin.PLUGIN_NAME: + plugins.append(plugin) + + return plugins +# endregion diff --git a/InvenTree/plugin/integration.py b/InvenTree/plugin/integration.py index c00b81419d..33afae852c 100644 --- a/InvenTree/plugin/integration.py +++ b/InvenTree/plugin/integration.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- -"""class for IntegrationPluginBase and Mixins for it""" +""" +Class for IntegrationPluginBase and Mixin Base +""" import logging import os @@ -11,7 +13,7 @@ from django.urls.base import reverse from django.conf import settings from django.utils.translation import ugettext_lazy as _ -import plugin.plugin as plugin +import plugin.plugin as plugin_base from plugin.helpers import get_git_log, GitStatus @@ -20,7 +22,7 @@ logger = logging.getLogger("inventree") class MixinBase: """ - General base for mixins + Base set of mixin functions and mechanisms """ def __init__(self) -> None: @@ -65,7 +67,7 @@ class MixinBase: return mixins -class IntegrationPluginBase(MixinBase, plugin.InvenTreePlugin): +class IntegrationPluginBase(MixinBase, plugin_base.InvenTreePluginBase): """ The IntegrationPluginBase class is used to integrate with 3rd party software """ @@ -83,32 +85,42 @@ class IntegrationPluginBase(MixinBase, plugin.InvenTreePlugin): self.def_path = inspect.getfile(self.__class__) self.path = os.path.dirname(self.def_path) - self.set_package() + self.define_package() @property def _is_package(self): + """ + Is the plugin delivered as a package + """ return getattr(self, 'is_package', False) # region properties @property def slug(self): + """ + Slug of plugin + """ return self.plugin_slug() @property def name(self): + """ + Name of plugin + """ return self.plugin_name() @property def human_name(self): - """human readable name for labels etc.""" - human_name = getattr(self, 'PLUGIN_TITLE', None) - if not human_name: - human_name = self.plugin_name() - return human_name + """ + Human readable name of plugin + """ + return self.plugin_title() @property def description(self): - """description of plugin""" + """ + Description of plugin + """ description = getattr(self, 'DESCRIPTION', None) if not description: description = self.plugin_name() @@ -116,7 +128,9 @@ class IntegrationPluginBase(MixinBase, plugin.InvenTreePlugin): @property def author(self): - """returns author of plugin - either from plugin settings or git""" + """ + Author of plugin - either from plugin settings or git + """ author = getattr(self, 'AUTHOR', None) if not author: author = self.package.get('author') @@ -126,7 +140,9 @@ class IntegrationPluginBase(MixinBase, plugin.InvenTreePlugin): @property def pub_date(self): - """returns publishing date of plugin - either from plugin settings or git""" + """ + Publishing date of plugin - either from plugin settings or git + """ pub_date = getattr(self, 'PUBLISH_DATE', None) if not pub_date: pub_date = self.package.get('date') @@ -138,42 +154,56 @@ class IntegrationPluginBase(MixinBase, plugin.InvenTreePlugin): @property def version(self): - """returns version of plugin""" + """ + Version of plugin + """ version = getattr(self, 'VERSION', None) return version @property def website(self): - """returns website of plugin""" + """ + Website of plugin - if set else None + """ website = getattr(self, 'WEBSITE', None) return website @property def license(self): - """returns license of plugin""" + """ + License of plugin + """ license = getattr(self, 'LICENSE', None) return license # endregion @property def package_path(self): - """returns the path to the plugin""" + """ + Path to the plugin + """ if self._is_package: return self.__module__ return pathlib.Path(self.def_path).relative_to(settings.BASE_DIR) @property def settings_url(self): - """returns url to the settings panel""" + """ + URL to the settings panel for this plugin + """ return f'{reverse("settings")}#select-plugin-{self.slug}' # region mixins def mixin(self, key): - """check if mixin is registered""" + """ + Check if mixin is registered + """ return key in self._mixins def mixin_enabled(self, key): - """check if mixin is enabled and ready""" + """ + Check if mixin is registered, enabled and ready + """ if self.mixin(key): fnc_name = self._mixins.get(key) @@ -186,17 +216,23 @@ class IntegrationPluginBase(MixinBase, plugin.InvenTreePlugin): # endregion # region package info - def get_package_commit(self): - """get last git commit for plugin""" + def _get_package_commit(self): + """ + Get last git commit for the plugin + """ return get_git_log(self.def_path) - def get_package_metadata(self): - """get package metadata for plugin""" + def _get_package_metadata(self): + """ + Get package metadata for plugin + """ return {} - def set_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() + def define_package(self): + """ + Add package info of the plugin into plugins context + """ + package = self._get_package_metadata() if self._is_package else self._get_package_commit() # process date if package.get('date'): diff --git a/InvenTree/plugin/loader.py b/InvenTree/plugin/loader.py index 2d17f8c36f..aaba9fe060 100644 --- a/InvenTree/plugin/loader.py +++ b/InvenTree/plugin/loader.py @@ -4,7 +4,7 @@ load templates for loaded plugins from django.template.loaders.filesystem import Loader as FilesystemLoader from pathlib import Path -from plugin import plugin_registry +from plugin import registry class PluginTemplateLoader(FilesystemLoader): @@ -12,7 +12,7 @@ class PluginTemplateLoader(FilesystemLoader): def get_dirs(self): dirname = 'templates' template_dirs = [] - for plugin in plugin_registry.plugins.values(): + for plugin in registry.plugins.values(): new_path = Path(plugin.path) / dirname if Path(new_path).is_dir(): template_dirs.append(new_path) diff --git a/InvenTree/plugin/mixins/__init__.py b/InvenTree/plugin/mixins/__init__.py index ca5f0c615d..8097b0b459 100644 --- a/InvenTree/plugin/mixins/__init__.py +++ b/InvenTree/plugin/mixins/__init__.py @@ -3,6 +3,8 @@ Utility class to enable simpler imports """ 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__ = [ 'APICallMixin', @@ -12,4 +14,6 @@ __all__ = [ 'ScheduleMixin', 'SettingsMixin', 'UrlsMixin', + 'ActionMixin', + 'BarcodeMixin', ] diff --git a/InvenTree/plugin/models.py b/InvenTree/plugin/models.py index 8b81eb2062..30735e7510 100644 --- a/InvenTree/plugin/models.py +++ b/InvenTree/plugin/models.py @@ -10,7 +10,7 @@ from django.db import models import common.models -from plugin import InvenTreePlugin, plugin_registry +from plugin import InvenTreePluginBase, registry class PluginConfig(models.Model): @@ -72,7 +72,7 @@ class PluginConfig(models.Model): self.__org_active = self.active # append settings from registry - self.plugin = plugin_registry.plugins.get(self.key, None) + self.plugin = registry.plugins.get(self.key, None) def get_plugin_meta(name): if self.plugin: @@ -95,10 +95,10 @@ class PluginConfig(models.Model): if not reload: if self.active is False and self.__org_active is True: - plugin_registry.reload_plugins() + registry.reload_plugins() elif self.active is True and self.__org_active is False: - plugin_registry.reload_plugins() + registry.reload_plugins() return ret @@ -164,10 +164,10 @@ class PluginSetting(common.models.BaseInvenTreeSetting): if plugin: - if issubclass(plugin.__class__, InvenTreePlugin): + if issubclass(plugin.__class__, InvenTreePluginBase): 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) @@ -182,7 +182,7 @@ class PluginSetting(common.models.BaseInvenTreeSetting): plugin = kwargs.get('plugin', None) if plugin: - if issubclass(plugin.__class__, InvenTreePlugin): + if issubclass(plugin.__class__, InvenTreePluginBase): plugin = plugin.plugin_config() filters['plugin'] = plugin diff --git a/InvenTree/plugin/plugin.py b/InvenTree/plugin/plugin.py index 8842553ba7..9abbcc041e 100644 --- a/InvenTree/plugin/plugin.py +++ b/InvenTree/plugin/plugin.py @@ -2,14 +2,16 @@ """ Base Class for InvenTree plugins """ +import warnings from django.db.utils import OperationalError, ProgrammingError from django.utils.text import slugify -class InvenTreePlugin(): +class InvenTreePluginBase(): """ Base class for a plugin + DO NOT USE THIS DIRECTLY, USE plugin.IntegrationPluginBase """ def __init__(self): @@ -24,11 +26,15 @@ class InvenTreePlugin(): def plugin_name(self): """ - Return the name of this plugin plugin + Name of plugin """ return self.PLUGIN_NAME def plugin_slug(self): + """ + Slug of plugin + If not set plugin name slugified + """ slug = getattr(self, 'PLUGIN_SLUG', None) @@ -38,6 +44,9 @@ class InvenTreePlugin(): return slugify(slug.lower()) def plugin_title(self): + """ + Title of plugin + """ if self.PLUGIN_TITLE: return self.PLUGIN_TITLE @@ -75,3 +84,13 @@ class InvenTreePlugin(): return cfg.active else: return False + + +# TODO @matmair remove after InvenTree 0.7.0 release +class InvenTreePlugin(InvenTreePluginBase): + """ + This is here for leagcy reasons and will be removed in the next major release + """ + def __init__(self): + warnings.warn("Using the InvenTreePlugin is depreceated", DeprecationWarning) + super().__init__() diff --git a/InvenTree/plugin/plugins.py b/InvenTree/plugin/plugins.py deleted file mode 100644 index e2be1e6427..0000000000 --- a/InvenTree/plugin/plugins.py +++ /dev/null @@ -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) diff --git a/InvenTree/plugin/registry.py b/InvenTree/plugin/registry.py index 448365b338..e31f3c6529 100644 --- a/InvenTree/plugin/registry.py +++ b/InvenTree/plugin/registry.py @@ -28,9 +28,8 @@ except: from maintenance_mode.core import maintenance_mode_on from maintenance_mode.core import get_maintenance_mode, set_maintenance_mode -from plugin import plugins as inventree_plugins from .integration import IntegrationPluginBase -from .helpers import get_plugin_error, IntegrationPluginError +from .helpers import handle_error, log_error, get_plugins, IntegrationPluginError logger = logging.getLogger('inventree') @@ -60,18 +59,16 @@ class PluginsRegistry: # mixins self.mixins_settings = {} - # region public plugin functions + # region public functions + # region loading / unloading def load_plugins(self): """ Load and activate all IntegrationPlugins """ - if not settings.PLUGINS_ENABLED: # Plugins not enabled, do nothing return - from plugin.helpers import log_plugin_error - logger.info('Start loading plugins') # Set maintanace mode @@ -95,7 +92,7 @@ class PluginsRegistry: break except IntegrationPluginError as error: logger.error(f'[PLUGIN] Encountered an error with {error.path}:\n{error.message}') - log_plugin_error({error.path: error.message}, 'load') + log_error({error.path: error.message}, 'load') blocked_plugin = error.path # we will not try to load this app again # Initialize apps without any integration plugins @@ -179,7 +176,7 @@ class PluginsRegistry: # Collect plugins from paths for plugin in settings.PLUGIN_DIRS: - modules = inventree_plugins.get_plugins(importlib.import_module(plugin), IntegrationPluginBase, True) + modules = get_plugins(importlib.import_module(plugin), IntegrationPluginBase) if modules: [self.plugin_modules.append(item) for item in modules] @@ -192,12 +189,29 @@ class PluginsRegistry: plugin.is_package = True self.plugin_modules.append(plugin) except Exception as error: - get_plugin_error(error, do_log=True, log_name='discovery') + handle_error(error, do_raise=False, log_name='discovery') # Log collected plugins logger.info(f'Collected {len(self.plugin_modules)} plugins!') logger.info(", ".join([a.__module__ for a in self.plugin_modules])) + # endregion + # region registry functions + def with_mixin(self, mixin: str): + """ + Returns reference to all plugins that have a specified mixin enabled + """ + 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): """ Initialise all found plugins @@ -254,7 +268,7 @@ class PluginsRegistry: plugin = plugin() except Exception as error: # log error and raise it -> disable plugin - get_plugin_error(error, do_raise=True, do_log=True, log_name='init') + handle_error(error, log_name='init') logger.info(f'Loaded integration plugin {plugin.slug}') plugin.is_package = was_packaged @@ -290,7 +304,9 @@ class PluginsRegistry: self.deactivate_integration_app() self.deactivate_integration_schedule() self.deactivate_integration_settings() + # endregion + # region mixin specific loading ... def activate_integration_settings(self, plugins): logger.info('Activating plugin settings') @@ -536,7 +552,8 @@ class PluginsRegistry: cmd(*args, **kwargs) return True, [] except Exception as error: - get_plugin_error(error, do_raise=True) + handle_error(error) + # endregion -plugin_registry = PluginsRegistry() +registry = PluginsRegistry() diff --git a/InvenTree/plugin/samples/integration/test_api_caller.py b/InvenTree/plugin/samples/integration/test_api_caller.py index e15edfad94..32ee07bbd3 100644 --- a/InvenTree/plugin/samples/integration/test_api_caller.py +++ b/InvenTree/plugin/samples/integration/test_api_caller.py @@ -2,7 +2,7 @@ from django.test import TestCase -from plugin import plugin_registry +from plugin import registry class SampleApiCallerPluginTests(TestCase): @@ -11,8 +11,8 @@ class SampleApiCallerPluginTests(TestCase): def test_return(self): """check if the external api call works""" # The plugin should be defined - self.assertIn('sample-api-caller', plugin_registry.plugins) - plg = plugin_registry.plugins['sample-api-caller'] + self.assertIn('sample-api-caller', registry.plugins) + plg = registry.plugins['sample-api-caller'] self.assertTrue(plg) # do an api call diff --git a/InvenTree/plugin/templatetags/plugin_extras.py b/InvenTree/plugin/templatetags/plugin_extras.py index 7852133ebb..f9557c84ff 100644 --- a/InvenTree/plugin/templatetags/plugin_extras.py +++ b/InvenTree/plugin/templatetags/plugin_extras.py @@ -7,7 +7,7 @@ from django import template from django.urls import reverse from common.models import InvenTreeSetting -from plugin import plugin_registry +from plugin import registry register = template.Library() @@ -15,31 +15,41 @@ register = template.Library() @register.simple_tag() 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() 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() 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() def mixin_enabled(plugin, key, *args, **kwargs): - """ Return if the mixin is existant and configured in the plugin """ + """ + Is the mixin registerd and configured in the plugin? + """ return plugin.mixin_enabled(key) @register.simple_tag() def navigation_enabled(*args, **kwargs): - """Return if plugin navigation is enabled""" + """ + Is plugin navigation enabled? + """ if djangosettings.PLUGIN_TESTING: return True return InvenTreeSetting.get_setting('ENABLE_PLUGINS_NAVIGATION') @@ -47,7 +57,10 @@ def navigation_enabled(*args, **kwargs): @register.simple_tag() def safe_url(view_name, *args, **kwargs): - """ safe lookup for urls """ + """ + Safe lookup fnc for URLs + Returns None if not found + """ try: return reverse(view_name, args=args, kwargs=kwargs) except: @@ -56,5 +69,7 @@ def safe_url(view_name, *args, **kwargs): @register.simple_tag() def plugin_errors(*args, **kwargs): - """Return all plugin errors""" - return plugin_registry.errors + """ + All plugin errors in the current session + """ + return registry.errors diff --git a/InvenTree/plugin/test_api.py b/InvenTree/plugin/test_api.py index fdc97e407b..67ca5a69ad 100644 --- a/InvenTree/plugin/test_api.py +++ b/InvenTree/plugin/test_api.py @@ -64,14 +64,14 @@ class PluginDetailAPITest(InvenTreeAPITestCase): Test the PluginConfig action commands """ from plugin.models import PluginConfig - from plugin import plugin_registry + from plugin import registry url = reverse('admin:plugin_pluginconfig_changelist') fixtures = PluginConfig.objects.all() # check if plugins were registered -> in some test setups the startup has no db access if not fixtures: - plugin_registry.reload_plugins() + registry.reload_plugins() fixtures = PluginConfig.objects.all() print([str(a) for a in fixtures]) diff --git a/InvenTree/plugin/test_plugin.py b/InvenTree/plugin/test_plugin.py index 2013ad43c8..f88b6e6176 100644 --- a/InvenTree/plugin/test_plugin.py +++ b/InvenTree/plugin/test_plugin.py @@ -4,20 +4,18 @@ Unit tests for plugins from django.test import TestCase -import plugin.plugin -import plugin.integration from plugin.samples.integration.sample import SampleIntegrationPlugin from plugin.samples.integration.another_sample import WrongIntegrationPlugin, NoIntegrationPlugin import plugin.templatetags.plugin_extras as plugin_tags -from plugin import plugin_registry +from plugin import registry, InvenTreePluginBase class InvenTreePluginTests(TestCase): """ Tests for InvenTreePlugin """ def setUp(self): - self.plugin = plugin.plugin.InvenTreePlugin() + self.plugin = InvenTreePluginBase() - class NamedPlugin(plugin.plugin.InvenTreePlugin): + class NamedPlugin(InvenTreePluginBase): """a named plugin""" PLUGIN_NAME = 'abc123' @@ -34,20 +32,6 @@ class InvenTreePluginTests(TestCase): self.assertEqual(self.named_plugin.plugin_name(), 'abc123') -class PluginIntegrationTests(TestCase): - """ Tests for general plugin functions """ - - def test_plugin_loading(self): - """check if plugins load as expected""" - # plugin_names_barcode = [a().plugin_name() for a in load_barcode_plugins()] # TODO refactor barcode plugin to support standard loading - # 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): """ Tests for the plugin extras """ @@ -58,17 +42,17 @@ class PluginTagTests(TestCase): def test_tag_plugin_list(self): """test that all plugins are listed""" - self.assertEqual(plugin_tags.plugin_list(), plugin_registry.plugins) + self.assertEqual(plugin_tags.plugin_list(), registry.plugins) def test_tag_incative_plugin_list(self): """test that all inactive plugins are listed""" - self.assertEqual(plugin_tags.inactive_plugin_list(), plugin_registry.plugins_inactive) + self.assertEqual(plugin_tags.inactive_plugin_list(), registry.plugins_inactive) def test_tag_plugin_settings(self): """check all plugins are listed""" self.assertEqual( plugin_tags.plugin_settings(self.sample), - plugin_registry.mixins_settings.get(self.sample) + registry.mixins_settings.get(self.sample) ) def test_tag_mixin_enabled(self): @@ -90,4 +74,4 @@ class PluginTagTests(TestCase): def test_tag_plugin_errors(self): """test that all errors are listed""" - self.assertEqual(plugin_tags.plugin_errors(), plugin_registry.errors) + self.assertEqual(plugin_tags.plugin_errors(), registry.errors) diff --git a/InvenTree/plugin/urls.py b/InvenTree/plugin/urls.py index 1457aaf6f1..d41a84e2f1 100644 --- a/InvenTree/plugin/urls.py +++ b/InvenTree/plugin/urls.py @@ -4,7 +4,7 @@ URL lookup for plugin app from django.conf.urls import url, include -from plugin import plugin_registry +from plugin import registry PLUGIN_BASE = 'plugin' # Constant for links @@ -17,7 +17,7 @@ def get_plugin_urls(): urls = [] - for plugin in plugin_registry.plugins.values(): + for plugin in registry.plugins.values(): if plugin.mixin_enabled('urls'): urls.append(plugin.urlpatterns) diff --git a/InvenTree/templates/500.html b/InvenTree/templates/500.html new file mode 100644 index 0000000000..3fab6c0a17 --- /dev/null +++ b/InvenTree/templates/500.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block page_title %} +InvenTree | {% trans "Internal Server Error" %} +{% endblock %} + +{% block content %} + +
+

{% trans "Internal Server Error" %}

+ +
+ {% trans "The InvenTree server raised an internal error" %}
+ {% trans "Refer to the error log in the admin interface for further details" %} +
+
+ +{% endblock %} \ No newline at end of file diff --git a/InvenTree/templates/js/translated/part.js b/InvenTree/templates/js/translated/part.js index 020c5d1a6c..093aec388b 100644 --- a/InvenTree/templates/js/translated/part.js +++ b/InvenTree/templates/js/translated/part.js @@ -929,6 +929,7 @@ function loadPartPurchaseOrderTable(table, part_id, options={}) { { field: 'actions', title: '', + switchable: false, formatter: function(value, row) { if (row.received >= row.quantity) {