diff --git a/InvenTree/plugin/base/barcodes/mixins.py b/InvenTree/plugin/base/barcodes/mixins.py index ef3b6004ee..ce87f8163f 100644 --- a/InvenTree/plugin/base/barcodes/mixins.py +++ b/InvenTree/plugin/base/barcodes/mixins.py @@ -1,6 +1,5 @@ -""" -Plugin mixin classes for barcode plugin -""" +"""Plugin mixin classes for barcode plugin""" + import hashlib import string @@ -10,15 +9,13 @@ from stock.serializers import LocationSerializer, StockItemSerializer def hash_barcode(barcode_data): - """ - Calculate an MD5 hash of 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) @@ -30,16 +27,16 @@ def hash_barcode(barcode_data): class BarcodeMixin: - """ - Mixin that enables barcode handeling + """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 - """ + """Meta options for this mixin""" + MIXIN_NAME = 'Barcode' def __init__(self): @@ -48,35 +45,27 @@ class BarcodeMixin: @property def has_barcode(self): - """ - Does this plugin have everything needed to process a barcode - """ + """Does this plugin have everything needed to process a barcode""" return True def init(self, barcode_data): - """ - Initialize the BarcodePlugin instance + """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. + """Attempt to retrieve a StockItem associated with this barcode. + Default implementation returns None """ - return None # pragma: no cover def getStockItemByHash(self): + """Attempt to retrieve a StockItem associated with this barcode, based on the barcode hash. """ - Attempt to retrieve a StockItem associated with this barcode, - based on the barcode hash. - """ - try: item = StockItem.objects.get(uid=self.hash()) return item @@ -84,48 +73,37 @@ class BarcodeMixin: return None def renderStockItem(self, item): - """ - Render a stock item to JSON response - """ - + """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. + """Attempt to retrieve a StockLocation associated with this barcode. + Default implementation returns None """ - return None # pragma: no cover def renderStockLocation(self, loc): - """ - Render a stock location to a JSON response - """ - + """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. + """Attempt to retrieve a Part associated with this barcode. + Default implementation returns None """ - return None # pragma: no cover def renderPart(self, part): - """ - Render a part to JSON response - """ - + """Render a part to JSON response""" serializer = PartSerializer(part) return serializer.data def hash(self): - """ - Calculate a hash for the barcode data. + """Calculate a hash for the barcode data. + This is supposed to uniquely identify the barcode contents, at least within the bardcode sub-type. @@ -134,13 +112,9 @@ class BarcodeMixin: 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 - """ + """Default implementation returns False""" return False # pragma: no cover diff --git a/InvenTree/plugin/base/barcodes/test_barcode.py b/InvenTree/plugin/base/barcodes/test_barcode.py index 4b66f8b208..178223816e 100644 --- a/InvenTree/plugin/base/barcodes/test_barcode.py +++ b/InvenTree/plugin/base/barcodes/test_barcode.py @@ -1,8 +1,4 @@ -# -*- coding: utf-8 -*- - -""" -Unit tests for Barcode endpoints -""" +"""Unit tests for Barcode endpoints""" from django.urls import reverse @@ -62,10 +58,7 @@ class BarcodeAPITest(InvenTreeAPITestCase): self.assertIsNone(data['plugin']) def test_find_part(self): - """ - Test that we can lookup a part based on ID - """ - + """Test that we can lookup a part based on ID""" response = self.client.post( self.scan_url, { @@ -98,10 +91,7 @@ class BarcodeAPITest(InvenTreeAPITestCase): self.assertEqual(response.data['part'], 'Part does not exist') def test_find_stock_item(self): - """ - Test that we can lookup a stock item based on ID - """ - + """Test that we can lookup a stock item based on ID""" response = self.client.post( self.scan_url, { @@ -119,7 +109,6 @@ class BarcodeAPITest(InvenTreeAPITestCase): def test_invalid_item(self): """Test response for invalid stock item""" - response = self.client.post( self.scan_url, { @@ -135,10 +124,7 @@ class BarcodeAPITest(InvenTreeAPITestCase): self.assertEqual(response.data['stockitem'], 'Stock item does not exist') def test_find_location(self): - """ - Test that we can lookup a stock location based on ID - """ - + """Test that we can lookup a stock location based on ID""" response = self.client.post( self.scan_url, { @@ -156,7 +142,6 @@ class BarcodeAPITest(InvenTreeAPITestCase): def test_invalid_location(self): """Test response for an invalid location""" - response = self.client.post( self.scan_url, { @@ -215,10 +200,7 @@ class BarcodeAPITest(InvenTreeAPITestCase): self.assertEqual(pk, item.pk) def test_association(self): - """ - Test that a barcode can be associated with a StockItem - """ - + """Test that a barcode can be associated with a StockItem""" item = StockItem.objects.get(pk=522) self.assertEqual(len(item.uid), 0) diff --git a/InvenTree/plugin/base/event/events.py b/InvenTree/plugin/base/event/events.py index d870269736..d510755465 100644 --- a/InvenTree/plugin/base/event/events.py +++ b/InvenTree/plugin/base/event/events.py @@ -1,6 +1,4 @@ -""" -Functions for triggering and responding to server side events -""" +"""Functions for triggering and responding to server side events""" import logging @@ -17,13 +15,11 @@ logger = logging.getLogger('inventree') def trigger_event(event, *args, **kwargs): - """ - Trigger an event with optional arguments. + """Trigger an event with optional arguments. This event will be stored in the database, and the worker will respond to it later on. """ - if not settings.PLUGINS_ENABLED: # Do nothing if plugins are not enabled return # pragma: no cover @@ -44,8 +40,7 @@ def trigger_event(event, *args, **kwargs): def register_event(event, *args, **kwargs): - """ - Register the event with any interested plugins. + """Register the event with any interested plugins. Note: This function is processed by the background worker, as it performs multiple database access operations. @@ -80,14 +75,11 @@ def register_event(event, *args, **kwargs): def process_event(plugin_slug, event, *args, **kwargs): - """ - Respond to a triggered event. + """Respond to a triggered event. This function is run by the background worker process. - This function may queue multiple functions to be handled by the background worker. """ - logger.info(f"Plugin '{plugin_slug}' is processing triggered event '{event}'") plugin = registry.plugins.get(plugin_slug, None) @@ -100,11 +92,10 @@ def process_event(plugin_slug, event, *args, **kwargs): def allow_table_event(table_name): - """ - Determine if an automatic event should be fired for a given table. + """Determine if an automatic event should be fired for a given table. + We *do not* want events to be fired for some tables! """ - if isImportingData(): # Prevent table events during the data import process return False # pragma: no cover @@ -143,10 +134,7 @@ def allow_table_event(table_name): @receiver(post_save) def after_save(sender, instance, created, **kwargs): - """ - Trigger an event whenever a database entry is saved - """ - + """Trigger an event whenever a database entry is saved""" table = sender.objects.model._meta.db_table instance_id = getattr(instance, 'id', None) @@ -173,10 +161,7 @@ def after_save(sender, instance, created, **kwargs): @receiver(post_delete) def after_delete(sender, instance, **kwargs): - """ - Trigger an event whenever a database entry is deleted - """ - + """Trigger an event whenever a database entry is deleted""" table = sender.objects.model._meta.db_table if not allow_table_event(table): diff --git a/InvenTree/plugin/base/event/mixins.py b/InvenTree/plugin/base/event/mixins.py index 8df9bfd164..d18c800bfe 100644 --- a/InvenTree/plugin/base/event/mixins.py +++ b/InvenTree/plugin/base/event/mixins.py @@ -4,24 +4,22 @@ from plugin.helpers import MixinNotImplementedError class EventMixin: - """ - Mixin that provides support for responding to triggered events. + """Mixin that provides support for responding to triggered events. Implementing classes must provide a "process_event" function: """ def process_event(self, event, *args, **kwargs): - """ - Function to handle events + """Function to handle events + Must be overridden by plugin """ # Default implementation does not do anything raise MixinNotImplementedError class MixinMeta: - """ - Meta options for this mixin - """ + """Meta options for this mixin""" + MIXIN_NAME = 'Events' def __init__(self): diff --git a/InvenTree/plugin/base/integration/mixins.py b/InvenTree/plugin/base/integration/mixins.py index 4cd2123ef3..7cf24a1ea3 100644 --- a/InvenTree/plugin/base/integration/mixins.py +++ b/InvenTree/plugin/base/integration/mixins.py @@ -1,6 +1,4 @@ -""" -Plugin mixin classes -""" +"""Plugin mixin classes""" import json import logging @@ -21,9 +19,7 @@ logger = logging.getLogger('inventree') class SettingsMixin: - """ - Mixin that enables global settings for the plugin - """ + """Mixin that enables global settings for the plugin""" class MixinMeta: MIXIN_NAME = 'Settings' @@ -35,23 +31,15 @@ class SettingsMixin: @property def has_settings(self): - """ - Does this plugin use custom global settings - """ + """Does this plugin use custom global settings""" return bool(self.settings) def get_setting(self, key): - """ - Return the 'value' of the setting associated with this plugin - """ - + """Return the 'value' of the setting associated with this plugin""" return PluginSetting.get_setting(key, plugin=self) def set_setting(self, key, value, user=None): - """ - Set plugin setting value by key - """ - + """Set plugin setting value by key""" try: plugin, _ = PluginConfig.objects.get_or_create(key=self.plugin_slug(), name=self.plugin_name()) except (OperationalError, ProgrammingError): # pragma: no cover @@ -66,8 +54,7 @@ class SettingsMixin: class ScheduleMixin: - """ - Mixin that provides support for scheduled tasks. + """Mixin that provides support for scheduled tasks. Implementing classes must provide a dict object called SCHEDULED_TASKS, which provides information on the tasks to be scheduled. @@ -99,9 +86,8 @@ class ScheduleMixin: SCHEDULED_TASKS = {} class MixinMeta: - """ - Meta options for this mixin - """ + """Meta options for this mixin""" + MIXIN_NAME = 'Schedule' def __init__(self): @@ -116,16 +102,11 @@ class ScheduleMixin: @property def has_scheduled_tasks(self): - """ - Are tasks defined for this plugin - """ + """Are tasks defined for this plugin""" return bool(self.scheduled_tasks) def validate_scheduled_tasks(self): - """ - Check that the provided scheduled tasks are valid - """ - + """Check that the provided scheduled tasks are valid""" if not self.has_scheduled_tasks: raise MixinImplementationError("SCHEDULED_TASKS not defined") @@ -147,25 +128,18 @@ class ScheduleMixin: raise MixinImplementationError(f"Task '{key}' is missing 'minutes' parameter") def get_task_name(self, key): - """ - Task name for 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 - """ + """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()] def register_tasks(self): - """ - Register the tasks with the database - """ - + """Register the tasks with the database""" try: from django_q.models import Schedule @@ -182,10 +156,7 @@ class ScheduleMixin: func_name = task['func'].strip() if '.' in func_name: - """ - Dotted notation indicates that we wish to run a globally defined function, - from a specified Python module. - """ + """Dotted notation indicates that we wish to run a globally defined function, from a specified Python module.""" Schedule.objects.create( name=task_name, @@ -196,8 +167,7 @@ class ScheduleMixin: ) else: - """ - Non-dotted notation indicates that we wish to call a 'member function' of the calling plugin. + """Non-dotted notation indicates that we wish to call a 'member function' of the calling plugin. This is managed by the plugin registry itself. """ @@ -218,10 +188,7 @@ class ScheduleMixin: logger.warning("register_tasks failed, database not ready") def unregister_tasks(self): - """ - Deregister the tasks with the database - """ - + """Deregister the tasks with the database""" try: from django_q.models import Schedule @@ -240,14 +207,11 @@ class ScheduleMixin: class UrlsMixin: - """ - Mixin that enables custom URLs for the plugin - """ + """Mixin that enables custom URLs for the plugin""" class MixinMeta: - """ - Meta options for this mixin - """ + """Meta options for this mixin""" + MIXIN_NAME = 'URLs' def __init__(self): @@ -256,54 +220,41 @@ class UrlsMixin: self.urls = self.setup_urls() 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): - """ - Base url for this plugin - """ + """Base url for this plugin""" return f'{PLUGIN_BASE}/{self.slug}/' @property def internal_name(self): - """ - Internal url pattern name - """ + """Internal url pattern name""" return f'plugin:{self.slug}:' @property def urlpatterns(self): - """ - Urlpatterns for this plugin - """ + """Urlpatterns for this plugin""" if self.has_urls: return re_path(f'^{self.slug}/', include((self.urls, self.slug)), name=self.slug) return None @property def has_urls(self): - """ - Does this plugin use custom urls - """ + """Does this plugin use custom urls""" return bool(self.urls) class NavigationMixin: - """ - Mixin that enables custom navigation links with the plugin - """ + """Mixin that enables custom navigation links with the plugin""" NAVIGATION_TAB_NAME = None NAVIGATION_TAB_ICON = "fas fa-question" class MixinMeta: - """ - Meta options for this mixin - """ + """Meta options for this mixin""" + MIXIN_NAME = 'Navigation Links' def __init__(self): @@ -312,9 +263,7 @@ class NavigationMixin: self.navigation = self.setup_navigation() 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 @@ -325,16 +274,12 @@ class NavigationMixin: @property def has_naviation(self): - """ - Does this plugin define navigation elements - """ + """Does this plugin define navigation elements""" return bool(self.navigation) @property def navigation_name(self): - """ - Name for navigation tab - """ + """Name for navigation tab""" name = getattr(self, 'NAVIGATION_TAB_NAME', None) if not name: name = self.human_name @@ -342,21 +287,16 @@ class NavigationMixin: @property def navigation_icon(self): - """ - Icon-name for navigation tab - """ + """Icon-name for navigation tab""" return getattr(self, 'NAVIGATION_TAB_ICON', "fas fa-question") class AppMixin: - """ - Mixin that enables full django app functions for a plugin - """ + """Mixin that enables full django app functions for a plugin""" class MixinMeta: - """m - Mta options for this mixin - """ + """Meta options for this mixin""" + MIXIN_NAME = 'App registration' def __init__(self): @@ -365,15 +305,12 @@ 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 class APICallMixin: - """ - Mixin that enables easier API calls for a plugin + """Mixin that enables easier API calls for a plugin Steps to set up: 1. Add this mixin before (left of) SettingsMixin and PluginBase @@ -424,7 +361,7 @@ class APICallMixin: API_TOKEN = 'Bearer' class MixinMeta: - """meta options for this mixin""" + """Meta options for this mixin""" MIXIN_NAME = 'API calls' def __init__(self): @@ -487,8 +424,7 @@ class APICallMixin: class PanelMixin: - """ - Mixin which allows integration of custom 'panels' into a particular page. + """Mixin which allows integration of custom 'panels' into a particular page. The mixin provides a number of key functionalities: @@ -540,17 +476,15 @@ class PanelMixin: self.add_mixin('panel', True, __class__) def get_custom_panels(self, view, request): - """ This method *must* be implemented by the plugin class """ + """This method *must* be implemented by the plugin class""" raise MixinNotImplementedError(f"{__class__} is missing the 'get_custom_panels' method") def get_panel_context(self, view, request, context): - """ - Build the context data to be used for template rendering. + """Build the context data to be used for template rendering. Custom class can override this to provide any custom context data. (See the example in "custom_panel_sample.py") """ - # Provide some standard context items to the template for rendering context['plugin'] = self context['request'] = request diff --git a/InvenTree/plugin/base/integration/test_mixins.py b/InvenTree/plugin/base/integration/test_mixins.py index e91f4da365..b3eb9f4418 100644 --- a/InvenTree/plugin/base/integration/test_mixins.py +++ b/InvenTree/plugin/base/integration/test_mixins.py @@ -1,4 +1,4 @@ -""" Unit tests for base mixins for plugins """ +"""Unit tests for base mixins for plugins""" from django.conf import settings from django.test import TestCase @@ -170,9 +170,7 @@ class APICallMixinTest(BaseMixinDefinition, TestCase): API_TOKEN_SETTING = 'API_TOKEN' def get_external_url(self, simple: bool = True): - ''' - returns data from the sample endpoint - ''' + """Returns data from the sample endpoint""" return self.api_call('api/users/2', simple_response=simple) self.mixin = MixinCls() @@ -263,7 +261,6 @@ class PanelMixinTests(InvenTreeTestCase): def test_installed(self): """Test that the sample panel plugin is installed""" - plugins = registry.with_mixin('panel') self.assertTrue(len(plugins) > 0) @@ -276,7 +273,6 @@ class PanelMixinTests(InvenTreeTestCase): def test_disabled(self): """Test that the panels *do not load* if the plugin is not enabled""" - plugin = registry.get_plugin('samplepanel') plugin.set_setting('ENABLE_HELLO_WORLD', True) @@ -308,7 +304,6 @@ class PanelMixinTests(InvenTreeTestCase): """ Test that the panels *do* load if the plugin is enabled """ - plugin = registry.get_plugin('samplepanel') self.assertEqual(len(registry.with_mixin('panel', active=True)), 0) @@ -383,7 +378,6 @@ class PanelMixinTests(InvenTreeTestCase): def test_mixin(self): """Test that ImplementationError is raised""" - with self.assertRaises(MixinNotImplementedError): class Wrong(PanelMixin, InvenTreePlugin): pass diff --git a/InvenTree/plugin/base/label/label.py b/InvenTree/plugin/base/label/label.py index d85251dd81..f02420bf61 100644 --- a/InvenTree/plugin/base/label/label.py +++ b/InvenTree/plugin/base/label/label.py @@ -1,4 +1,5 @@ """Functions to print a label to a mixin printer""" + import logging from django.utils.translation import gettext_lazy as _ @@ -10,8 +11,7 @@ logger = logging.getLogger('inventree') def print_label(plugin_slug, label_image, label_instance=None, user=None): - """ - Print label with the provided plugin. + """Print label with the provided plugin. This task is nominally handled by the background worker. @@ -21,7 +21,6 @@ def print_label(plugin_slug, label_image, label_instance=None, user=None): plugin_slug: The unique slug (key) of the plugin label_image: A PIL.Image image object to be printed """ - logger.info(f"Plugin '{plugin_slug}' is printing a label") plugin = registry.plugins.get(plugin_slug, None)