From eae80f667dfa3cf1c144d5f4d18afb57517edf3e Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 12:25:17 +0200 Subject: [PATCH 001/135] remove all functional code from /plugins --- InvenTree/plugins/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/InvenTree/plugins/__init__.py b/InvenTree/plugins/__init__.py index ea758ff8c5..8113ffbecf 100644 --- a/InvenTree/plugins/__init__.py +++ b/InvenTree/plugins/__init__.py @@ -1,2 +1,5 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals # pragma: no cover +""" +Directory for custom plugin development + +Please read the docs for more information https://inventree.readthedocs.io/en/latest/extend/plugins/#local-directory +""" \ No newline at end of file From 87fa2d2067987bc901d85cecc4741fb3eafed411 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 12:28:15 +0200 Subject: [PATCH 002/135] pep fix --- InvenTree/plugins/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/plugins/__init__.py b/InvenTree/plugins/__init__.py index 8113ffbecf..926e30e23c 100644 --- a/InvenTree/plugins/__init__.py +++ b/InvenTree/plugins/__init__.py @@ -2,4 +2,4 @@ Directory for custom plugin development Please read the docs for more information https://inventree.readthedocs.io/en/latest/extend/plugins/#local-directory -""" \ No newline at end of file +""" From b28f71374e6f7b6b12410abd0458e7d9011575c0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 12:35:42 +0200 Subject: [PATCH 003/135] move barcodes --- InvenTree/InvenTree/urls.py | 2 +- InvenTree/plugin/{ => base/barcodes}/barcode.py | 2 +- InvenTree/plugin/{builtin => base}/barcodes/mixins.py | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename InvenTree/plugin/{ => base/barcodes}/barcode.py (99%) rename InvenTree/plugin/{builtin => base}/barcodes/mixins.py (100%) diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index d408bd7346..9c89422a18 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -27,7 +27,7 @@ from order.api import order_api_urls from label.api import label_api_urls from report.api import report_api_urls from plugin.api import plugin_api_urls -from plugin.barcode import barcode_api_urls +from plugin.base.barcodes.barcode import barcode_api_urls from django.conf import settings from django.conf.urls.static import static diff --git a/InvenTree/plugin/barcode.py b/InvenTree/plugin/base/barcodes/barcode.py similarity index 99% rename from InvenTree/plugin/barcode.py rename to InvenTree/plugin/base/barcodes/barcode.py index 98b111f073..21a142f841 100644 --- a/InvenTree/plugin/barcode.py +++ b/InvenTree/plugin/base/barcodes/barcode.py @@ -11,7 +11,7 @@ from stock.models import StockItem from stock.serializers import StockItemSerializer from plugin.builtin.barcodes.inventree_barcode import InvenTreeBarcodePlugin -from plugin.builtin.barcodes.mixins import hash_barcode +from plugin.base.barcodes.mixins import hash_barcode from plugin import registry diff --git a/InvenTree/plugin/builtin/barcodes/mixins.py b/InvenTree/plugin/base/barcodes/mixins.py similarity index 100% rename from InvenTree/plugin/builtin/barcodes/mixins.py rename to InvenTree/plugin/base/barcodes/mixins.py From b030c424805804a11215ba927a6d604439ba653c Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 12:36:54 +0200 Subject: [PATCH 004/135] move inegration mixins --- InvenTree/plugin/{builtin => base}/integration/mixins.py | 0 InvenTree/plugin/mixins/__init__.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename InvenTree/plugin/{builtin => base}/integration/mixins.py (100%) diff --git a/InvenTree/plugin/builtin/integration/mixins.py b/InvenTree/plugin/base/integration/mixins.py similarity index 100% rename from InvenTree/plugin/builtin/integration/mixins.py rename to InvenTree/plugin/base/integration/mixins.py diff --git a/InvenTree/plugin/mixins/__init__.py b/InvenTree/plugin/mixins/__init__.py index 97d27b48fc..1f75e1200c 100644 --- a/InvenTree/plugin/mixins/__init__.py +++ b/InvenTree/plugin/mixins/__init__.py @@ -2,7 +2,7 @@ Utility class to enable simpler imports """ -from ..builtin.integration.mixins import APICallMixin, AppMixin, LabelPrintingMixin, SettingsMixin, EventMixin, ScheduleMixin, UrlsMixin, NavigationMixin, PanelMixin +from ..base.integration.mixins import APICallMixin, AppMixin, LabelPrintingMixin, SettingsMixin, EventMixin, ScheduleMixin, UrlsMixin, NavigationMixin, PanelMixin from common.notifications import SingleNotificationMethod, BulkNotificationMethod From 308dd53c3512580113b1099303a9a931db6046ae Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 12:37:22 +0200 Subject: [PATCH 005/135] rename tests --- .../action/{test_samples_action.py => test_simpleactionplugin.py} | 0 .../integration/{test_samples_integration.py => test_sample.py} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename InvenTree/plugin/builtin/action/{test_samples_action.py => test_simpleactionplugin.py} (100%) rename InvenTree/plugin/samples/integration/{test_samples_integration.py => test_sample.py} (100%) diff --git a/InvenTree/plugin/builtin/action/test_samples_action.py b/InvenTree/plugin/builtin/action/test_simpleactionplugin.py similarity index 100% rename from InvenTree/plugin/builtin/action/test_samples_action.py rename to InvenTree/plugin/builtin/action/test_simpleactionplugin.py diff --git a/InvenTree/plugin/samples/integration/test_samples_integration.py b/InvenTree/plugin/samples/integration/test_sample.py similarity index 100% rename from InvenTree/plugin/samples/integration/test_samples_integration.py rename to InvenTree/plugin/samples/integration/test_sample.py From 0d89959d494c210d33183f207ecca3a5a4b66cee Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 12:38:14 +0200 Subject: [PATCH 006/135] move action --- InvenTree/plugin/__init__.py | 2 +- InvenTree/plugin/{ => base/action}/action.py | 2 +- InvenTree/plugin/{builtin => base}/action/mixins.py | 0 InvenTree/plugin/builtin/action/simpleactionplugin.py | 2 +- InvenTree/plugin/mixins/__init__.py | 2 +- InvenTree/plugin/test_action.py | 2 +- 6 files changed, 5 insertions(+), 5 deletions(-) rename InvenTree/plugin/{ => base/action}/action.py (91%) rename InvenTree/plugin/{builtin => base}/action/mixins.py (100%) diff --git a/InvenTree/plugin/__init__.py b/InvenTree/plugin/__init__.py index b8e40e4271..32bababe2c 100644 --- a/InvenTree/plugin/__init__.py +++ b/InvenTree/plugin/__init__.py @@ -5,7 +5,7 @@ Utility file to enable simper imports from .registry import registry from .plugin import InvenTreePluginBase from .integration import IntegrationPluginBase -from .action import ActionPlugin +from .base.action.action import ActionPlugin from .helpers import MixinNotImplementedError, MixinImplementationError diff --git a/InvenTree/plugin/action.py b/InvenTree/plugin/base/action/action.py similarity index 91% rename from InvenTree/plugin/action.py rename to InvenTree/plugin/base/action/action.py index 9586355aea..fd43848cc2 100644 --- a/InvenTree/plugin/action.py +++ b/InvenTree/plugin/base/action/action.py @@ -4,7 +4,7 @@ import logging import warnings -from plugin.builtin.action.mixins import ActionMixin +from plugin.base.action.mixins import ActionMixin import plugin.integration diff --git a/InvenTree/plugin/builtin/action/mixins.py b/InvenTree/plugin/base/action/mixins.py similarity index 100% rename from InvenTree/plugin/builtin/action/mixins.py rename to InvenTree/plugin/base/action/mixins.py diff --git a/InvenTree/plugin/builtin/action/simpleactionplugin.py b/InvenTree/plugin/builtin/action/simpleactionplugin.py index 01a0829887..b7acd2d5f6 100644 --- a/InvenTree/plugin/builtin/action/simpleactionplugin.py +++ b/InvenTree/plugin/builtin/action/simpleactionplugin.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """sample implementation for ActionPlugin""" -from plugin.action import ActionPlugin +from plugin.base.action.action import ActionPlugin class SimpleActionPlugin(ActionPlugin): diff --git a/InvenTree/plugin/mixins/__init__.py b/InvenTree/plugin/mixins/__init__.py index 1f75e1200c..5b959e317c 100644 --- a/InvenTree/plugin/mixins/__init__.py +++ b/InvenTree/plugin/mixins/__init__.py @@ -6,7 +6,7 @@ from ..base.integration.mixins import APICallMixin, AppMixin, LabelPrintingMixin from common.notifications import SingleNotificationMethod, BulkNotificationMethod -from ..builtin.action.mixins import ActionMixin +from ..base.action.mixins import ActionMixin from ..builtin.barcodes.mixins import BarcodeMixin __all__ = [ diff --git a/InvenTree/plugin/test_action.py b/InvenTree/plugin/test_action.py index 2a9e4a9a37..299833ccd1 100644 --- a/InvenTree/plugin/test_action.py +++ b/InvenTree/plugin/test_action.py @@ -2,7 +2,7 @@ from django.test import TestCase -from plugin.action import ActionPlugin +from plugin.base.action.action import ActionPlugin class ActionPluginTests(TestCase): From 34467003ad6ff2e70ce77a74e0ce79a6543f9b91 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 12:38:37 +0200 Subject: [PATCH 007/135] fix bacodeMixin base --- InvenTree/plugin/mixins/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/plugin/mixins/__init__.py b/InvenTree/plugin/mixins/__init__.py index 5b959e317c..af20a05491 100644 --- a/InvenTree/plugin/mixins/__init__.py +++ b/InvenTree/plugin/mixins/__init__.py @@ -7,7 +7,7 @@ from ..base.integration.mixins import APICallMixin, AppMixin, LabelPrintingMixin from common.notifications import SingleNotificationMethod, BulkNotificationMethod from ..base.action.mixins import ActionMixin -from ..builtin.barcodes.mixins import BarcodeMixin +from ..base.barcodes.mixins import BarcodeMixin __all__ = [ 'APICallMixin', From 87ef2a338282988593c7d9c21813ad7fcee006d1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 12:41:08 +0200 Subject: [PATCH 008/135] remove old code --- InvenTree/plugin/plugin.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/InvenTree/plugin/plugin.py b/InvenTree/plugin/plugin.py index c6af7f20d8..005beacb0c 100644 --- a/InvenTree/plugin/plugin.py +++ b/InvenTree/plugin/plugin.py @@ -82,12 +82,3 @@ class InvenTreePluginBase(): else: return False # pragma: no cover - -# 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): # pragma: no cover - warnings.warn("Using the InvenTreePlugin is depreceated", DeprecationWarning) - super().__init__() From 18ad87433ed93bd815768e8b6808b3652fa1ea10 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 12:45:25 +0200 Subject: [PATCH 009/135] move plugin and integration together --- InvenTree/plugin/integration.py | 261 -------------------------------- InvenTree/plugin/plugin.py | 256 ++++++++++++++++++++++++++++++- 2 files changed, 254 insertions(+), 263 deletions(-) delete mode 100644 InvenTree/plugin/integration.py diff --git a/InvenTree/plugin/integration.py b/InvenTree/plugin/integration.py deleted file mode 100644 index c622c0402c..0000000000 --- a/InvenTree/plugin/integration.py +++ /dev/null @@ -1,261 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Class for IntegrationPluginBase and Mixin Base -""" - -import logging -import os -import inspect -from datetime import datetime -import pathlib - -from django.urls.base import reverse -from django.conf import settings -from django.utils.translation import gettext_lazy as _ - -import plugin.plugin as plugin_base -from plugin.helpers import get_git_log, GitStatus - - -logger = logging.getLogger("inventree") - - -class MixinBase: - """ - Base set of mixin functions and mechanisms - """ - - def __init__(self) -> None: - self._mixinreg = {} - self._mixins = {} - - def add_mixin(self, key: str, fnc_enabled=True, cls=None): - """ - Add a mixin to the plugins registry - """ - - self._mixins[key] = fnc_enabled - self.setup_mixin(key, cls=cls) - - def setup_mixin(self, key, cls=None): - """ - Define mixin details for the current mixin -> provides meta details for all active mixins - """ - - # get human name - human_name = getattr(cls.MixinMeta, 'MIXIN_NAME', key) if cls and hasattr(cls, 'MixinMeta') else key - - # register - self._mixinreg[key] = { - 'key': key, - 'human_name': human_name, - } - - @property - def registered_mixins(self, with_base: bool = False): - """ - Get all registered mixins for the plugin - """ - - mixins = getattr(self, '_mixinreg', None) - if mixins: - # filter out base - if not with_base and 'base' in mixins: - del mixins['base'] - # only return dict - mixins = [a for a in mixins.values()] - return mixins - - -class IntegrationPluginBase(MixinBase, plugin_base.InvenTreePluginBase): - """ - The IntegrationPluginBase class is used to integrate with 3rd party software - """ - - AUTHOR = None - DESCRIPTION = None - PUBLISH_DATE = None - VERSION = None - WEBSITE = None - LICENSE = None - - def __init__(self): - super().__init__() - self.add_mixin('base') - self.def_path = inspect.getfile(self.__class__) - self.path = os.path.dirname(self.def_path) - - self.define_package() - - @property - def _is_package(self): - """ - Is the plugin delivered as a package - """ - return getattr(self, 'is_package', False) - - @property - def is_sample(self): - """ - Is this plugin part of the samples? - """ - path = str(self.package_path) - return path.startswith('plugin/samples/') - - # 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 of plugin - """ - return self.plugin_title() - - @property - def description(self): - """ - Description of plugin - """ - description = getattr(self, 'DESCRIPTION', None) - if not description: - description = self.plugin_name() - return description - - @property - def author(self): - """ - Author of plugin - either from plugin settings or git - """ - author = getattr(self, 'AUTHOR', None) - if not author: - author = self.package.get('author') - if not author: - author = _('No author found') # pragma: no cover - return author - - @property - def pub_date(self): - """ - 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') - else: - pub_date = datetime.fromisoformat(str(pub_date)) - if not pub_date: - pub_date = _('No date found') # pragma: no cover - return pub_date - - @property - def version(self): - """ - Version of plugin - """ - version = getattr(self, 'VERSION', None) - return version - - @property - def website(self): - """ - Website of plugin - if set else None - """ - website = getattr(self, 'WEBSITE', None) - return website - - @property - def license(self): - """ - License of plugin - """ - lic = getattr(self, 'LICENSE', None) - return lic - # endregion - - @property - def package_path(self): - """ - Path to the plugin - """ - if self._is_package: - return self.__module__ # pragma: no cover - return pathlib.Path(self.def_path).relative_to(settings.BASE_DIR) - - @property - def settings_url(self): - """ - 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 - """ - return key in self._mixins - - def mixin_enabled(self, key): - """ - Check if mixin is registered, enabled and ready - """ - if self.mixin(key): - fnc_name = self._mixins.get(key) - - # Allow for simple case where the mixin is "always" ready - if fnc_name is True: - return True - - return getattr(self, fnc_name, True) - return False - # endregion - - # region package info - 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 - """ - return {} # pragma: no cover # TODO add usage for package metadata - - 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'): - package['date'] = datetime.fromisoformat(package.get('date')) - - # process sign state - sign_state = getattr(GitStatus, str(package.get('verified')), GitStatus.N) - if sign_state.status == 0: - self.sign_color = 'success' # pragma: no cover - elif sign_state.status == 1: - self.sign_color = 'warning' - else: - self.sign_color = 'danger' # pragma: no cover - - # set variables - self.package = package - self.sign_state = sign_state - # endregion diff --git a/InvenTree/plugin/plugin.py b/InvenTree/plugin/plugin.py index 005beacb0c..6a1a147b5f 100644 --- a/InvenTree/plugin/plugin.py +++ b/InvenTree/plugin/plugin.py @@ -2,13 +2,25 @@ """ Base Class for InvenTree plugins """ -import warnings +import logging +import os +import inspect +from datetime import datetime +import pathlib +from django.conf import settings from django.db.utils import OperationalError, ProgrammingError from django.utils.text import slugify +from django.utils.translation import gettext_lazy as _ +from django.urls.base import reverse + +from plugin.helpers import get_git_log, GitStatus -class InvenTreePluginBase(): +logger = logging.getLogger("inventree") + + +class InvenTreePluginBase: """ Base class for a plugin DO NOT USE THIS DIRECTLY, USE plugin.IntegrationPluginBase @@ -82,3 +94,243 @@ class InvenTreePluginBase(): else: return False # pragma: no cover + +class MixinBase: + """ + Base set of mixin functions and mechanisms + """ + + def __init__(self) -> None: + self._mixinreg = {} + self._mixins = {} + + def add_mixin(self, key: str, fnc_enabled=True, cls=None): + """ + Add a mixin to the plugins registry + """ + + self._mixins[key] = fnc_enabled + self.setup_mixin(key, cls=cls) + + def setup_mixin(self, key, cls=None): + """ + Define mixin details for the current mixin -> provides meta details for all active mixins + """ + + # get human name + human_name = getattr(cls.MixinMeta, 'MIXIN_NAME', key) if cls and hasattr(cls, 'MixinMeta') else key + + # register + self._mixinreg[key] = { + 'key': key, + 'human_name': human_name, + } + + @property + def registered_mixins(self, with_base: bool = False): + """ + Get all registered mixins for the plugin + """ + + mixins = getattr(self, '_mixinreg', None) + if mixins: + # filter out base + if not with_base and 'base' in mixins: + del mixins['base'] + # only return dict + mixins = [a for a in mixins.values()] + return mixins + + +class IntegrationPluginBase(MixinBase, InvenTreePluginBase): + """ + The IntegrationPluginBase class is used to integrate with 3rd party software + """ + + AUTHOR = None + DESCRIPTION = None + PUBLISH_DATE = None + VERSION = None + WEBSITE = None + LICENSE = None + + def __init__(self): + super().__init__() + self.add_mixin('base') + self.def_path = inspect.getfile(self.__class__) + self.path = os.path.dirname(self.def_path) + + self.define_package() + + @property + def _is_package(self): + """ + Is the plugin delivered as a package + """ + return getattr(self, 'is_package', False) + + @property + def is_sample(self): + """ + Is this plugin part of the samples? + """ + path = str(self.package_path) + return path.startswith('plugin/samples/') + + # 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 of plugin + """ + return self.plugin_title() + + @property + def description(self): + """ + Description of plugin + """ + description = getattr(self, 'DESCRIPTION', None) + if not description: + description = self.plugin_name() + return description + + @property + def author(self): + """ + Author of plugin - either from plugin settings or git + """ + author = getattr(self, 'AUTHOR', None) + if not author: + author = self.package.get('author') + if not author: + author = _('No author found') # pragma: no cover + return author + + @property + def pub_date(self): + """ + 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') + else: + pub_date = datetime.fromisoformat(str(pub_date)) + if not pub_date: + pub_date = _('No date found') # pragma: no cover + return pub_date + + @property + def version(self): + """ + Version of plugin + """ + version = getattr(self, 'VERSION', None) + return version + + @property + def website(self): + """ + Website of plugin - if set else None + """ + website = getattr(self, 'WEBSITE', None) + return website + + @property + def license(self): + """ + License of plugin + """ + lic = getattr(self, 'LICENSE', None) + return lic + # endregion + + @property + def package_path(self): + """ + Path to the plugin + """ + if self._is_package: + return self.__module__ # pragma: no cover + return pathlib.Path(self.def_path).relative_to(settings.BASE_DIR) + + @property + def settings_url(self): + """ + 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 + """ + return key in self._mixins + + def mixin_enabled(self, key): + """ + Check if mixin is registered, enabled and ready + """ + if self.mixin(key): + fnc_name = self._mixins.get(key) + + # Allow for simple case where the mixin is "always" ready + if fnc_name is True: + return True + + return getattr(self, fnc_name, True) + return False + # endregion + + # region package info + 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 + """ + return {} # pragma: no cover # TODO add usage for package metadata + + 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'): + package['date'] = datetime.fromisoformat(package.get('date')) + + # process sign state + sign_state = getattr(GitStatus, str(package.get('verified')), GitStatus.N) + if sign_state.status == 0: + self.sign_color = 'success' # pragma: no cover + elif sign_state.status == 1: + self.sign_color = 'warning' + else: + self.sign_color = 'danger' # pragma: no cover + + # set variables + self.package = package + self.sign_state = sign_state + # endregion From 1da12fe5ff4345a1cb2b475d81ee4771b3009c0e Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 12:45:40 +0200 Subject: [PATCH 010/135] add import comment --- InvenTree/plugin/plugin.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/InvenTree/plugin/plugin.py b/InvenTree/plugin/plugin.py index 6a1a147b5f..0f6b5d9675 100644 --- a/InvenTree/plugin/plugin.py +++ b/InvenTree/plugin/plugin.py @@ -145,6 +145,8 @@ class MixinBase: class IntegrationPluginBase(MixinBase, InvenTreePluginBase): """ The IntegrationPluginBase class is used to integrate with 3rd party software + + DO NOT USE THIS DIRECTLY, USE plugin.IntegrationPluginBase """ AUTHOR = None From 4ae5ed36a7d97ab6895d75772f8709e4e789e741 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 12:53:31 +0200 Subject: [PATCH 011/135] fix imports --- InvenTree/plugin/__init__.py | 3 +-- InvenTree/plugin/base/action/action.py | 4 ++-- InvenTree/plugin/registry.py | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/InvenTree/plugin/__init__.py b/InvenTree/plugin/__init__.py index 32bababe2c..35a6687628 100644 --- a/InvenTree/plugin/__init__.py +++ b/InvenTree/plugin/__init__.py @@ -3,8 +3,7 @@ Utility file to enable simper imports """ from .registry import registry -from .plugin import InvenTreePluginBase -from .integration import IntegrationPluginBase +from .plugin import InvenTreePluginBase, IntegrationPluginBase from .base.action.action import ActionPlugin from .helpers import MixinNotImplementedError, MixinImplementationError diff --git a/InvenTree/plugin/base/action/action.py b/InvenTree/plugin/base/action/action.py index fd43848cc2..f012c45116 100644 --- a/InvenTree/plugin/base/action/action.py +++ b/InvenTree/plugin/base/action/action.py @@ -5,13 +5,13 @@ import logging import warnings from plugin.base.action.mixins import ActionMixin -import plugin.integration +import plugin.plugin logger = logging.getLogger("inventree") -class ActionPlugin(ActionMixin, plugin.integration.IntegrationPluginBase): +class ActionPlugin(ActionMixin, plugin.plugin.IntegrationPluginBase): """ Legacy action definition - will be replaced Please use the new Integration Plugin API and the Action mixin diff --git a/InvenTree/plugin/registry.py b/InvenTree/plugin/registry.py index 9a007307d3..985d89a7a4 100644 --- a/InvenTree/plugin/registry.py +++ b/InvenTree/plugin/registry.py @@ -31,7 +31,7 @@ except: # pragma: no cover from maintenance_mode.core import maintenance_mode_on from maintenance_mode.core import get_maintenance_mode, set_maintenance_mode -from .integration import IntegrationPluginBase +from .plugin import IntegrationPluginBase from .helpers import handle_error, log_error, get_plugins, IntegrationPluginError From 934b795f9b8489da4a4b9385d2fba2a89f710e06 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 12:54:01 +0200 Subject: [PATCH 012/135] split tests to functions tests --- .../{test_integration.py => test_mixins.py} | 78 +------------------ InvenTree/plugin/test_plugin.py | 76 ++++++++++++++++++ 2 files changed, 77 insertions(+), 77 deletions(-) rename InvenTree/plugin/{test_integration.py => test_mixins.py} (72%) diff --git a/InvenTree/plugin/test_integration.py b/InvenTree/plugin/test_mixins.py similarity index 72% rename from InvenTree/plugin/test_integration.py rename to InvenTree/plugin/test_mixins.py index 5b211e2d96..4fb97fe20f 100644 --- a/InvenTree/plugin/test_integration.py +++ b/InvenTree/plugin/test_mixins.py @@ -1,18 +1,14 @@ -""" Unit tests for integration plugins """ +""" Unit tests for base mixins for plugins """ from django.test import TestCase from django.conf import settings from django.urls import include, re_path from django.contrib.auth import get_user_model -from datetime import datetime - from plugin import IntegrationPluginBase from plugin.mixins import AppMixin, SettingsMixin, UrlsMixin, NavigationMixin, APICallMixin from plugin.urls import PLUGIN_BASE -from plugin.samples.integration.sample import SampleIntegrationPlugin - class BaseMixinDefinition: def test_mixin_name(self): @@ -216,75 +212,3 @@ class APICallMixinTest(BaseMixinDefinition, TestCase): with self.assertRaises(ValueError): self.mixin_wrong.has_api_call() - -class IntegrationPluginBaseTests(TestCase): - """ Tests for IntegrationPluginBase """ - - def setUp(self): - self.plugin = IntegrationPluginBase() - - class SimpeIntegrationPluginBase(IntegrationPluginBase): - PLUGIN_NAME = 'SimplePlugin' - - self.plugin_simple = SimpeIntegrationPluginBase() - - class NameIntegrationPluginBase(IntegrationPluginBase): - PLUGIN_NAME = 'Aplugin' - PLUGIN_SLUG = 'a' - PLUGIN_TITLE = 'a titel' - PUBLISH_DATE = "1111-11-11" - AUTHOR = 'AA BB' - DESCRIPTION = 'A description' - VERSION = '1.2.3a' - WEBSITE = 'http://aa.bb/cc' - LICENSE = 'MIT' - - self.plugin_name = NameIntegrationPluginBase() - self.plugin_sample = SampleIntegrationPlugin() - - def test_action_name(self): - """check the name definition possibilities""" - # plugin_name - self.assertEqual(self.plugin.plugin_name(), '') - self.assertEqual(self.plugin_simple.plugin_name(), 'SimplePlugin') - self.assertEqual(self.plugin_name.plugin_name(), 'Aplugin') - - # is_sampe - self.assertEqual(self.plugin.is_sample, False) - self.assertEqual(self.plugin_sample.is_sample, True) - - # slug - self.assertEqual(self.plugin.slug, '') - self.assertEqual(self.plugin_simple.slug, 'simpleplugin') - self.assertEqual(self.plugin_name.slug, 'a') - - # human_name - self.assertEqual(self.plugin.human_name, '') - self.assertEqual(self.plugin_simple.human_name, 'SimplePlugin') - self.assertEqual(self.plugin_name.human_name, 'a titel') - - # description - self.assertEqual(self.plugin.description, '') - self.assertEqual(self.plugin_simple.description, 'SimplePlugin') - self.assertEqual(self.plugin_name.description, 'A description') - - # author - self.assertEqual(self.plugin_name.author, 'AA BB') - - # pub_date - self.assertEqual(self.plugin_name.pub_date, datetime(1111, 11, 11, 0, 0)) - - # version - self.assertEqual(self.plugin.version, None) - self.assertEqual(self.plugin_simple.version, None) - self.assertEqual(self.plugin_name.version, '1.2.3a') - - # website - self.assertEqual(self.plugin.website, None) - self.assertEqual(self.plugin_simple.website, None) - self.assertEqual(self.plugin_name.website, 'http://aa.bb/cc') - - # license - self.assertEqual(self.plugin.license, None) - self.assertEqual(self.plugin_simple.license, None) - self.assertEqual(self.plugin_name.license, 'MIT') diff --git a/InvenTree/plugin/test_plugin.py b/InvenTree/plugin/test_plugin.py index c0835c2fb3..eee3230d2a 100644 --- a/InvenTree/plugin/test_plugin.py +++ b/InvenTree/plugin/test_plugin.py @@ -2,12 +2,15 @@ Unit tests for plugins """ +from datetime import datetime + from django.test import TestCase from plugin.samples.integration.sample import SampleIntegrationPlugin from plugin.samples.integration.another_sample import WrongIntegrationPlugin, NoIntegrationPlugin import plugin.templatetags.plugin_extras as plugin_tags from plugin import registry, InvenTreePluginBase +from plugin.plugin import IntegrationPluginBase class InvenTreePluginTests(TestCase): @@ -79,3 +82,76 @@ class PluginTagTests(TestCase): def test_tag_plugin_errors(self): """test that all errors are listed""" self.assertEqual(plugin_tags.plugin_errors(), registry.errors) + + +class IntegrationPluginBaseTests(TestCase): + """ Tests for IntegrationPluginBase """ + + def setUp(self): + self.plugin = IntegrationPluginBase() + + class SimpeIntegrationPluginBase(IntegrationPluginBase): + PLUGIN_NAME = 'SimplePlugin' + + self.plugin_simple = SimpeIntegrationPluginBase() + + class NameIntegrationPluginBase(IntegrationPluginBase): + PLUGIN_NAME = 'Aplugin' + PLUGIN_SLUG = 'a' + PLUGIN_TITLE = 'a titel' + PUBLISH_DATE = "1111-11-11" + AUTHOR = 'AA BB' + DESCRIPTION = 'A description' + VERSION = '1.2.3a' + WEBSITE = 'http://aa.bb/cc' + LICENSE = 'MIT' + + self.plugin_name = NameIntegrationPluginBase() + self.plugin_sample = SampleIntegrationPlugin() + + def test_action_name(self): + """check the name definition possibilities""" + # plugin_name + self.assertEqual(self.plugin.plugin_name(), '') + self.assertEqual(self.plugin_simple.plugin_name(), 'SimplePlugin') + self.assertEqual(self.plugin_name.plugin_name(), 'Aplugin') + + # is_sampe + self.assertEqual(self.plugin.is_sample, False) + self.assertEqual(self.plugin_sample.is_sample, True) + + # slug + self.assertEqual(self.plugin.slug, '') + self.assertEqual(self.plugin_simple.slug, 'simpleplugin') + self.assertEqual(self.plugin_name.slug, 'a') + + # human_name + self.assertEqual(self.plugin.human_name, '') + self.assertEqual(self.plugin_simple.human_name, 'SimplePlugin') + self.assertEqual(self.plugin_name.human_name, 'a titel') + + # description + self.assertEqual(self.plugin.description, '') + self.assertEqual(self.plugin_simple.description, 'SimplePlugin') + self.assertEqual(self.plugin_name.description, 'A description') + + # author + self.assertEqual(self.plugin_name.author, 'AA BB') + + # pub_date + self.assertEqual(self.plugin_name.pub_date, datetime(1111, 11, 11, 0, 0)) + + # version + self.assertEqual(self.plugin.version, None) + self.assertEqual(self.plugin_simple.version, None) + self.assertEqual(self.plugin_name.version, '1.2.3a') + + # website + self.assertEqual(self.plugin.website, None) + self.assertEqual(self.plugin_simple.website, None) + self.assertEqual(self.plugin_name.website, 'http://aa.bb/cc') + + # license + self.assertEqual(self.plugin.license, None) + self.assertEqual(self.plugin_simple.license, None) + self.assertEqual(self.plugin_name.license, 'MIT') From 16a232870268df627b27ba78c1f638474d20eab9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 12:55:04 +0200 Subject: [PATCH 013/135] move tests to their functions --- InvenTree/plugin/{ => base/action}/test_action.py | 0 InvenTree/plugin/{ => base/barcodes}/test_barcode.py | 0 InvenTree/plugin/{ => base/integration}/test_mixins.py | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename InvenTree/plugin/{ => base/action}/test_action.py (100%) rename InvenTree/plugin/{ => base/barcodes}/test_barcode.py (100%) rename InvenTree/plugin/{ => base/integration}/test_mixins.py (100%) diff --git a/InvenTree/plugin/test_action.py b/InvenTree/plugin/base/action/test_action.py similarity index 100% rename from InvenTree/plugin/test_action.py rename to InvenTree/plugin/base/action/test_action.py diff --git a/InvenTree/plugin/test_barcode.py b/InvenTree/plugin/base/barcodes/test_barcode.py similarity index 100% rename from InvenTree/plugin/test_barcode.py rename to InvenTree/plugin/base/barcodes/test_barcode.py diff --git a/InvenTree/plugin/test_mixins.py b/InvenTree/plugin/base/integration/test_mixins.py similarity index 100% rename from InvenTree/plugin/test_mixins.py rename to InvenTree/plugin/base/integration/test_mixins.py From 499f37a97aacc679c3a989bf5e0c96f95d563be0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 12:55:52 +0200 Subject: [PATCH 014/135] PEP fix --- InvenTree/plugin/base/integration/test_mixins.py | 1 - 1 file changed, 1 deletion(-) diff --git a/InvenTree/plugin/base/integration/test_mixins.py b/InvenTree/plugin/base/integration/test_mixins.py index 4fb97fe20f..b611454d1b 100644 --- a/InvenTree/plugin/base/integration/test_mixins.py +++ b/InvenTree/plugin/base/integration/test_mixins.py @@ -211,4 +211,3 @@ class APICallMixinTest(BaseMixinDefinition, TestCase): # cover wrong token setting with self.assertRaises(ValueError): self.mixin_wrong.has_api_call() - From 51cca7a13c550bfae6d84178ffa2169c56e2bfe9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 13:10:29 +0200 Subject: [PATCH 015/135] move apis to their respective bases --- InvenTree/InvenTree/api.py | 41 ----------------- InvenTree/InvenTree/urls.py | 13 +----- InvenTree/plugin/api.py | 13 ++++++ InvenTree/plugin/base/action/api.py | 45 +++++++++++++++++++ .../base/barcodes/{barcode.py => api.py} | 2 +- InvenTree/plugin/urls.py | 1 + 6 files changed, 62 insertions(+), 53 deletions(-) create mode 100644 InvenTree/plugin/base/action/api.py rename InvenTree/plugin/base/barcodes/{barcode.py => api.py} (99%) diff --git a/InvenTree/InvenTree/api.py b/InvenTree/InvenTree/api.py index 171fe414d2..a8b2903a90 100644 --- a/InvenTree/InvenTree/api.py +++ b/InvenTree/InvenTree/api.py @@ -13,15 +13,11 @@ from django_filters.rest_framework import DjangoFilterBackend from rest_framework import filters from rest_framework import permissions -from rest_framework.response import Response -from rest_framework.views import APIView from .views import AjaxView from .version import inventreeVersion, inventreeApiVersion, inventreeInstanceName from .status import is_worker_running -from plugin import registry - class InfoView(AjaxView): """ Simple JSON endpoint for InvenTree information. @@ -81,40 +77,3 @@ class AttachmentMixin: attachment = serializer.save() attachment.user = self.request.user attachment.save() - - -class ActionPluginView(APIView): - """ - Endpoint for running custom action plugins. - """ - - permission_classes = [ - permissions.IsAuthenticated, - ] - - def post(self, request, *args, **kwargs): - - action = request.data.get('action', None) - - data = request.data.get('data', None) - - if action is None: - return Response({ - 'error': _("No action specified") - }) - - 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() - - return Response(plugin.get_response()) - - # If we got to here, no matching action was found - return Response({ - 'error': _("No matching action found"), - "action": action, - }) diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index 9c89422a18..591a482450 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -27,7 +27,6 @@ from order.api import order_api_urls from label.api import label_api_urls from report.api import report_api_urls from plugin.api import plugin_api_urls -from plugin.base.barcodes.barcode import barcode_api_urls from django.conf import settings from django.conf.urls.static import static @@ -45,20 +44,13 @@ from .views import DynamicJsView from .views import NotificationsView from .api import InfoView, NotFoundView -from .api import ActionPluginView from users.api import user_urls admin.site.site_header = "InvenTree Admin" -apipatterns = [] -if settings.PLUGINS_ENABLED: - apipatterns.append( - re_path(r'^plugin/', include(plugin_api_urls)) - ) - -apipatterns += [ +apipatterns = [ re_path(r'^settings/', include(settings_api_urls)), re_path(r'^part/', include(part_api_urls)), re_path(r'^bom/', include(bom_api_urls)), @@ -73,8 +65,7 @@ apipatterns += [ re_path(r'^user/', include(user_urls)), # Plugin endpoints - re_path(r'^action/', ActionPluginView.as_view(), name='api-action-plugin'), - re_path(r'^barcode/', include(barcode_api_urls)), + path('', include(plugin_api_urls)), # Webhook enpoint path('', include(common_api_urls)), diff --git a/InvenTree/plugin/api.py b/InvenTree/plugin/api.py index b9fd6e643d..64c6f43d57 100644 --- a/InvenTree/plugin/api.py +++ b/InvenTree/plugin/api.py @@ -5,6 +5,7 @@ JSON API for the plugin app # -*- coding: utf-8 -*- from __future__ import unicode_literals +from django.conf import settings from django.urls import include, re_path from rest_framework import generics @@ -16,6 +17,8 @@ from rest_framework.response import Response from django_filters.rest_framework import DjangoFilterBackend from common.api import GlobalSettingsPermissions +from plugin.base.barcodes.api import barcode_api_urls +from plugin.base.action.api import ActionPluginView from plugin.models import PluginConfig, PluginSetting import plugin.serializers as PluginSerializers from plugin.registry import registry @@ -157,6 +160,11 @@ class PluginSettingDetail(generics.RetrieveUpdateAPIView): plugin_api_urls = [ + re_path(r'^action/', ActionPluginView.as_view(), name='api-action-plugin'), + re_path(r'^barcode/', include(barcode_api_urls)), +] + +general_plugin_api_urls = [ # Plugin settings URLs re_path(r'^settings/', include([ @@ -174,3 +182,8 @@ plugin_api_urls = [ # Anything else re_path(r'^.*$', PluginList.as_view(), name='api-plugin-list'), ] + +if settings.PLUGINS_ENABLED: + plugin_api_urls.append( + re_path(r'^plugin/', include(general_plugin_api_urls)) + ) diff --git a/InvenTree/plugin/base/action/api.py b/InvenTree/plugin/base/action/api.py new file mode 100644 index 0000000000..998e410dce --- /dev/null +++ b/InvenTree/plugin/base/action/api.py @@ -0,0 +1,45 @@ +"""APIs for action plugins""" +from django.utils.translation import gettext_lazy as _ + +from rest_framework import permissions +from rest_framework.response import Response +from rest_framework.views import APIView + +from plugin import registry + + +class ActionPluginView(APIView): + """ + Endpoint for running custom action plugins. + """ + + permission_classes = [ + permissions.IsAuthenticated, + ] + + def post(self, request, *args, **kwargs): + + action = request.data.get('action', None) + + data = request.data.get('data', None) + + if action is None: + return Response({ + 'error': _("No action specified") + }) + + 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() + + return Response(plugin.get_response()) + + # If we got to here, no matching action was found + return Response({ + 'error': _("No matching action found"), + "action": action, + }) diff --git a/InvenTree/plugin/base/barcodes/barcode.py b/InvenTree/plugin/base/barcodes/api.py similarity index 99% rename from InvenTree/plugin/base/barcodes/barcode.py rename to InvenTree/plugin/base/barcodes/api.py index 21a142f841..296986b2d1 100644 --- a/InvenTree/plugin/base/barcodes/barcode.py +++ b/InvenTree/plugin/base/barcodes/api.py @@ -237,7 +237,7 @@ class BarcodeAssign(APIView): barcode_api_urls = [ - + # Link a barcode to a part path('link/', BarcodeAssign.as_view(), name='api-barcode-link'), # Catch-all performs barcode 'scan' diff --git a/InvenTree/plugin/urls.py b/InvenTree/plugin/urls.py index 08f547aca2..e97316773e 100644 --- a/InvenTree/plugin/urls.py +++ b/InvenTree/plugin/urls.py @@ -5,6 +5,7 @@ URL lookup for plugin app from django.urls import include, re_path from plugin import registry +from plugin.base.barcodes.api import barcode_api_urls PLUGIN_BASE = 'plugin' # Constant for links From 6ec3ec19a24ac8480b870b50a78742718d04be88 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 13:11:27 +0200 Subject: [PATCH 016/135] move parts to conform to general style --- InvenTree/InvenTree/urls.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index 591a482450..ab761c611d 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -27,6 +27,7 @@ from order.api import order_api_urls from label.api import label_api_urls from report.api import report_api_urls from plugin.api import plugin_api_urls +from users.api import user_urls from django.conf import settings from django.conf.urls.static import static @@ -45,8 +46,6 @@ from .views import NotificationsView from .api import InfoView, NotFoundView -from users.api import user_urls - admin.site.site_header = "InvenTree Admin" @@ -60,8 +59,6 @@ apipatterns = [ re_path(r'^order/', include(order_api_urls)), re_path(r'^label/', include(label_api_urls)), re_path(r'^report/', include(report_api_urls)), - - # User URLs re_path(r'^user/', include(user_urls)), # Plugin endpoints From 9788419b0c5d5c3e0d5fdfd345eb17aeeb2f41ab Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 13:13:34 +0200 Subject: [PATCH 017/135] remove unneeded import --- InvenTree/plugin/urls.py | 1 - 1 file changed, 1 deletion(-) diff --git a/InvenTree/plugin/urls.py b/InvenTree/plugin/urls.py index e97316773e..08f547aca2 100644 --- a/InvenTree/plugin/urls.py +++ b/InvenTree/plugin/urls.py @@ -5,7 +5,6 @@ URL lookup for plugin app from django.urls import include, re_path from plugin import registry -from plugin.base.barcodes.api import barcode_api_urls PLUGIN_BASE = 'plugin' # Constant for links From 5a139ec74bef7f49c4289c1f19b4cbe28860faa8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 13:19:24 +0200 Subject: [PATCH 018/135] make import simpler the minimum version is 3.9 --- InvenTree/plugin/registry.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/InvenTree/plugin/registry.py b/InvenTree/plugin/registry.py index 985d89a7a4..7c2c538103 100644 --- a/InvenTree/plugin/registry.py +++ b/InvenTree/plugin/registry.py @@ -12,7 +12,7 @@ import os import subprocess from typing import OrderedDict -from importlib import reload +from importlib import reload, metadata from django.apps import apps from django.conf import settings @@ -22,12 +22,6 @@ from django.urls import clear_url_caches from django.contrib import admin from django.utils.text import slugify -try: - from importlib import metadata -except: # pragma: no cover - import importlib_metadata as metadata - # TODO remove when python minimum is 3.8 - from maintenance_mode.core import maintenance_mode_on from maintenance_mode.core import get_maintenance_mode, set_maintenance_mode From 970503f424a2144ea24077d24a4fa56b276d04a6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 13:28:59 +0200 Subject: [PATCH 019/135] refactor events --- InvenTree/build/models.py | 2 +- InvenTree/order/models.py | 2 +- InvenTree/plugin/__init__.py | 6 +++- InvenTree/plugin/{ => base/event}/events.py | 0 InvenTree/plugin/base/event/mixins.py | 29 +++++++++++++++++++ InvenTree/plugin/base/integration/mixins.py | 26 ----------------- InvenTree/plugin/mixins/__init__.py | 3 +- InvenTree/plugin/samples/event/__init__.py | 0 .../{integration => event}/event_sample.py | 0 InvenTree/stock/models.py | 2 +- 10 files changed, 39 insertions(+), 31 deletions(-) rename InvenTree/plugin/{ => base/event}/events.py (100%) create mode 100644 InvenTree/plugin/base/event/mixins.py create mode 100644 InvenTree/plugin/samples/event/__init__.py rename InvenTree/plugin/samples/{integration => event}/event_sample.py (100%) diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index a1517d73dd..8b4ee5e0b2 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -36,7 +36,7 @@ import InvenTree.fields import InvenTree.helpers import InvenTree.tasks -from plugin.events import trigger_event +from plugin import trigger_event from part import models as PartModels from stock import models as StockModels diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 6ca5b7a293..0bf14cffc8 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -30,7 +30,7 @@ from users import models as UserModels from part import models as PartModels from stock import models as stock_models from company.models import Company, SupplierPart -from plugin.events import trigger_event +from plugin import trigger_event import InvenTree.helpers from InvenTree.fields import InvenTreeModelMoneyField, RoundingDecimalField diff --git a/InvenTree/plugin/__init__.py b/InvenTree/plugin/__init__.py index 35a6687628..3e2aa48b47 100644 --- a/InvenTree/plugin/__init__.py +++ b/InvenTree/plugin/__init__.py @@ -3,16 +3,20 @@ Utility file to enable simper imports """ from .registry import registry +from .base.event.events import trigger_event + from .plugin import InvenTreePluginBase, IntegrationPluginBase from .base.action.action import ActionPlugin from .helpers import MixinNotImplementedError, MixinImplementationError __all__ = [ + 'registry', + 'trigger_event', + 'ActionPlugin', 'IntegrationPluginBase', 'InvenTreePluginBase', - 'registry', 'MixinNotImplementedError', 'MixinImplementationError', ] diff --git a/InvenTree/plugin/events.py b/InvenTree/plugin/base/event/events.py similarity index 100% rename from InvenTree/plugin/events.py rename to InvenTree/plugin/base/event/events.py diff --git a/InvenTree/plugin/base/event/mixins.py b/InvenTree/plugin/base/event/mixins.py new file mode 100644 index 0000000000..8df9bfd164 --- /dev/null +++ b/InvenTree/plugin/base/event/mixins.py @@ -0,0 +1,29 @@ +"""Plugin mixin class for events""" + +from plugin.helpers import MixinNotImplementedError + + +class EventMixin: + """ + 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 + Must be overridden by plugin + """ + # Default implementation does not do anything + raise MixinNotImplementedError + + class MixinMeta: + """ + Meta options for this mixin + """ + MIXIN_NAME = 'Events' + + def __init__(self): + super().__init__() + self.add_mixin('events', True, __class__) diff --git a/InvenTree/plugin/base/integration/mixins.py b/InvenTree/plugin/base/integration/mixins.py index b22efc9415..5bae993e61 100644 --- a/InvenTree/plugin/base/integration/mixins.py +++ b/InvenTree/plugin/base/integration/mixins.py @@ -238,32 +238,6 @@ class ScheduleMixin: logger.warning("unregister_tasks failed, database not ready") -class EventMixin: - """ - 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 - Must be overridden by plugin - """ - # Default implementation does not do anything - raise MixinNotImplementedError - - class MixinMeta: - """ - Meta options for this mixin - """ - MIXIN_NAME = 'Events' - - def __init__(self): - super().__init__() - self.add_mixin('events', True, __class__) - - class UrlsMixin: """ Mixin that enables custom URLs for the plugin diff --git a/InvenTree/plugin/mixins/__init__.py b/InvenTree/plugin/mixins/__init__.py index af20a05491..8683b5c45f 100644 --- a/InvenTree/plugin/mixins/__init__.py +++ b/InvenTree/plugin/mixins/__init__.py @@ -2,12 +2,13 @@ Utility class to enable simpler imports """ -from ..base.integration.mixins import APICallMixin, AppMixin, LabelPrintingMixin, SettingsMixin, EventMixin, ScheduleMixin, UrlsMixin, NavigationMixin, PanelMixin +from ..base.integration.mixins import APICallMixin, AppMixin, LabelPrintingMixin, SettingsMixin, ScheduleMixin, UrlsMixin, NavigationMixin, PanelMixin from common.notifications import SingleNotificationMethod, BulkNotificationMethod from ..base.action.mixins import ActionMixin from ..base.barcodes.mixins import BarcodeMixin +from ..base.events.mixins import EventMixin __all__ = [ 'APICallMixin', diff --git a/InvenTree/plugin/samples/event/__init__.py b/InvenTree/plugin/samples/event/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/InvenTree/plugin/samples/integration/event_sample.py b/InvenTree/plugin/samples/event/event_sample.py similarity index 100% rename from InvenTree/plugin/samples/integration/event_sample.py rename to InvenTree/plugin/samples/event/event_sample.py diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 53e1321e1a..cf240fc5dc 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -38,7 +38,7 @@ import common.models import report.models import label.models -from plugin.events import trigger_event +from plugin import trigger_event from InvenTree.status_codes import StockStatus, StockHistoryCode from InvenTree.models import InvenTreeTree, InvenTreeAttachment From 1098327ba9926b7af85c8d437807b7d5e03ee448 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 13:32:29 +0200 Subject: [PATCH 020/135] move out templete renderer into helper --- InvenTree/plugin/base/integration/mixins.py | 3 +- InvenTree/plugin/helpers.py | 28 +++++++++++++++++++ InvenTree/plugin/template.py | 31 +-------------------- 3 files changed, 30 insertions(+), 32 deletions(-) diff --git a/InvenTree/plugin/base/integration/mixins.py b/InvenTree/plugin/base/integration/mixins.py index 5bae993e61..32bdd00681 100644 --- a/InvenTree/plugin/base/integration/mixins.py +++ b/InvenTree/plugin/base/integration/mixins.py @@ -11,9 +11,8 @@ from django.db.utils import OperationalError, ProgrammingError import InvenTree.helpers -from plugin.helpers import MixinImplementationError, MixinNotImplementedError +from plugin.helpers import MixinImplementationError, MixinNotImplementedError, render_template from plugin.models import PluginConfig, PluginSetting -from plugin.template import render_template from plugin.urls import PLUGIN_BASE diff --git a/InvenTree/plugin/helpers.py b/InvenTree/plugin/helpers.py index f1753b1b45..011a8f84f0 100644 --- a/InvenTree/plugin/helpers.py +++ b/InvenTree/plugin/helpers.py @@ -8,12 +8,17 @@ import sysconfig import traceback import inspect import pkgutil +import logging +from django import template from django.conf import settings from django.core.exceptions import AppRegistryNotReady from django.db.utils import IntegrityError +logger = logging.getLogger('inventree') + + # region logging / errors class IntegrationPluginError(Exception): """ @@ -217,3 +222,26 @@ def get_plugins(pkg, baseclass): return plugins # endregion + +# region templates +def render_template(plugin, template_file, context=None): + """ + Locate and render a template file, available in the global template context. + """ + + try: + tmp = template.loader.get_template(template_file) + except template.TemplateDoesNotExist: + logger.error(f"Plugin {plugin.slug} could not locate template '{template_file}'") + + return f""" +
+ Template file {template_file} does not exist. +
+ """ + + # Render with the provided context + html = tmp.render(context) + + return html +# endregion diff --git a/InvenTree/plugin/template.py b/InvenTree/plugin/template.py index 53ee7bb6db..f33b5f13fb 100644 --- a/InvenTree/plugin/template.py +++ b/InvenTree/plugin/template.py @@ -1,19 +1,12 @@ -""" -load templates for loaded plugins -""" +"""Load templates for loaded plugins""" -import logging from pathlib import Path -from django import template from django.template.loaders.filesystem import Loader as FilesystemLoader from plugin import registry -logger = logging.getLogger('inventree') - - class PluginTemplateLoader(FilesystemLoader): """ A custom template loader which allows loading of templates from installed plugins. @@ -38,25 +31,3 @@ class PluginTemplateLoader(FilesystemLoader): template_dirs.append(new_path) return tuple(template_dirs) - - -def render_template(plugin, template_file, context=None): - """ - Locate and render a template file, available in the global template context. - """ - - try: - tmp = template.loader.get_template(template_file) - except template.TemplateDoesNotExist: - logger.error(f"Plugin {plugin.slug} could not locate template '{template_file}'") - - return f""" -
- Template file {template_file} does not exist. -
- """ - - # Render with the provided context - html = tmp.render(context) - - return html From 887609cf2a6f2f4d93eaf0596819472454db22c1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 13:38:24 +0200 Subject: [PATCH 021/135] move import --- InvenTree/plugin/base/event/events.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/plugin/base/event/events.py b/InvenTree/plugin/base/event/events.py index 043f3b97a3..eb94f3b0e2 100644 --- a/InvenTree/plugin/base/event/events.py +++ b/InvenTree/plugin/base/event/events.py @@ -14,7 +14,6 @@ from django.db import transaction from django.db.models.signals import post_save, post_delete from django.dispatch.dispatcher import receiver -from common.models import InvenTreeSetting import common.notifications from InvenTree.ready import canAppAccessDatabase, isImportingData @@ -59,6 +58,7 @@ def register_event(event, *args, **kwargs): Note: This function is processed by the background worker, as it performs multiple database access operations. """ + from common.models import InvenTreeSetting logger.debug(f"Registering triggered event: '{event}'") From dd5f53d2ef3ef54a655b2db8933bbba71534e952 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 13:42:15 +0200 Subject: [PATCH 022/135] PEP fix --- InvenTree/plugin/helpers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/InvenTree/plugin/helpers.py b/InvenTree/plugin/helpers.py index 011a8f84f0..db6f9635a2 100644 --- a/InvenTree/plugin/helpers.py +++ b/InvenTree/plugin/helpers.py @@ -223,6 +223,7 @@ def get_plugins(pkg, baseclass): return plugins # endregion + # region templates def render_template(plugin, template_file, context=None): """ From 182ffd880a848a4a736291d56b9c283effece379 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 13:46:07 +0200 Subject: [PATCH 023/135] init directories --- InvenTree/plugin/base/__init__.py | 0 InvenTree/plugin/base/action/__init__.py | 0 InvenTree/plugin/base/barcodes/__init__.py | 0 InvenTree/plugin/base/event/__init__.py | 0 InvenTree/plugin/base/integration/__init__.py | 0 5 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 InvenTree/plugin/base/__init__.py create mode 100644 InvenTree/plugin/base/action/__init__.py create mode 100644 InvenTree/plugin/base/barcodes/__init__.py create mode 100644 InvenTree/plugin/base/event/__init__.py create mode 100644 InvenTree/plugin/base/integration/__init__.py diff --git a/InvenTree/plugin/base/__init__.py b/InvenTree/plugin/base/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/InvenTree/plugin/base/action/__init__.py b/InvenTree/plugin/base/action/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/InvenTree/plugin/base/barcodes/__init__.py b/InvenTree/plugin/base/barcodes/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/InvenTree/plugin/base/event/__init__.py b/InvenTree/plugin/base/event/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/InvenTree/plugin/base/integration/__init__.py b/InvenTree/plugin/base/integration/__init__.py new file mode 100644 index 0000000000..e69de29bb2 From 7361a9637a24292df1cfe4a71c5ad172937077e2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 13:49:49 +0200 Subject: [PATCH 024/135] move event back --- InvenTree/plugin/__init__.py | 2 +- InvenTree/plugin/{base/event => }/events.py | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename InvenTree/plugin/{base/event => }/events.py (100%) diff --git a/InvenTree/plugin/__init__.py b/InvenTree/plugin/__init__.py index 3e2aa48b47..94ff41e9ea 100644 --- a/InvenTree/plugin/__init__.py +++ b/InvenTree/plugin/__init__.py @@ -3,7 +3,7 @@ Utility file to enable simper imports """ from .registry import registry -from .base.event.events import trigger_event +from .events import trigger_event from .plugin import InvenTreePluginBase, IntegrationPluginBase from .base.action.action import ActionPlugin diff --git a/InvenTree/plugin/base/event/events.py b/InvenTree/plugin/events.py similarity index 100% rename from InvenTree/plugin/base/event/events.py rename to InvenTree/plugin/events.py From 3503ca4b4e4268dcd25cb03ac8d01dcdbe8bd5f6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 13:54:08 +0200 Subject: [PATCH 025/135] move import back too --- InvenTree/build/models.py | 2 +- InvenTree/order/models.py | 2 +- InvenTree/plugin/__init__.py | 2 -- InvenTree/stock/models.py | 2 +- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 8b4ee5e0b2..a1517d73dd 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -36,7 +36,7 @@ import InvenTree.fields import InvenTree.helpers import InvenTree.tasks -from plugin import trigger_event +from plugin.events import trigger_event from part import models as PartModels from stock import models as StockModels diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 0bf14cffc8..6ca5b7a293 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -30,7 +30,7 @@ from users import models as UserModels from part import models as PartModels from stock import models as stock_models from company.models import Company, SupplierPart -from plugin import trigger_event +from plugin.events import trigger_event import InvenTree.helpers from InvenTree.fields import InvenTreeModelMoneyField, RoundingDecimalField diff --git a/InvenTree/plugin/__init__.py b/InvenTree/plugin/__init__.py index 94ff41e9ea..ca7fc0c305 100644 --- a/InvenTree/plugin/__init__.py +++ b/InvenTree/plugin/__init__.py @@ -3,7 +3,6 @@ Utility file to enable simper imports """ from .registry import registry -from .events import trigger_event from .plugin import InvenTreePluginBase, IntegrationPluginBase from .base.action.action import ActionPlugin @@ -12,7 +11,6 @@ from .helpers import MixinNotImplementedError, MixinImplementationError __all__ = [ 'registry', - 'trigger_event', 'ActionPlugin', 'IntegrationPluginBase', diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index cf240fc5dc..53e1321e1a 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -38,7 +38,7 @@ import common.models import report.models import label.models -from plugin import trigger_event +from plugin.events import trigger_event from InvenTree.status_codes import StockStatus, StockHistoryCode from InvenTree.models import InvenTreeTree, InvenTreeAttachment From 066eaa119dc82311032edde19489eeefaafaf083 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 14:01:29 +0200 Subject: [PATCH 026/135] fix import --- InvenTree/plugin/mixins/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/plugin/mixins/__init__.py b/InvenTree/plugin/mixins/__init__.py index 8683b5c45f..aac3b2b6d2 100644 --- a/InvenTree/plugin/mixins/__init__.py +++ b/InvenTree/plugin/mixins/__init__.py @@ -8,7 +8,7 @@ from common.notifications import SingleNotificationMethod, BulkNotificationMetho from ..base.action.mixins import ActionMixin from ..base.barcodes.mixins import BarcodeMixin -from ..base.events.mixins import EventMixin +from ..base.event.mixins import EventMixin __all__ = [ 'APICallMixin', From 1636459ccd8db075fd08f33b654e0a12833316e3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 14:02:45 +0200 Subject: [PATCH 027/135] move to use shim --- InvenTree/build/models.py | 2 +- InvenTree/order/models.py | 2 +- InvenTree/plugin/{ => base/event}/events.py | 0 InvenTree/plugin/event.py | 5 +++++ InvenTree/stock/models.py | 2 +- 5 files changed, 8 insertions(+), 3 deletions(-) rename InvenTree/plugin/{ => base/event}/events.py (100%) create mode 100644 InvenTree/plugin/event.py diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index a1517d73dd..f08eac305f 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -36,7 +36,7 @@ import InvenTree.fields import InvenTree.helpers import InvenTree.tasks -from plugin.events import trigger_event +from plugin.event import trigger_event from part import models as PartModels from stock import models as StockModels diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 6ca5b7a293..3edd180536 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -30,7 +30,7 @@ from users import models as UserModels from part import models as PartModels from stock import models as stock_models from company.models import Company, SupplierPart -from plugin.events import trigger_event +from plugin.event import trigger_event import InvenTree.helpers from InvenTree.fields import InvenTreeModelMoneyField, RoundingDecimalField diff --git a/InvenTree/plugin/events.py b/InvenTree/plugin/base/event/events.py similarity index 100% rename from InvenTree/plugin/events.py rename to InvenTree/plugin/base/event/events.py diff --git a/InvenTree/plugin/event.py b/InvenTree/plugin/event.py new file mode 100644 index 0000000000..f6bc7fd7a8 --- /dev/null +++ b/InvenTree/plugin/event.py @@ -0,0 +1,5 @@ +""" +Import helper for events +""" + +from plugin.base.event.events import trigger_event diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 53e1321e1a..662626d425 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -38,7 +38,7 @@ import common.models import report.models import label.models -from plugin.events import trigger_event +from plugin.event import trigger_event from InvenTree.status_codes import StockStatus, StockHistoryCode from InvenTree.models import InvenTreeTree, InvenTreeAttachment From 538e5e6c21b45dfea9d73fb5906709d9b447d3f5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 14:03:24 +0200 Subject: [PATCH 028/135] rename shim --- InvenTree/build/models.py | 2 +- InvenTree/order/models.py | 2 +- InvenTree/plugin/{event.py => events.py} | 0 InvenTree/stock/models.py | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename InvenTree/plugin/{event.py => events.py} (100%) diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index f08eac305f..a1517d73dd 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -36,7 +36,7 @@ import InvenTree.fields import InvenTree.helpers import InvenTree.tasks -from plugin.event import trigger_event +from plugin.events import trigger_event from part import models as PartModels from stock import models as StockModels diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 3edd180536..6ca5b7a293 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -30,7 +30,7 @@ from users import models as UserModels from part import models as PartModels from stock import models as stock_models from company.models import Company, SupplierPart -from plugin.event import trigger_event +from plugin.events import trigger_event import InvenTree.helpers from InvenTree.fields import InvenTreeModelMoneyField, RoundingDecimalField diff --git a/InvenTree/plugin/event.py b/InvenTree/plugin/events.py similarity index 100% rename from InvenTree/plugin/event.py rename to InvenTree/plugin/events.py diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 662626d425..53e1321e1a 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -38,7 +38,7 @@ import common.models import report.models import label.models -from plugin.event import trigger_event +from plugin.events import trigger_event from InvenTree.status_codes import StockStatus, StockHistoryCode from InvenTree.models import InvenTreeTree, InvenTreeAttachment From 19cfa540d9b67ad75a8093a707a84b0d616354ea Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 14:06:11 +0200 Subject: [PATCH 029/135] add export definition --- InvenTree/plugin/events.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/InvenTree/plugin/events.py b/InvenTree/plugin/events.py index f6bc7fd7a8..193a1805fe 100644 --- a/InvenTree/plugin/events.py +++ b/InvenTree/plugin/events.py @@ -3,3 +3,7 @@ Import helper for events """ from plugin.base.event.events import trigger_event + +__all__ = [ + trigger_event, +] From 05839ca94c2f35ab9aa6755398e861d98d3ba213 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 14:20:05 +0200 Subject: [PATCH 030/135] remove legacy action --- InvenTree/plugin/__init__.py | 4 ---- InvenTree/plugin/base/action/action.py | 23 ------------------- InvenTree/plugin/base/action/test_action.py | 21 +++++++++-------- .../builtin/action/simpleactionplugin.py | 9 ++++---- 4 files changed, 17 insertions(+), 40 deletions(-) delete mode 100644 InvenTree/plugin/base/action/action.py diff --git a/InvenTree/plugin/__init__.py b/InvenTree/plugin/__init__.py index ca7fc0c305..6ccd106253 100644 --- a/InvenTree/plugin/__init__.py +++ b/InvenTree/plugin/__init__.py @@ -3,16 +3,12 @@ Utility file to enable simper imports """ from .registry import registry - from .plugin import InvenTreePluginBase, IntegrationPluginBase -from .base.action.action import ActionPlugin - from .helpers import MixinNotImplementedError, MixinImplementationError __all__ = [ 'registry', - 'ActionPlugin', 'IntegrationPluginBase', 'InvenTreePluginBase', 'MixinNotImplementedError', diff --git a/InvenTree/plugin/base/action/action.py b/InvenTree/plugin/base/action/action.py deleted file mode 100644 index f012c45116..0000000000 --- a/InvenTree/plugin/base/action/action.py +++ /dev/null @@ -1,23 +0,0 @@ -# -*- coding: utf-8 -*- -"""Class for ActionPlugin""" - -import logging -import warnings - -from plugin.base.action.mixins import ActionMixin -import plugin.plugin - - -logger = logging.getLogger("inventree") - - -class ActionPlugin(ActionMixin, plugin.plugin.IntegrationPluginBase): - """ - Legacy action definition - will be replaced - Please use the new Integration Plugin API and the Action mixin - """ - # TODO @matmair remove this with InvenTree 0.7.0 - def __init__(self, user=None, data=None): - warnings.warn("using the ActionPlugin is depreceated", DeprecationWarning) - super().__init__() - self.init(user, data) diff --git a/InvenTree/plugin/base/action/test_action.py b/InvenTree/plugin/base/action/test_action.py index 299833ccd1..273aaa8b94 100644 --- a/InvenTree/plugin/base/action/test_action.py +++ b/InvenTree/plugin/base/action/test_action.py @@ -2,32 +2,35 @@ from django.test import TestCase -from plugin.base.action.action import ActionPlugin +from plugin import IntegrationPluginBase +from plugin.mixins import ActionMixin -class ActionPluginTests(TestCase): - """ Tests for ActionPlugin """ +class ActionMixinTests(TestCase): + """ Tests for ActionMixin """ ACTION_RETURN = 'a action was performed' def setUp(self): - self.plugin = ActionPlugin('user') + class SimplePlugin(ActionMixin, IntegrationPluginBase): + pass + self.plugin = SimplePlugin('user') - class TestActionPlugin(ActionPlugin): + class TestActionPlugin(ActionMixin, IntegrationPluginBase): """a action plugin""" ACTION_NAME = 'abc123' def perform_action(self): - return ActionPluginTests.ACTION_RETURN + 'action' + return ActionMixinTests.ACTION_RETURN + 'action' def get_result(self): - return ActionPluginTests.ACTION_RETURN + 'result' + return ActionMixinTests.ACTION_RETURN + 'result' def get_info(self): - return ActionPluginTests.ACTION_RETURN + 'info' + return ActionMixinTests.ACTION_RETURN + 'info' self.action_plugin = TestActionPlugin('user') - class NameActionPlugin(ActionPlugin): + class NameActionPlugin(ActionMixin, IntegrationPluginBase): PLUGIN_NAME = 'Aplugin' self.action_name = NameActionPlugin('user') diff --git a/InvenTree/plugin/builtin/action/simpleactionplugin.py b/InvenTree/plugin/builtin/action/simpleactionplugin.py index b7acd2d5f6..e9c7c48386 100644 --- a/InvenTree/plugin/builtin/action/simpleactionplugin.py +++ b/InvenTree/plugin/builtin/action/simpleactionplugin.py @@ -1,12 +1,13 @@ # -*- coding: utf-8 -*- -"""sample implementation for ActionPlugin""" -from plugin.base.action.action import ActionPlugin +"""sample implementation for ActionMixin""" +from plugin import IntegrationPluginBase +from plugin.mixins import ActionMixin -class SimpleActionPlugin(ActionPlugin): +class SimpleActionPlugin(ActionMixin, IntegrationPluginBase): """ An EXTREMELY simple action plugin which demonstrates - the capability of the ActionPlugin class + the capability of the ActionMixin class """ PLUGIN_NAME = "SimpleActionPlugin" From 01e8b5dce3671b61365b9c7dff3d65b2ffe0f1eb Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 14:21:16 +0200 Subject: [PATCH 031/135] remove InvenTreePluginBase --- InvenTree/common/models.py | 4 +-- InvenTree/plugin/__init__.py | 3 +-- InvenTree/plugin/models.py | 4 +-- InvenTree/plugin/test_plugin.py | 48 +++++++++++++++------------------ 4 files changed, 26 insertions(+), 33 deletions(-) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 11157763cb..e50fb06b6e 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -257,9 +257,9 @@ class BaseInvenTreeSetting(models.Model): plugin = kwargs.get('plugin', None) if plugin is not None: - from plugin import InvenTreePluginBase + from plugin import IntegrationPluginBase - if issubclass(plugin.__class__, InvenTreePluginBase): + if issubclass(plugin.__class__, IntegrationPluginBase): plugin = plugin.plugin_config() filters['plugin'] = plugin diff --git a/InvenTree/plugin/__init__.py b/InvenTree/plugin/__init__.py index 6ccd106253..827072b5ae 100644 --- a/InvenTree/plugin/__init__.py +++ b/InvenTree/plugin/__init__.py @@ -3,14 +3,13 @@ Utility file to enable simper imports """ from .registry import registry -from .plugin import InvenTreePluginBase, IntegrationPluginBase +from .plugin import IntegrationPluginBase from .helpers import MixinNotImplementedError, MixinImplementationError __all__ = [ 'registry', 'IntegrationPluginBase', - 'InvenTreePluginBase', 'MixinNotImplementedError', 'MixinImplementationError', ] diff --git a/InvenTree/plugin/models.py b/InvenTree/plugin/models.py index 0624693abc..a3cc25ec96 100644 --- a/InvenTree/plugin/models.py +++ b/InvenTree/plugin/models.py @@ -11,7 +11,7 @@ from django.contrib.auth.models import User import common.models -from plugin import InvenTreePluginBase, registry +from plugin import IntegrationPluginBase, registry class PluginConfig(models.Model): @@ -135,7 +135,7 @@ class PluginSetting(common.models.GenericReferencedSettingClass, common.models.B if plugin: - if issubclass(plugin.__class__, InvenTreePluginBase): + if issubclass(plugin.__class__, IntegrationPluginBase): plugin = plugin.plugin_config() kwargs['settings'] = registry.mixins_settings.get(plugin.key, {}) diff --git a/InvenTree/plugin/test_plugin.py b/InvenTree/plugin/test_plugin.py index eee3230d2a..601c557a1a 100644 --- a/InvenTree/plugin/test_plugin.py +++ b/InvenTree/plugin/test_plugin.py @@ -9,36 +9,10 @@ from django.test import TestCase from plugin.samples.integration.sample import SampleIntegrationPlugin from plugin.samples.integration.another_sample import WrongIntegrationPlugin, NoIntegrationPlugin import plugin.templatetags.plugin_extras as plugin_tags -from plugin import registry, InvenTreePluginBase +from plugin import registry, IntegrationPluginBase from plugin.plugin import IntegrationPluginBase -class InvenTreePluginTests(TestCase): - """ Tests for InvenTreePlugin """ - def setUp(self): - self.plugin = InvenTreePluginBase() - - class NamedPlugin(InvenTreePluginBase): - """a named plugin""" - PLUGIN_NAME = 'abc123' - - self.named_plugin = NamedPlugin() - - def test_basic_plugin_init(self): - """check if a basic plugin intis""" - self.assertEqual(self.plugin.PLUGIN_NAME, '') - self.assertEqual(self.plugin.plugin_name(), '') - - def test_basic_plugin_name(self): - """check if the name of a basic plugin can be set""" - self.assertEqual(self.named_plugin.PLUGIN_NAME, 'abc123') - self.assertEqual(self.named_plugin.plugin_name(), 'abc123') - - def test_basic_is_active(self): - """check if a basic plugin is active""" - self.assertEqual(self.plugin.is_active(), False) - - class PluginTagTests(TestCase): """ Tests for the plugin extras """ @@ -90,6 +64,12 @@ class IntegrationPluginBaseTests(TestCase): def setUp(self): self.plugin = IntegrationPluginBase() + class NamedPlugin(IntegrationPluginBase): + """a named plugin""" + PLUGIN_NAME = 'abc123' + + self.named_plugin = NamedPlugin() + class SimpeIntegrationPluginBase(IntegrationPluginBase): PLUGIN_NAME = 'SimplePlugin' @@ -109,6 +89,20 @@ class IntegrationPluginBaseTests(TestCase): self.plugin_name = NameIntegrationPluginBase() self.plugin_sample = SampleIntegrationPlugin() + def test_basic_plugin_init(self): + """check if a basic plugin intis""" + self.assertEqual(self.plugin.PLUGIN_NAME, '') + self.assertEqual(self.plugin.plugin_name(), '') + + def test_basic_plugin_name(self): + """check if the name of a basic plugin can be set""" + self.assertEqual(self.named_plugin.PLUGIN_NAME, 'abc123') + self.assertEqual(self.named_plugin.plugin_name(), 'abc123') + + def test_basic_is_active(self): + """check if a basic plugin is active""" + self.assertEqual(self.plugin.is_active(), False) + def test_action_name(self): """check the name definition possibilities""" # plugin_name From 1571b99ed259b8b8be4a07c17a0c4fc451e0b1b9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 14:28:26 +0200 Subject: [PATCH 032/135] rename IntegrationPluginBase to InvenTreePlugin --- InvenTree/common/models.py | 4 ++-- InvenTree/plugin/__init__.py | 4 ++-- InvenTree/plugin/base/action/test_action.py | 8 +++---- .../plugin/base/integration/test_mixins.py | 24 +++++++++---------- .../builtin/action/simpleactionplugin.py | 4 ++-- .../builtin/barcodes/inventree_barcode.py | 4 ++-- .../builtin/integration/core_notifications.py | 4 ++-- InvenTree/plugin/models.py | 4 ++-- InvenTree/plugin/plugin.py | 6 ++--- InvenTree/plugin/registry.py | 4 ++-- .../plugin/samples/event/event_sample.py | 4 ++-- .../samples/integration/another_sample.py | 6 ++--- .../plugin/samples/integration/api_caller.py | 4 ++-- .../plugin/samples/integration/broken_file.py | 4 ++-- .../samples/integration/broken_sample.py | 4 ++-- .../integration/custom_panel_sample.py | 4 ++-- .../plugin/samples/integration/sample.py | 4 ++-- .../samples/integration/scheduled_task.py | 4 ++-- .../integration/test_scheduled_task.py | 4 ++-- InvenTree/plugin/test_plugin.py | 20 ++++++++-------- 20 files changed, 62 insertions(+), 62 deletions(-) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index e50fb06b6e..d378c7616a 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -257,9 +257,9 @@ class BaseInvenTreeSetting(models.Model): plugin = kwargs.get('plugin', None) if plugin is not None: - from plugin import IntegrationPluginBase + from plugin import InvenTreePlugin - if issubclass(plugin.__class__, IntegrationPluginBase): + if issubclass(plugin.__class__, InvenTreePlugin): plugin = plugin.plugin_config() filters['plugin'] = plugin diff --git a/InvenTree/plugin/__init__.py b/InvenTree/plugin/__init__.py index 827072b5ae..3b2f9fe871 100644 --- a/InvenTree/plugin/__init__.py +++ b/InvenTree/plugin/__init__.py @@ -3,13 +3,13 @@ Utility file to enable simper imports """ from .registry import registry -from .plugin import IntegrationPluginBase +from .plugin import InvenTreePlugin from .helpers import MixinNotImplementedError, MixinImplementationError __all__ = [ 'registry', - 'IntegrationPluginBase', + 'InvenTreePlugin', 'MixinNotImplementedError', 'MixinImplementationError', ] diff --git a/InvenTree/plugin/base/action/test_action.py b/InvenTree/plugin/base/action/test_action.py index 273aaa8b94..24caf175fc 100644 --- a/InvenTree/plugin/base/action/test_action.py +++ b/InvenTree/plugin/base/action/test_action.py @@ -2,7 +2,7 @@ from django.test import TestCase -from plugin import IntegrationPluginBase +from plugin import InvenTreePlugin from plugin.mixins import ActionMixin @@ -11,11 +11,11 @@ class ActionMixinTests(TestCase): ACTION_RETURN = 'a action was performed' def setUp(self): - class SimplePlugin(ActionMixin, IntegrationPluginBase): + class SimplePlugin(ActionMixin, InvenTreePlugin): pass self.plugin = SimplePlugin('user') - class TestActionPlugin(ActionMixin, IntegrationPluginBase): + class TestActionPlugin(ActionMixin, InvenTreePlugin): """a action plugin""" ACTION_NAME = 'abc123' @@ -30,7 +30,7 @@ class ActionMixinTests(TestCase): self.action_plugin = TestActionPlugin('user') - class NameActionPlugin(ActionMixin, IntegrationPluginBase): + class NameActionPlugin(ActionMixin, InvenTreePlugin): PLUGIN_NAME = 'Aplugin' self.action_name = NameActionPlugin('user') diff --git a/InvenTree/plugin/base/integration/test_mixins.py b/InvenTree/plugin/base/integration/test_mixins.py index b611454d1b..2283a3a1d2 100644 --- a/InvenTree/plugin/base/integration/test_mixins.py +++ b/InvenTree/plugin/base/integration/test_mixins.py @@ -5,7 +5,7 @@ from django.conf import settings from django.urls import include, re_path from django.contrib.auth import get_user_model -from plugin import IntegrationPluginBase +from plugin import InvenTreePlugin from plugin.mixins import AppMixin, SettingsMixin, UrlsMixin, NavigationMixin, APICallMixin from plugin.urls import PLUGIN_BASE @@ -26,11 +26,11 @@ class SettingsMixinTest(BaseMixinDefinition, TestCase): TEST_SETTINGS = {'SETTING1': {'default': '123', }} def setUp(self): - class SettingsCls(SettingsMixin, IntegrationPluginBase): + class SettingsCls(SettingsMixin, InvenTreePlugin): SETTINGS = self.TEST_SETTINGS self.mixin = SettingsCls() - class NoSettingsCls(SettingsMixin, IntegrationPluginBase): + class NoSettingsCls(SettingsMixin, InvenTreePlugin): pass self.mixin_nothing = NoSettingsCls() @@ -61,13 +61,13 @@ class UrlsMixinTest(BaseMixinDefinition, TestCase): MIXIN_ENABLE_CHECK = 'has_urls' def setUp(self): - class UrlsCls(UrlsMixin, IntegrationPluginBase): + class UrlsCls(UrlsMixin, InvenTreePlugin): def test(): return 'ccc' URLS = [re_path('testpath', test, name='test'), ] self.mixin = UrlsCls() - class NoUrlsCls(UrlsMixin, IntegrationPluginBase): + class NoUrlsCls(UrlsMixin, InvenTreePlugin): pass self.mixin_nothing = NoUrlsCls() @@ -100,7 +100,7 @@ class AppMixinTest(BaseMixinDefinition, TestCase): MIXIN_ENABLE_CHECK = 'has_app' def setUp(self): - class TestCls(AppMixin, IntegrationPluginBase): + class TestCls(AppMixin, InvenTreePlugin): pass self.mixin = TestCls() @@ -115,14 +115,14 @@ class NavigationMixinTest(BaseMixinDefinition, TestCase): MIXIN_ENABLE_CHECK = 'has_naviation' def setUp(self): - class NavigationCls(NavigationMixin, IntegrationPluginBase): + class NavigationCls(NavigationMixin, InvenTreePlugin): NAVIGATION = [ {'name': 'aa', 'link': 'plugin:test:test_view'}, ] NAVIGATION_TAB_NAME = 'abcd1' self.mixin = NavigationCls() - class NothingNavigationCls(NavigationMixin, IntegrationPluginBase): + class NothingNavigationCls(NavigationMixin, InvenTreePlugin): pass self.nothing_mixin = NothingNavigationCls() @@ -131,7 +131,7 @@ class NavigationMixinTest(BaseMixinDefinition, TestCase): self.assertEqual(self.mixin.navigation, [{'name': 'aa', 'link': 'plugin:test:test_view'}, ]) # check wrong links fails with self.assertRaises(NotImplementedError): - class NavigationCls(NavigationMixin, IntegrationPluginBase): + class NavigationCls(NavigationMixin, InvenTreePlugin): NAVIGATION = ['aa', 'aa'] NavigationCls() @@ -146,7 +146,7 @@ class APICallMixinTest(BaseMixinDefinition, TestCase): MIXIN_ENABLE_CHECK = 'has_api_call' def setUp(self): - class MixinCls(APICallMixin, SettingsMixin, IntegrationPluginBase): + class MixinCls(APICallMixin, SettingsMixin, InvenTreePlugin): PLUGIN_NAME = "Sample API Caller" SETTINGS = { @@ -170,11 +170,11 @@ class APICallMixinTest(BaseMixinDefinition, TestCase): return self.api_call('api/users/2') self.mixin = MixinCls() - class WrongCLS(APICallMixin, IntegrationPluginBase): + class WrongCLS(APICallMixin, InvenTreePlugin): pass self.mixin_wrong = WrongCLS() - class WrongCLS2(APICallMixin, IntegrationPluginBase): + class WrongCLS2(APICallMixin, InvenTreePlugin): API_URL_SETTING = 'test' self.mixin_wrong2 = WrongCLS2() diff --git a/InvenTree/plugin/builtin/action/simpleactionplugin.py b/InvenTree/plugin/builtin/action/simpleactionplugin.py index e9c7c48386..ef69511cfe 100644 --- a/InvenTree/plugin/builtin/action/simpleactionplugin.py +++ b/InvenTree/plugin/builtin/action/simpleactionplugin.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- """sample implementation for ActionMixin""" -from plugin import IntegrationPluginBase +from plugin import InvenTreePlugin from plugin.mixins import ActionMixin -class SimpleActionPlugin(ActionMixin, IntegrationPluginBase): +class SimpleActionPlugin(ActionMixin, InvenTreePlugin): """ An EXTREMELY simple action plugin which demonstrates the capability of the ActionMixin class diff --git a/InvenTree/plugin/builtin/barcodes/inventree_barcode.py b/InvenTree/plugin/builtin/barcodes/inventree_barcode.py index 939bf12c08..6b84f4a8c2 100644 --- a/InvenTree/plugin/builtin/barcodes/inventree_barcode.py +++ b/InvenTree/plugin/builtin/barcodes/inventree_barcode.py @@ -13,7 +13,7 @@ references model objects actually exist in the database. import json -from plugin import IntegrationPluginBase +from plugin import InvenTreePlugin from plugin.mixins import BarcodeMixin from stock.models import StockItem, StockLocation @@ -22,7 +22,7 @@ from part.models import Part from rest_framework.exceptions import ValidationError -class InvenTreeBarcodePlugin(BarcodeMixin, IntegrationPluginBase): +class InvenTreeBarcodePlugin(BarcodeMixin, InvenTreePlugin): PLUGIN_NAME = "InvenTreeBarcode" diff --git a/InvenTree/plugin/builtin/integration/core_notifications.py b/InvenTree/plugin/builtin/integration/core_notifications.py index a26c69e877..dbde7747e9 100644 --- a/InvenTree/plugin/builtin/integration/core_notifications.py +++ b/InvenTree/plugin/builtin/integration/core_notifications.py @@ -5,7 +5,7 @@ from django.utils.translation import ugettext_lazy as _ from allauth.account.models import EmailAddress -from plugin import IntegrationPluginBase +from plugin import InvenTreePlugin from plugin.mixins import BulkNotificationMethod, SettingsMixin import InvenTree.tasks @@ -15,7 +15,7 @@ class PlgMixin: return CoreNotificationsPlugin -class CoreNotificationsPlugin(SettingsMixin, IntegrationPluginBase): +class CoreNotificationsPlugin(SettingsMixin, InvenTreePlugin): """ Core notification methods for InvenTree """ diff --git a/InvenTree/plugin/models.py b/InvenTree/plugin/models.py index a3cc25ec96..1c5dff6b68 100644 --- a/InvenTree/plugin/models.py +++ b/InvenTree/plugin/models.py @@ -11,7 +11,7 @@ from django.contrib.auth.models import User import common.models -from plugin import IntegrationPluginBase, registry +from plugin import InvenTreePlugin, registry class PluginConfig(models.Model): @@ -135,7 +135,7 @@ class PluginSetting(common.models.GenericReferencedSettingClass, common.models.B if plugin: - if issubclass(plugin.__class__, IntegrationPluginBase): + if issubclass(plugin.__class__, InvenTreePlugin): plugin = plugin.plugin_config() kwargs['settings'] = registry.mixins_settings.get(plugin.key, {}) diff --git a/InvenTree/plugin/plugin.py b/InvenTree/plugin/plugin.py index 0f6b5d9675..45acb55e65 100644 --- a/InvenTree/plugin/plugin.py +++ b/InvenTree/plugin/plugin.py @@ -142,11 +142,11 @@ class MixinBase: return mixins -class IntegrationPluginBase(MixinBase, InvenTreePluginBase): +class InvenTreePlugin(MixinBase, InvenTreePluginBase): """ - The IntegrationPluginBase class is used to integrate with 3rd party software + The InvenTreePlugin class is used to integrate with 3rd party software - DO NOT USE THIS DIRECTLY, USE plugin.IntegrationPluginBase + DO NOT USE THIS DIRECTLY, USE plugin.InvenTreePlugin """ AUTHOR = None diff --git a/InvenTree/plugin/registry.py b/InvenTree/plugin/registry.py index 7c2c538103..4975144bfc 100644 --- a/InvenTree/plugin/registry.py +++ b/InvenTree/plugin/registry.py @@ -25,7 +25,7 @@ from django.utils.text import slugify from maintenance_mode.core import maintenance_mode_on from maintenance_mode.core import get_maintenance_mode, set_maintenance_mode -from .plugin import IntegrationPluginBase +from .plugin import InvenTreePlugin from .helpers import handle_error, log_error, get_plugins, IntegrationPluginError @@ -204,7 +204,7 @@ class PluginsRegistry: # Collect plugins from paths for plugin in settings.PLUGIN_DIRS: - modules = get_plugins(importlib.import_module(plugin), IntegrationPluginBase) + modules = get_plugins(importlib.import_module(plugin), InvenTreePlugin) if modules: [self.plugin_modules.append(item) for item in modules] diff --git a/InvenTree/plugin/samples/event/event_sample.py b/InvenTree/plugin/samples/event/event_sample.py index dddb97da1d..2e629a48a4 100644 --- a/InvenTree/plugin/samples/event/event_sample.py +++ b/InvenTree/plugin/samples/event/event_sample.py @@ -2,11 +2,11 @@ Sample plugin which responds to events """ -from plugin import IntegrationPluginBase +from plugin import InvenTreePlugin from plugin.mixins import EventMixin -class EventPluginSample(EventMixin, IntegrationPluginBase): +class EventPluginSample(EventMixin, InvenTreePlugin): """ A sample plugin which provides supports for triggered events """ diff --git a/InvenTree/plugin/samples/integration/another_sample.py b/InvenTree/plugin/samples/integration/another_sample.py index 9b3a3d8ec7..d36dbf13e8 100644 --- a/InvenTree/plugin/samples/integration/another_sample.py +++ b/InvenTree/plugin/samples/integration/another_sample.py @@ -1,9 +1,9 @@ """sample implementation for IntegrationPlugin""" -from plugin import IntegrationPluginBase +from plugin import InvenTreePlugin from plugin.mixins import UrlsMixin -class NoIntegrationPlugin(IntegrationPluginBase): +class NoIntegrationPlugin(InvenTreePlugin): """ An basic integration plugin """ @@ -11,7 +11,7 @@ class NoIntegrationPlugin(IntegrationPluginBase): PLUGIN_NAME = "NoIntegrationPlugin" -class WrongIntegrationPlugin(UrlsMixin, IntegrationPluginBase): +class WrongIntegrationPlugin(UrlsMixin, InvenTreePlugin): """ An basic integration plugin """ diff --git a/InvenTree/plugin/samples/integration/api_caller.py b/InvenTree/plugin/samples/integration/api_caller.py index 36e1583ba0..3de07508a0 100644 --- a/InvenTree/plugin/samples/integration/api_caller.py +++ b/InvenTree/plugin/samples/integration/api_caller.py @@ -1,11 +1,11 @@ """ Sample plugin for calling an external API """ -from plugin import IntegrationPluginBase +from plugin import InvenTreePlugin from plugin.mixins import APICallMixin, SettingsMixin -class SampleApiCallerPlugin(APICallMixin, SettingsMixin, IntegrationPluginBase): +class SampleApiCallerPlugin(APICallMixin, SettingsMixin, InvenTreePlugin): """ A small api call sample """ diff --git a/InvenTree/plugin/samples/integration/broken_file.py b/InvenTree/plugin/samples/integration/broken_file.py index c575cfb623..f28c72bef6 100644 --- a/InvenTree/plugin/samples/integration/broken_file.py +++ b/InvenTree/plugin/samples/integration/broken_file.py @@ -1,8 +1,8 @@ """sample of a broken python file that will be ignored on import""" -from plugin import IntegrationPluginBase +from plugin import InvenTreePlugin -class BrokenFileIntegrationPlugin(IntegrationPluginBase): +class BrokenFileIntegrationPlugin(InvenTreePlugin): """ An very broken integration plugin """ diff --git a/InvenTree/plugin/samples/integration/broken_sample.py b/InvenTree/plugin/samples/integration/broken_sample.py index 8901d83dfd..50aecc3c35 100644 --- a/InvenTree/plugin/samples/integration/broken_sample.py +++ b/InvenTree/plugin/samples/integration/broken_sample.py @@ -1,8 +1,8 @@ """sample of a broken integration plugin""" -from plugin import IntegrationPluginBase +from plugin import InvenTreePlugin -class BrokenIntegrationPlugin(IntegrationPluginBase): +class BrokenIntegrationPlugin(InvenTreePlugin): """ An very broken integration plugin """ diff --git a/InvenTree/plugin/samples/integration/custom_panel_sample.py b/InvenTree/plugin/samples/integration/custom_panel_sample.py index 73ca863576..36793ddf6a 100644 --- a/InvenTree/plugin/samples/integration/custom_panel_sample.py +++ b/InvenTree/plugin/samples/integration/custom_panel_sample.py @@ -2,14 +2,14 @@ Sample plugin which renders custom panels on certain pages """ -from plugin import IntegrationPluginBase +from plugin import InvenTreePlugin from plugin.mixins import PanelMixin, SettingsMixin from part.views import PartDetail from stock.views import StockLocationDetail -class CustomPanelSample(PanelMixin, SettingsMixin, IntegrationPluginBase): +class CustomPanelSample(PanelMixin, SettingsMixin, InvenTreePlugin): """ A sample plugin which renders some custom panels. """ diff --git a/InvenTree/plugin/samples/integration/sample.py b/InvenTree/plugin/samples/integration/sample.py index 2df3bc116a..115a943f87 100644 --- a/InvenTree/plugin/samples/integration/sample.py +++ b/InvenTree/plugin/samples/integration/sample.py @@ -2,7 +2,7 @@ Sample implementations for IntegrationPlugin """ -from plugin import IntegrationPluginBase +from plugin import InvenTreePlugin from plugin.mixins import AppMixin, SettingsMixin, UrlsMixin, NavigationMixin from django.http import HttpResponse @@ -10,7 +10,7 @@ from django.utils.translation import gettext_lazy as _ from django.urls import include, re_path -class SampleIntegrationPlugin(AppMixin, SettingsMixin, UrlsMixin, NavigationMixin, IntegrationPluginBase): +class SampleIntegrationPlugin(AppMixin, SettingsMixin, UrlsMixin, NavigationMixin, InvenTreePlugin): """ A full integration plugin example """ diff --git a/InvenTree/plugin/samples/integration/scheduled_task.py b/InvenTree/plugin/samples/integration/scheduled_task.py index 9ec70e2795..fe1069efb8 100644 --- a/InvenTree/plugin/samples/integration/scheduled_task.py +++ b/InvenTree/plugin/samples/integration/scheduled_task.py @@ -2,7 +2,7 @@ Sample plugin which supports task scheduling """ -from plugin import IntegrationPluginBase +from plugin import InvenTreePlugin from plugin.mixins import ScheduleMixin, SettingsMixin @@ -15,7 +15,7 @@ def print_world(): print("World") # pragma: no cover -class ScheduledTaskPlugin(ScheduleMixin, SettingsMixin, IntegrationPluginBase): +class ScheduledTaskPlugin(ScheduleMixin, SettingsMixin, InvenTreePlugin): """ A sample plugin which provides support for scheduled tasks """ diff --git a/InvenTree/plugin/samples/integration/test_scheduled_task.py b/InvenTree/plugin/samples/integration/test_scheduled_task.py index 314f3f3f1f..c4d234b0ba 100644 --- a/InvenTree/plugin/samples/integration/test_scheduled_task.py +++ b/InvenTree/plugin/samples/integration/test_scheduled_task.py @@ -2,7 +2,7 @@ from django.test import TestCase -from plugin import registry, IntegrationPluginBase +from plugin import registry, InvenTreePlugin from plugin.helpers import MixinImplementationError from plugin.registry import call_function from plugin.mixins import ScheduleMixin @@ -53,7 +53,7 @@ class ScheduledTaskPluginTests(TestCase): def test_init(self): """Check that all MixinImplementationErrors raise""" - class Base(ScheduleMixin, IntegrationPluginBase): + class Base(ScheduleMixin, InvenTreePlugin): PLUGIN_NAME = 'APlugin' class NoSchedules(Base): diff --git a/InvenTree/plugin/test_plugin.py b/InvenTree/plugin/test_plugin.py index 601c557a1a..09adbfd2da 100644 --- a/InvenTree/plugin/test_plugin.py +++ b/InvenTree/plugin/test_plugin.py @@ -9,8 +9,8 @@ from django.test import TestCase from plugin.samples.integration.sample import SampleIntegrationPlugin from plugin.samples.integration.another_sample import WrongIntegrationPlugin, NoIntegrationPlugin import plugin.templatetags.plugin_extras as plugin_tags -from plugin import registry, IntegrationPluginBase -from plugin.plugin import IntegrationPluginBase +from plugin import registry, InvenTreePlugin +from plugin.plugin import InvenTreePlugin class PluginTagTests(TestCase): @@ -58,24 +58,24 @@ class PluginTagTests(TestCase): self.assertEqual(plugin_tags.plugin_errors(), registry.errors) -class IntegrationPluginBaseTests(TestCase): - """ Tests for IntegrationPluginBase """ +class InvenTreePluginTests(TestCase): + """ Tests for InvenTreePlugin """ def setUp(self): - self.plugin = IntegrationPluginBase() + self.plugin = InvenTreePlugin() - class NamedPlugin(IntegrationPluginBase): + class NamedPlugin(InvenTreePlugin): """a named plugin""" PLUGIN_NAME = 'abc123' self.named_plugin = NamedPlugin() - class SimpeIntegrationPluginBase(IntegrationPluginBase): + class SimpleInvenTreePlugin(InvenTreePlugin): PLUGIN_NAME = 'SimplePlugin' - self.plugin_simple = SimpeIntegrationPluginBase() + self.plugin_simple = SimpleInvenTreePlugin() - class NameIntegrationPluginBase(IntegrationPluginBase): + class NameInvenTreePlugin(InvenTreePlugin): PLUGIN_NAME = 'Aplugin' PLUGIN_SLUG = 'a' PLUGIN_TITLE = 'a titel' @@ -86,7 +86,7 @@ class IntegrationPluginBaseTests(TestCase): WEBSITE = 'http://aa.bb/cc' LICENSE = 'MIT' - self.plugin_name = NameIntegrationPluginBase() + self.plugin_name = NameInvenTreePlugin() self.plugin_sample = SampleIntegrationPlugin() def test_basic_plugin_init(self): From dfb11cca1dfafdbcc9f66ab6b00ea3ad677cb82a Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 14:40:00 +0200 Subject: [PATCH 033/135] Add legacy --- InvenTree/plugin/__init__.py | 3 ++- InvenTree/plugin/plugin.py | 9 +++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/InvenTree/plugin/__init__.py b/InvenTree/plugin/__init__.py index 3b2f9fe871..ae38ae6e5b 100644 --- a/InvenTree/plugin/__init__.py +++ b/InvenTree/plugin/__init__.py @@ -3,13 +3,14 @@ Utility file to enable simper imports """ from .registry import registry -from .plugin import InvenTreePlugin +from .plugin import InvenTreePlugin, IntegrationPluginBase from .helpers import MixinNotImplementedError, MixinImplementationError __all__ = [ 'registry', 'InvenTreePlugin', + IntegrationPluginBase, 'MixinNotImplementedError', 'MixinImplementationError', ] diff --git a/InvenTree/plugin/plugin.py b/InvenTree/plugin/plugin.py index 45acb55e65..86c9f07936 100644 --- a/InvenTree/plugin/plugin.py +++ b/InvenTree/plugin/plugin.py @@ -7,6 +7,7 @@ import os import inspect from datetime import datetime import pathlib +import warnings from django.conf import settings from django.db.utils import OperationalError, ProgrammingError @@ -336,3 +337,11 @@ class InvenTreePlugin(MixinBase, InvenTreePluginBase): self.package = package self.sign_state = sign_state # endregion + + +class IntegrationPluginBase(InvenTreePlugin): + def __init__(self, *args, **kwargs): + """Send warning about using this reference""" + # TODO remove in 0.8.0 + warnings.warn("This import is deprecated - use InvenTreePlugin", DeprecationWarning) + super().__init__(*args, **kwargs) From f76dcdeb821b5284230e8a97dcb9538ec2c23089 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 14:45:45 +0200 Subject: [PATCH 034/135] move properties to meta --- InvenTree/plugin/plugin.py | 54 +++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 30 deletions(-) diff --git a/InvenTree/plugin/plugin.py b/InvenTree/plugin/plugin.py index 86c9f07936..285896f2f5 100644 --- a/InvenTree/plugin/plugin.py +++ b/InvenTree/plugin/plugin.py @@ -21,14 +21,8 @@ from plugin.helpers import get_git_log, GitStatus logger = logging.getLogger("inventree") -class InvenTreePluginBase: - """ - Base class for a plugin - DO NOT USE THIS DIRECTLY, USE plugin.IntegrationPluginBase - """ - - def __init__(self): - pass +class MetaBase: + """Base class for a plugins metadata""" # Override the plugin name for each concrete plugin instance PLUGIN_NAME = '' @@ -43,6 +37,13 @@ class InvenTreePluginBase: """ return self.PLUGIN_NAME + @property + def name(self): + """ + Name of plugin + """ + return self.plugin_name() + def plugin_slug(self): """ Slug of plugin @@ -56,6 +57,13 @@ class InvenTreePluginBase: return slugify(slug.lower()) + @property + def slug(self): + """ + Slug of plugin + """ + return self.plugin_slug() + def plugin_title(self): """ Title of plugin @@ -66,6 +74,13 @@ class InvenTreePluginBase: else: return self.plugin_name() + @property + def human_name(self): + """ + Human readable name of plugin + """ + return self.plugin_title() + def plugin_config(self): """ Return the PluginConfig object associated with this plugin @@ -143,7 +158,7 @@ class MixinBase: return mixins -class InvenTreePlugin(MixinBase, InvenTreePluginBase): +class InvenTreePlugin(MixinBase, MetaBase): """ The InvenTreePlugin class is used to integrate with 3rd party software @@ -181,27 +196,6 @@ class InvenTreePlugin(MixinBase, InvenTreePluginBase): return path.startswith('plugin/samples/') # 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 of plugin - """ - return self.plugin_title() - @property def description(self): """ From 355695c679f12b409a3055c9adca65c1921f59c1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 14:47:00 +0200 Subject: [PATCH 035/135] make MixinBase betterfor init --- InvenTree/plugin/plugin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/InvenTree/plugin/plugin.py b/InvenTree/plugin/plugin.py index 285896f2f5..703482089e 100644 --- a/InvenTree/plugin/plugin.py +++ b/InvenTree/plugin/plugin.py @@ -116,9 +116,10 @@ class MixinBase: Base set of mixin functions and mechanisms """ - def __init__(self) -> None: + def __init__(self, *args, **kwargs) -> None: self._mixinreg = {} self._mixins = {} + super().__init__(*args, **kwargs) def add_mixin(self, key: str, fnc_enabled=True, cls=None): """ From 9eadcce0eb885b06e36e42d4c51fb6d9c5c52074 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 14:48:36 +0200 Subject: [PATCH 036/135] move mixins --- InvenTree/plugin/plugin.py | 42 ++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/InvenTree/plugin/plugin.py b/InvenTree/plugin/plugin.py index 703482089e..0eefe42080 100644 --- a/InvenTree/plugin/plugin.py +++ b/InvenTree/plugin/plugin.py @@ -121,6 +121,26 @@ class MixinBase: self._mixins = {} super().__init__(*args, **kwargs) + def mixin(self, key): + """ + Check if mixin is registered + """ + return key in self._mixins + + def mixin_enabled(self, key): + """ + Check if mixin is registered, enabled and ready + """ + if self.mixin(key): + fnc_name = self._mixins.get(key) + + # Allow for simple case where the mixin is "always" ready + if fnc_name is True: + return True + + return getattr(self, fnc_name, True) + return False + def add_mixin(self, key: str, fnc_enabled=True, cls=None): """ Add a mixin to the plugins registry @@ -274,28 +294,6 @@ class InvenTreePlugin(MixinBase, MetaBase): """ return f'{reverse("settings")}#select-plugin-{self.slug}' - # region mixins - def mixin(self, key): - """ - Check if mixin is registered - """ - return key in self._mixins - - def mixin_enabled(self, key): - """ - Check if mixin is registered, enabled and ready - """ - if self.mixin(key): - fnc_name = self._mixins.get(key) - - # Allow for simple case where the mixin is "always" ready - if fnc_name is True: - return True - - return getattr(self, fnc_name, True) - return False - # endregion - # region package info def _get_package_commit(self): """ From b1b1db29b9de7097e59631a46852e5005470b38f Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 14:54:11 +0200 Subject: [PATCH 037/135] restructure --- InvenTree/plugin/plugin.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/InvenTree/plugin/plugin.py b/InvenTree/plugin/plugin.py index 0eefe42080..6cb079c05f 100644 --- a/InvenTree/plugin/plugin.py +++ b/InvenTree/plugin/plugin.py @@ -201,21 +201,6 @@ class InvenTreePlugin(MixinBase, MetaBase): self.define_package() - @property - def _is_package(self): - """ - Is the plugin delivered as a package - """ - return getattr(self, 'is_package', False) - - @property - def is_sample(self): - """ - Is this plugin part of the samples? - """ - path = str(self.package_path) - return path.startswith('plugin/samples/') - # region properties @property def description(self): @@ -278,6 +263,21 @@ class InvenTreePlugin(MixinBase, MetaBase): return lic # endregion + @property + def _is_package(self): + """ + Is the plugin delivered as a package + """ + return getattr(self, 'is_package', False) + + @property + def is_sample(self): + """ + Is this plugin part of the samples? + """ + path = str(self.package_path) + return path.startswith('plugin/samples/') + @property def package_path(self): """ From e8c9d71360f3781271019eea7b3c40113d87e0f5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 14:55:45 +0200 Subject: [PATCH 038/135] Adapt docs --- InvenTree/plugin/base/integration/mixins.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/plugin/base/integration/mixins.py b/InvenTree/plugin/base/integration/mixins.py index 32bdd00681..cf8b951a20 100644 --- a/InvenTree/plugin/base/integration/mixins.py +++ b/InvenTree/plugin/base/integration/mixins.py @@ -420,11 +420,11 @@ class APICallMixin: Example: ``` - from plugin import IntegrationPluginBase + from plugin import InvenTreePlugin from plugin.mixins import APICallMixin, SettingsMixin - class SampleApiCallerPlugin(APICallMixin, SettingsMixin, IntegrationPluginBase): + class SampleApiCallerPlugin(APICallMixin, SettingsMixin, InvenTreePlugin): ''' A small api call sample ''' From 5de6cc0d11032e2391927101cbe8a0243ef9cdee Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 14:58:29 +0200 Subject: [PATCH 039/135] remove double def --- InvenTree/plugin/test_plugin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/InvenTree/plugin/test_plugin.py b/InvenTree/plugin/test_plugin.py index 09adbfd2da..84263979a0 100644 --- a/InvenTree/plugin/test_plugin.py +++ b/InvenTree/plugin/test_plugin.py @@ -10,7 +10,6 @@ from plugin.samples.integration.sample import SampleIntegrationPlugin from plugin.samples.integration.another_sample import WrongIntegrationPlugin, NoIntegrationPlugin import plugin.templatetags.plugin_extras as plugin_tags from plugin import registry, InvenTreePlugin -from plugin.plugin import InvenTreePlugin class PluginTagTests(TestCase): From f17ef4585cae6c0faa963b36b7c235f9885da7da Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 16:18:32 +0200 Subject: [PATCH 040/135] fix wrong import --- InvenTree/common/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index d378c7616a..44895af091 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -361,9 +361,9 @@ class BaseInvenTreeSetting(models.Model): filters['user'] = user if plugin is not None: - from plugin import InvenTreePluginBase + from plugin import InvenTreePlugin - if issubclass(plugin.__class__, InvenTreePluginBase): + if issubclass(plugin.__class__, InvenTreePlugin): filters['plugin'] = plugin.plugin_config() else: filters['plugin'] = plugin From 81deb8201e7b44483245918ebc3e3e86040fd3c1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 16:35:38 +0200 Subject: [PATCH 041/135] new references for Meta values in plugin depreceate old values - add depreeation warning --- InvenTree/plugin/plugin.py | 42 +++++++++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/InvenTree/plugin/plugin.py b/InvenTree/plugin/plugin.py index 6cb079c05f..aa4ffe6a1f 100644 --- a/InvenTree/plugin/plugin.py +++ b/InvenTree/plugin/plugin.py @@ -25,17 +25,41 @@ class MetaBase: """Base class for a plugins metadata""" # Override the plugin name for each concrete plugin instance - PLUGIN_NAME = '' + NAME = '' + SLUG = None + TITLE = None - PLUGIN_SLUG = None + def get_meta_value(self, key: str, old_key: str = None, __default = None): + """Reference a meta item with a key - PLUGIN_TITLE = None + Args: + key (str): key for the value + old_key (str, optional): depreceated key - will throw warning + __default (optional): Value if nothing with key can be found. Defaults to None. + + Returns: + Value referenced with key, old_key or __default if set and not value found + """ + value = getattr(self, key, None) + + # The key was not used + if old_key and not value: + value = getattr(self, old_key, None) + + # Sound of a warning if old_key worked + if value: + warnings.warn(f'Usage of {old_key} was depreciated in 0.7.0 in favour of {key}', DeprecationWarning) + + # Use __default if still nothing set + if not value: + return __default + return value def plugin_name(self): """ Name of plugin """ - return self.PLUGIN_NAME + return self.get_meta_value('NAME', 'PLUGIN_NAME') @property def name(self): @@ -50,10 +74,7 @@ class MetaBase: If not set plugin name slugified """ - slug = getattr(self, 'PLUGIN_SLUG', None) - - if slug is None: - slug = self.plugin_name() + slug = self.get_meta_value('SLUG', 'PLUGIN_SLUG', self.plugin_name()) return slugify(slug.lower()) @@ -69,10 +90,7 @@ class MetaBase: Title of plugin """ - if self.PLUGIN_TITLE: - return self.PLUGIN_TITLE - else: - return self.plugin_name() + return self.get_meta_value('TITLE', 'PLUGIN_TITLE', self.plugin_name()) @property def human_name(self): From 1782974df8f3f17b45f25321046d30a9ef3c594b Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 16:46:03 +0200 Subject: [PATCH 042/135] replace depreceated values everywhere --- InvenTree/common/notifications.py | 2 +- InvenTree/plugin/base/action/mixins.py | 2 +- InvenTree/plugin/base/action/test_action.py | 2 +- InvenTree/plugin/base/integration/mixins.py | 4 ++-- InvenTree/plugin/base/integration/test_mixins.py | 2 +- .../plugin/builtin/action/simpleactionplugin.py | 2 +- .../plugin/builtin/barcodes/inventree_barcode.py | 2 +- .../builtin/integration/core_notifications.py | 2 +- InvenTree/plugin/helpers.py | 4 ++-- InvenTree/plugin/registry.py | 10 +++++----- InvenTree/plugin/samples/event/event_sample.py | 6 +++--- .../plugin/samples/integration/another_sample.py | 4 ++-- InvenTree/plugin/samples/integration/api_caller.py | 2 +- .../plugin/samples/integration/broken_sample.py | 6 +++--- .../samples/integration/custom_panel_sample.py | 6 +++--- InvenTree/plugin/samples/integration/sample.py | 6 +++--- .../plugin/samples/integration/scheduled_task.py | 6 +++--- .../samples/integration/test_scheduled_task.py | 2 +- InvenTree/plugin/test_plugin.py | 14 +++++++------- 19 files changed, 42 insertions(+), 42 deletions(-) diff --git a/InvenTree/common/notifications.py b/InvenTree/common/notifications.py index ef4de4fc61..7f6aece282 100644 --- a/InvenTree/common/notifications.py +++ b/InvenTree/common/notifications.py @@ -108,7 +108,7 @@ class NotificationMethod: return False # Check if method globally enabled - plg_instance = registry.plugins.get(plg_cls.PLUGIN_NAME.lower()) + plg_instance = registry.plugins.get(plg_cls.NAME.lower()) if plg_instance and not plg_instance.get_setting(self.GLOBAL_SETTING): return True diff --git a/InvenTree/plugin/base/action/mixins.py b/InvenTree/plugin/base/action/mixins.py index 18a1876659..e2fef5869f 100644 --- a/InvenTree/plugin/base/action/mixins.py +++ b/InvenTree/plugin/base/action/mixins.py @@ -24,7 +24,7 @@ class ActionMixin: Action name for this plugin. If the ACTION_NAME parameter is empty, - uses the PLUGIN_NAME instead. + uses the NAME instead. """ if self.ACTION_NAME: return self.ACTION_NAME diff --git a/InvenTree/plugin/base/action/test_action.py b/InvenTree/plugin/base/action/test_action.py index 24caf175fc..97768e94a0 100644 --- a/InvenTree/plugin/base/action/test_action.py +++ b/InvenTree/plugin/base/action/test_action.py @@ -31,7 +31,7 @@ class ActionMixinTests(TestCase): self.action_plugin = TestActionPlugin('user') class NameActionPlugin(ActionMixin, InvenTreePlugin): - PLUGIN_NAME = 'Aplugin' + NAME = 'Aplugin' self.action_name = NameActionPlugin('user') diff --git a/InvenTree/plugin/base/integration/mixins.py b/InvenTree/plugin/base/integration/mixins.py index cf8b951a20..257493e156 100644 --- a/InvenTree/plugin/base/integration/mixins.py +++ b/InvenTree/plugin/base/integration/mixins.py @@ -373,7 +373,7 @@ class LabelPrintingMixin: """ Mixin which enables direct printing of stock labels. - Each plugin must provide a PLUGIN_NAME attribute, which is used to uniquely identify the printer. + Each plugin must provide a NAME attribute, which is used to uniquely identify the printer. The plugin must also implement the print_label() function """ @@ -428,7 +428,7 @@ class APICallMixin: ''' A small api call sample ''' - PLUGIN_NAME = "Sample API Caller" + NAME = "Sample API Caller" SETTINGS = { 'API_TOKEN': { diff --git a/InvenTree/plugin/base/integration/test_mixins.py b/InvenTree/plugin/base/integration/test_mixins.py index 2283a3a1d2..b87583f301 100644 --- a/InvenTree/plugin/base/integration/test_mixins.py +++ b/InvenTree/plugin/base/integration/test_mixins.py @@ -147,7 +147,7 @@ class APICallMixinTest(BaseMixinDefinition, TestCase): def setUp(self): class MixinCls(APICallMixin, SettingsMixin, InvenTreePlugin): - PLUGIN_NAME = "Sample API Caller" + NAME = "Sample API Caller" SETTINGS = { 'API_TOKEN': { diff --git a/InvenTree/plugin/builtin/action/simpleactionplugin.py b/InvenTree/plugin/builtin/action/simpleactionplugin.py index ef69511cfe..d2a321789d 100644 --- a/InvenTree/plugin/builtin/action/simpleactionplugin.py +++ b/InvenTree/plugin/builtin/action/simpleactionplugin.py @@ -10,7 +10,7 @@ class SimpleActionPlugin(ActionMixin, InvenTreePlugin): the capability of the ActionMixin class """ - PLUGIN_NAME = "SimpleActionPlugin" + NAME = "SimpleActionPlugin" ACTION_NAME = "simple" def perform_action(self): diff --git a/InvenTree/plugin/builtin/barcodes/inventree_barcode.py b/InvenTree/plugin/builtin/barcodes/inventree_barcode.py index 6b84f4a8c2..f9f498ccd8 100644 --- a/InvenTree/plugin/builtin/barcodes/inventree_barcode.py +++ b/InvenTree/plugin/builtin/barcodes/inventree_barcode.py @@ -24,7 +24,7 @@ from rest_framework.exceptions import ValidationError class InvenTreeBarcodePlugin(BarcodeMixin, InvenTreePlugin): - PLUGIN_NAME = "InvenTreeBarcode" + NAME = "InvenTreeBarcode" def validate(self): """ diff --git a/InvenTree/plugin/builtin/integration/core_notifications.py b/InvenTree/plugin/builtin/integration/core_notifications.py index dbde7747e9..67b61eea3b 100644 --- a/InvenTree/plugin/builtin/integration/core_notifications.py +++ b/InvenTree/plugin/builtin/integration/core_notifications.py @@ -20,7 +20,7 @@ class CoreNotificationsPlugin(SettingsMixin, InvenTreePlugin): Core notification methods for InvenTree """ - PLUGIN_NAME = "CoreNotificationsPlugin" + NAME = "CoreNotificationsPlugin" AUTHOR = _('InvenTree contributors') DESCRIPTION = _('Integrated outgoing notificaton methods') diff --git a/InvenTree/plugin/helpers.py b/InvenTree/plugin/helpers.py index db6f9635a2..1217fa4d47 100644 --- a/InvenTree/plugin/helpers.py +++ b/InvenTree/plugin/helpers.py @@ -205,7 +205,7 @@ 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 + - Modules must have a non-empty NAME parameter """ plugins = [] @@ -217,7 +217,7 @@ def get_plugins(pkg, baseclass): # Iterate through each class in the module for item in get_classes(mod): plugin = item[1] - if issubclass(plugin, baseclass) and plugin.PLUGIN_NAME: + if issubclass(plugin, baseclass) and plugin.NAME: plugins.append(plugin) return plugins diff --git a/InvenTree/plugin/registry.py b/InvenTree/plugin/registry.py index 4975144bfc..a17f802baf 100644 --- a/InvenTree/plugin/registry.py +++ b/InvenTree/plugin/registry.py @@ -281,8 +281,8 @@ class PluginsRegistry: # Check if activated # These checks only use attributes - never use plugin supplied functions -> that would lead to arbitrary code execution!! - plug_name = plugin.PLUGIN_NAME - plug_key = plugin.PLUGIN_SLUG if getattr(plugin, 'PLUGIN_SLUG', None) else plug_name + plug_name = plugin.NAME + plug_key = plugin.SLUG if getattr(plugin, 'SLUG', None) else plug_name plug_key = slugify(plug_key) # keys are slugs! try: plugin_db_setting, _ = PluginConfig.objects.get_or_create(key=plug_key, name=plug_name) @@ -314,7 +314,7 @@ class PluginsRegistry: # now we can be sure that an admin has activated the plugin # TODO check more stuff -> as of Nov 2021 there are not many checks in place # but we could enhance those to check signatures, run the plugin against a whitelist etc. - logger.info(f'Loading integration plugin {plugin.PLUGIN_NAME}') + logger.info(f'Loading plugin {plug_name}') try: plugin = plugin() @@ -322,7 +322,7 @@ class PluginsRegistry: # log error and raise it -> disable plugin handle_error(error, log_name='init') - logger.debug(f'Loaded integration plugin {plugin.PLUGIN_NAME}') + logger.debug(f'Loaded plugin {plug_name}') plugin.is_package = was_packaged @@ -516,7 +516,7 @@ class PluginsRegistry: plugin_path = '.'.join(pathlib.Path(plugin.path).relative_to(settings.BASE_DIR).parts) except ValueError: # pragma: no cover # plugin is shipped as package - plugin_path = plugin.PLUGIN_NAME + plugin_path = plugin.NAME return plugin_path def deactivate_integration_app(self): diff --git a/InvenTree/plugin/samples/event/event_sample.py b/InvenTree/plugin/samples/event/event_sample.py index 2e629a48a4..5411781e05 100644 --- a/InvenTree/plugin/samples/event/event_sample.py +++ b/InvenTree/plugin/samples/event/event_sample.py @@ -11,9 +11,9 @@ class EventPluginSample(EventMixin, InvenTreePlugin): A sample plugin which provides supports for triggered events """ - PLUGIN_NAME = "EventPlugin" - PLUGIN_SLUG = "event" - PLUGIN_TITLE = "Triggered Events" + NAME = "EventPlugin" + SLUG = "event" + TITLE = "Triggered Events" def process_event(self, event, *args, **kwargs): """ Custom event processing """ diff --git a/InvenTree/plugin/samples/integration/another_sample.py b/InvenTree/plugin/samples/integration/another_sample.py index d36dbf13e8..f6f9306a95 100644 --- a/InvenTree/plugin/samples/integration/another_sample.py +++ b/InvenTree/plugin/samples/integration/another_sample.py @@ -8,7 +8,7 @@ class NoIntegrationPlugin(InvenTreePlugin): An basic integration plugin """ - PLUGIN_NAME = "NoIntegrationPlugin" + NAME = "NoIntegrationPlugin" class WrongIntegrationPlugin(UrlsMixin, InvenTreePlugin): @@ -16,4 +16,4 @@ class WrongIntegrationPlugin(UrlsMixin, InvenTreePlugin): An basic integration plugin """ - PLUGIN_NAME = "WrongIntegrationPlugin" + NAME = "WrongIntegrationPlugin" diff --git a/InvenTree/plugin/samples/integration/api_caller.py b/InvenTree/plugin/samples/integration/api_caller.py index 3de07508a0..98a145de34 100644 --- a/InvenTree/plugin/samples/integration/api_caller.py +++ b/InvenTree/plugin/samples/integration/api_caller.py @@ -9,7 +9,7 @@ class SampleApiCallerPlugin(APICallMixin, SettingsMixin, InvenTreePlugin): """ A small api call sample """ - PLUGIN_NAME = "Sample API Caller" + NAME = "Sample API Caller" SETTINGS = { 'API_TOKEN': { diff --git a/InvenTree/plugin/samples/integration/broken_sample.py b/InvenTree/plugin/samples/integration/broken_sample.py index 50aecc3c35..0d17d661ae 100644 --- a/InvenTree/plugin/samples/integration/broken_sample.py +++ b/InvenTree/plugin/samples/integration/broken_sample.py @@ -6,9 +6,9 @@ class BrokenIntegrationPlugin(InvenTreePlugin): """ An very broken integration plugin """ - PLUGIN_NAME = 'Test' - PLUGIN_TITLE = 'Broken Plugin' - PLUGIN_SLUG = 'broken' + NAME = 'Test' + TITLE = 'Broken Plugin' + SLUG = 'broken' def __init__(self): super().__init__() diff --git a/InvenTree/plugin/samples/integration/custom_panel_sample.py b/InvenTree/plugin/samples/integration/custom_panel_sample.py index 36793ddf6a..0203fc4e04 100644 --- a/InvenTree/plugin/samples/integration/custom_panel_sample.py +++ b/InvenTree/plugin/samples/integration/custom_panel_sample.py @@ -14,9 +14,9 @@ class CustomPanelSample(PanelMixin, SettingsMixin, InvenTreePlugin): A sample plugin which renders some custom panels. """ - PLUGIN_NAME = "CustomPanelExample" - PLUGIN_SLUG = "panel" - PLUGIN_TITLE = "Custom Panel Example" + NAME = "CustomPanelExample" + SLUG = "panel" + TITLE = "Custom Panel Example" DESCRIPTION = "An example plugin demonstrating how custom panels can be added to the user interface" VERSION = "0.1" diff --git a/InvenTree/plugin/samples/integration/sample.py b/InvenTree/plugin/samples/integration/sample.py index 115a943f87..0d83c262d5 100644 --- a/InvenTree/plugin/samples/integration/sample.py +++ b/InvenTree/plugin/samples/integration/sample.py @@ -15,9 +15,9 @@ class SampleIntegrationPlugin(AppMixin, SettingsMixin, UrlsMixin, NavigationMixi A full integration plugin example """ - PLUGIN_NAME = "SampleIntegrationPlugin" - PLUGIN_SLUG = "sample" - PLUGIN_TITLE = "Sample Plugin" + NAME = "SampleIntegrationPlugin" + SLUG = "sample" + TITLE = "Sample Plugin" NAVIGATION_TAB_NAME = "Sample Nav" NAVIGATION_TAB_ICON = 'fas fa-plus' diff --git a/InvenTree/plugin/samples/integration/scheduled_task.py b/InvenTree/plugin/samples/integration/scheduled_task.py index fe1069efb8..2a59f820c6 100644 --- a/InvenTree/plugin/samples/integration/scheduled_task.py +++ b/InvenTree/plugin/samples/integration/scheduled_task.py @@ -20,9 +20,9 @@ class ScheduledTaskPlugin(ScheduleMixin, SettingsMixin, InvenTreePlugin): A sample plugin which provides support for scheduled tasks """ - PLUGIN_NAME = "ScheduledTasksPlugin" - PLUGIN_SLUG = "schedule" - PLUGIN_TITLE = "Scheduled Tasks" + NAME = "ScheduledTasksPlugin" + SLUG = "schedule" + TITLE = "Scheduled Tasks" SCHEDULED_TASKS = { 'member': { diff --git a/InvenTree/plugin/samples/integration/test_scheduled_task.py b/InvenTree/plugin/samples/integration/test_scheduled_task.py index c4d234b0ba..5be1dce250 100644 --- a/InvenTree/plugin/samples/integration/test_scheduled_task.py +++ b/InvenTree/plugin/samples/integration/test_scheduled_task.py @@ -54,7 +54,7 @@ class ScheduledTaskPluginTests(TestCase): def test_init(self): """Check that all MixinImplementationErrors raise""" class Base(ScheduleMixin, InvenTreePlugin): - PLUGIN_NAME = 'APlugin' + NAME = 'APlugin' class NoSchedules(Base): """Plugin without schedules""" diff --git a/InvenTree/plugin/test_plugin.py b/InvenTree/plugin/test_plugin.py index 84263979a0..f93ad3c67c 100644 --- a/InvenTree/plugin/test_plugin.py +++ b/InvenTree/plugin/test_plugin.py @@ -65,19 +65,19 @@ class InvenTreePluginTests(TestCase): class NamedPlugin(InvenTreePlugin): """a named plugin""" - PLUGIN_NAME = 'abc123' + NAME = 'abc123' self.named_plugin = NamedPlugin() class SimpleInvenTreePlugin(InvenTreePlugin): - PLUGIN_NAME = 'SimplePlugin' + NAME = 'SimplePlugin' self.plugin_simple = SimpleInvenTreePlugin() class NameInvenTreePlugin(InvenTreePlugin): - PLUGIN_NAME = 'Aplugin' - PLUGIN_SLUG = 'a' - PLUGIN_TITLE = 'a titel' + NAME = 'Aplugin' + SLUG = 'a' + TITLE = 'a titel' PUBLISH_DATE = "1111-11-11" AUTHOR = 'AA BB' DESCRIPTION = 'A description' @@ -90,12 +90,12 @@ class InvenTreePluginTests(TestCase): def test_basic_plugin_init(self): """check if a basic plugin intis""" - self.assertEqual(self.plugin.PLUGIN_NAME, '') + self.assertEqual(self.plugin.NAME, '') self.assertEqual(self.plugin.plugin_name(), '') def test_basic_plugin_name(self): """check if the name of a basic plugin can be set""" - self.assertEqual(self.named_plugin.PLUGIN_NAME, 'abc123') + self.assertEqual(self.named_plugin.NAME, 'abc123') self.assertEqual(self.named_plugin.plugin_name(), 'abc123') def test_basic_is_active(self): From 37f72e9652e59bacaa50aa55ce959dd797579142 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 16:50:36 +0200 Subject: [PATCH 043/135] remove integration naming from docs --- InvenTree/plugin/registry.py | 19 ++++++------------- .../samples/integration/another_sample.py | 4 ++-- .../plugin/samples/integration/broken_file.py | 2 +- .../samples/integration/broken_sample.py | 4 ++-- .../plugin/samples/integration/sample.py | 2 +- .../plugin/templatetags/plugin_extras.py | 4 ++-- 6 files changed, 14 insertions(+), 21 deletions(-) diff --git a/InvenTree/plugin/registry.py b/InvenTree/plugin/registry.py index a17f802baf..c942e98b37 100644 --- a/InvenTree/plugin/registry.py +++ b/InvenTree/plugin/registry.py @@ -51,7 +51,6 @@ class PluginsRegistry: self.apps_loading = True # Marks if apps were reloaded yet self.git_is_modern = True # Is a modern version of git available - # integration specific self.installed_apps = [] # Holds all added plugin_paths # mixins @@ -123,7 +122,7 @@ class PluginsRegistry: 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 + # Initialize apps without any plugins self._clean_registry() self._clean_installed_apps() self._activate_plugins(force_reload=True) @@ -192,9 +191,7 @@ class PluginsRegistry: logger.info('Finished reloading plugins') def collect_plugins(self): - """ - Collect integration plugins from all possible ways of loading - """ + """Collect plugins from all possible ways of loading""" if not settings.PLUGINS_ENABLED: # Plugins not enabled, do nothing @@ -274,7 +271,7 @@ class PluginsRegistry: logger.info('Starting plugin initialisation') - # Initialize integration plugins + # Initialize plugins for plugin in self.plugin_modules: # Check if package was_packaged = getattr(plugin, 'is_package', False) @@ -337,7 +334,7 @@ class PluginsRegistry: def _activate_plugins(self, force_reload=False): """ - Run integration functions for all plugins + Run activation functions for all plugins :param force_reload: force reload base apps, defaults to False :type force_reload: bool, optional @@ -351,9 +348,7 @@ class PluginsRegistry: self.activate_integration_app(plugins, force_reload=force_reload) def _deactivate_plugins(self): - """ - Run integration deactivation functions for all plugins - """ + """Run deactivation functions for all plugins""" self.deactivate_integration_app() self.deactivate_integration_schedule() @@ -520,9 +515,7 @@ class PluginsRegistry: return plugin_path def deactivate_integration_app(self): - """ - Deactivate integration app - some magic required - """ + """Deactivate AppMixin plugins - some magic required""" # unregister models from admin for plugin_path in self.installed_apps: diff --git a/InvenTree/plugin/samples/integration/another_sample.py b/InvenTree/plugin/samples/integration/another_sample.py index f6f9306a95..0cc5ce21c3 100644 --- a/InvenTree/plugin/samples/integration/another_sample.py +++ b/InvenTree/plugin/samples/integration/another_sample.py @@ -5,7 +5,7 @@ from plugin.mixins import UrlsMixin class NoIntegrationPlugin(InvenTreePlugin): """ - An basic integration plugin + An basic plugin """ NAME = "NoIntegrationPlugin" @@ -13,7 +13,7 @@ class NoIntegrationPlugin(InvenTreePlugin): class WrongIntegrationPlugin(UrlsMixin, InvenTreePlugin): """ - An basic integration plugin + An basic wron plugin with urls """ NAME = "WrongIntegrationPlugin" diff --git a/InvenTree/plugin/samples/integration/broken_file.py b/InvenTree/plugin/samples/integration/broken_file.py index f28c72bef6..52c6005771 100644 --- a/InvenTree/plugin/samples/integration/broken_file.py +++ b/InvenTree/plugin/samples/integration/broken_file.py @@ -4,7 +4,7 @@ from plugin import InvenTreePlugin class BrokenFileIntegrationPlugin(InvenTreePlugin): """ - An very broken integration plugin + An very broken plugin """ diff --git a/InvenTree/plugin/samples/integration/broken_sample.py b/InvenTree/plugin/samples/integration/broken_sample.py index 0d17d661ae..ebd7821fe0 100644 --- a/InvenTree/plugin/samples/integration/broken_sample.py +++ b/InvenTree/plugin/samples/integration/broken_sample.py @@ -1,10 +1,10 @@ -"""sample of a broken integration plugin""" +"""sample of a broken plugin""" from plugin import InvenTreePlugin class BrokenIntegrationPlugin(InvenTreePlugin): """ - An very broken integration plugin + An very broken plugin """ NAME = 'Test' TITLE = 'Broken Plugin' diff --git a/InvenTree/plugin/samples/integration/sample.py b/InvenTree/plugin/samples/integration/sample.py index 0d83c262d5..38ed259522 100644 --- a/InvenTree/plugin/samples/integration/sample.py +++ b/InvenTree/plugin/samples/integration/sample.py @@ -12,7 +12,7 @@ from django.urls import include, re_path class SampleIntegrationPlugin(AppMixin, SettingsMixin, UrlsMixin, NavigationMixin, InvenTreePlugin): """ - A full integration plugin example + A full plugin example """ NAME = "SampleIntegrationPlugin" diff --git a/InvenTree/plugin/templatetags/plugin_extras.py b/InvenTree/plugin/templatetags/plugin_extras.py index a30f7ec2e4..3516aab0e3 100644 --- a/InvenTree/plugin/templatetags/plugin_extras.py +++ b/InvenTree/plugin/templatetags/plugin_extras.py @@ -16,7 +16,7 @@ register = template.Library() @register.simple_tag() def plugin_list(*args, **kwargs): """ - List of all installed integration plugins + List of all installed plugins """ return registry.plugins @@ -24,7 +24,7 @@ def plugin_list(*args, **kwargs): @register.simple_tag() def inactive_plugin_list(*args, **kwargs): """ - List of all inactive integration plugins + List of all inactive plugins """ return registry.plugins_inactive From 175066ba19d249a7e8e192bff10970f71bd3744e Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 16:51:50 +0200 Subject: [PATCH 044/135] rename functions --- InvenTree/plugin/registry.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/InvenTree/plugin/registry.py b/InvenTree/plugin/registry.py index c942e98b37..da7fb90dbb 100644 --- a/InvenTree/plugin/registry.py +++ b/InvenTree/plugin/registry.py @@ -343,20 +343,20 @@ class PluginsRegistry: plugins = self.plugins.items() logger.info(f'Found {len(plugins)} active plugins') - self.activate_integration_settings(plugins) - self.activate_integration_schedule(plugins) - self.activate_integration_app(plugins, force_reload=force_reload) + self.activate_plugin_settings(plugins) + self.activate_plugin_schedule(plugins) + self.activate_plugin_app(plugins, force_reload=force_reload) def _deactivate_plugins(self): """Run deactivation functions for all plugins""" - self.deactivate_integration_app() - self.deactivate_integration_schedule() - self.deactivate_integration_settings() + self.deactivate_plugin_app() + self.deactivate_plugin_schedule() + self.deactivate_plugin_settings() # endregion # region mixin specific loading ... - def activate_integration_settings(self, plugins): + def activate_plugin_settings(self, plugins): logger.info('Activating plugin settings') @@ -367,7 +367,7 @@ class PluginsRegistry: plugin_setting = plugin.settings self.mixins_settings[slug] = plugin_setting - def deactivate_integration_settings(self): + def deactivate_plugin_settings(self): # collect all settings plugin_settings = {} @@ -378,7 +378,7 @@ class PluginsRegistry: # clear cache self.mixins_settings = {} - def activate_integration_schedule(self, plugins): + def activate_plugin_schedule(self, plugins): logger.info('Activating plugin tasks') @@ -422,14 +422,14 @@ class PluginsRegistry: # Database might not yet be ready logger.warning("activate_integration_schedule failed, database not ready") - def deactivate_integration_schedule(self): + def deactivate_plugin_schedule(self): """ Deactivate ScheduleMixin currently nothing is done """ pass - def activate_integration_app(self, plugins, force_reload=False): + def activate_plugin_app(self, plugins, force_reload=False): """ Activate AppMixin plugins - add custom apps and reload @@ -514,7 +514,7 @@ class PluginsRegistry: plugin_path = plugin.NAME return plugin_path - def deactivate_integration_app(self): + def deactivate_plugin_app(self): """Deactivate AppMixin plugins - some magic required""" # unregister models from admin From 272d2f79cc8b8ce4c0da887ced78e809dcabd33d Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 16:55:38 +0200 Subject: [PATCH 045/135] patch mixin --- InvenTree/plugin/base/action/mixins.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/InvenTree/plugin/base/action/mixins.py b/InvenTree/plugin/base/action/mixins.py index e2fef5869f..70fea86a7e 100644 --- a/InvenTree/plugin/base/action/mixins.py +++ b/InvenTree/plugin/base/action/mixins.py @@ -15,9 +15,10 @@ class ActionMixin: """ MIXIN_NAME = 'Actions' - def __init__(self): + def __init__(self, user=None, data=None): super().__init__() self.add_mixin('action', True, __class__) + self.init(user, data) def action_name(self): """ From 5cb4f0e73d5b1a38ae3925cd66a8656353d0ac6b Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 16:57:23 +0200 Subject: [PATCH 046/135] PEP fix --- InvenTree/plugin/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/plugin/plugin.py b/InvenTree/plugin/plugin.py index aa4ffe6a1f..2acebcbd47 100644 --- a/InvenTree/plugin/plugin.py +++ b/InvenTree/plugin/plugin.py @@ -29,7 +29,7 @@ class MetaBase: SLUG = None TITLE = None - def get_meta_value(self, key: str, old_key: str = None, __default = None): + def get_meta_value(self, key: str, old_key: str = None, __default=None): """Reference a meta item with a key Args: From 2a551e7ab981dff422cfce6689f16418d9426850 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 17:08:12 +0200 Subject: [PATCH 047/135] Add test to replace removed coverage --- InvenTree/InvenTree/test_api.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/InvenTree/InvenTree/test_api.py b/InvenTree/InvenTree/test_api.py index 0e813d48de..9a285963aa 100644 --- a/InvenTree/InvenTree/test_api.py +++ b/InvenTree/InvenTree/test_api.py @@ -96,6 +96,13 @@ class HTMLAPITests(TestCase): response = self.client.get(url, HTTP_ACCEPT='text/html') self.assertEqual(response.status_code, 200) + def test_not_found(self): + """Test that the NotFoundView is working""" + url = reverse('api-404') + + # Check JSON response + response = self.client.get(url, HTTP_ACCEPT='application/json') + self.assertEqual(response.status_code, 404) class APITests(InvenTreeAPITestCase): """ Tests for the InvenTree API """ From d7948902641ecbb63a07479490c37fdbd78e01a6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 17:38:42 +0200 Subject: [PATCH 048/135] fix assertations --- InvenTree/plugin/plugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/plugin/plugin.py b/InvenTree/plugin/plugin.py index 2acebcbd47..f949781c10 100644 --- a/InvenTree/plugin/plugin.py +++ b/InvenTree/plugin/plugin.py @@ -43,7 +43,7 @@ class MetaBase: value = getattr(self, key, None) # The key was not used - if old_key and not value: + if old_key and value is None: value = getattr(self, old_key, None) # Sound of a warning if old_key worked @@ -51,7 +51,7 @@ class MetaBase: warnings.warn(f'Usage of {old_key} was depreciated in 0.7.0 in favour of {key}', DeprecationWarning) # Use __default if still nothing set - if not value: + if (value is None) and __default: return __default return value From 50156466dbd2edff31fd15f8fb01495ac98d23b9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 17:51:23 +0200 Subject: [PATCH 049/135] fix title asks --- InvenTree/plugin/plugin.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/InvenTree/plugin/plugin.py b/InvenTree/plugin/plugin.py index f949781c10..b008865f5c 100644 --- a/InvenTree/plugin/plugin.py +++ b/InvenTree/plugin/plugin.py @@ -74,7 +74,9 @@ class MetaBase: If not set plugin name slugified """ - slug = self.get_meta_value('SLUG', 'PLUGIN_SLUG', self.plugin_name()) + slug = self.get_meta_value('SLUG', 'PLUGIN_SLUG', None) + if not slug: + slug = self.plugin_name() return slugify(slug.lower()) @@ -90,7 +92,10 @@ class MetaBase: Title of plugin """ - return self.get_meta_value('TITLE', 'PLUGIN_TITLE', self.plugin_name()) + title = self.get_meta_value('TITLE', 'PLUGIN_TITLE', None) + if title: + return title + return self.plugin_name() @property def human_name(self): From 993f6380dbf28a1523a2a283b74835ed76c405e2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 17:53:28 +0200 Subject: [PATCH 050/135] pep fix --- InvenTree/InvenTree/test_api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/InvenTree/InvenTree/test_api.py b/InvenTree/InvenTree/test_api.py index 9a285963aa..fa351ebb3c 100644 --- a/InvenTree/InvenTree/test_api.py +++ b/InvenTree/InvenTree/test_api.py @@ -104,6 +104,7 @@ class HTMLAPITests(TestCase): response = self.client.get(url, HTTP_ACCEPT='application/json') self.assertEqual(response.status_code, 404) + class APITests(InvenTreeAPITestCase): """ Tests for the InvenTree API """ From e859b9975bd007401a54a2078cf73fa95748a99c Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Wed, 11 May 2022 19:03:07 +0200 Subject: [PATCH 051/135] Update test_api.py --- InvenTree/InvenTree/test_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/test_api.py b/InvenTree/InvenTree/test_api.py index fa351ebb3c..945b7a875f 100644 --- a/InvenTree/InvenTree/test_api.py +++ b/InvenTree/InvenTree/test_api.py @@ -102,7 +102,7 @@ class HTMLAPITests(TestCase): # Check JSON response response = self.client.get(url, HTTP_ACCEPT='application/json') - self.assertEqual(response.status_code, 404) + self.assertEqual(response.status_code, 200) class APITests(InvenTreeAPITestCase): From 036b58ea042f7aba83ab5445d60e9c518bcf30ed Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 21:52:49 +0200 Subject: [PATCH 052/135] update path in test --- InvenTree/InvenTree/test_api.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/InvenTree/InvenTree/test_api.py b/InvenTree/InvenTree/test_api.py index fa351ebb3c..f55dfbcda2 100644 --- a/InvenTree/InvenTree/test_api.py +++ b/InvenTree/InvenTree/test_api.py @@ -98,10 +98,8 @@ class HTMLAPITests(TestCase): def test_not_found(self): """Test that the NotFoundView is working""" - url = reverse('api-404') - # Check JSON response - response = self.client.get(url, HTTP_ACCEPT='application/json') + response = self.client.get('/api/anc') self.assertEqual(response.status_code, 404) From 63fa527fa4fd02b3376cc99e6e08c92308a8d081 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 21:58:03 +0200 Subject: [PATCH 053/135] ignore for cover --- InvenTree/InvenTree/apps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/apps.py b/InvenTree/InvenTree/apps.py index 7787bbfb0c..8397f14429 100644 --- a/InvenTree/InvenTree/apps.py +++ b/InvenTree/InvenTree/apps.py @@ -192,7 +192,7 @@ class InvenTreeConfig(AppConfig): with transaction.atomic(): new_user = user.objects.create_superuser(add_user, add_email, add_password) logger.info(f'User {str(new_user)} was created!') - except IntegrityError as _e: + except IntegrityError as _e: # pragma: no cover logger.warning(f'The user "{add_user}" could not be created due to the following error:\n{str(_e)}') if settings.TESTING_ENV: raise _e From 86f663e8343d13da6c80bce59d81cc72d035a48e Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 21:58:14 +0200 Subject: [PATCH 054/135] this was only used for testing --- InvenTree/InvenTree/apps.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/InvenTree/InvenTree/apps.py b/InvenTree/InvenTree/apps.py index 8397f14429..e068d44e4f 100644 --- a/InvenTree/InvenTree/apps.py +++ b/InvenTree/InvenTree/apps.py @@ -194,8 +194,6 @@ class InvenTreeConfig(AppConfig): logger.info(f'User {str(new_user)} was created!') except IntegrityError as _e: # pragma: no cover logger.warning(f'The user "{add_user}" could not be created due to the following error:\n{str(_e)}') - if settings.TESTING_ENV: - raise _e # do not try again settings.USER_ADDED = True From 8366620c1d3ee4d21510a43c560c7d9fdd61985b Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 22:02:41 +0200 Subject: [PATCH 055/135] add test for user creation --- InvenTree/InvenTree/tests.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py index dc3aff85e6..707960cb28 100644 --- a/InvenTree/InvenTree/tests.py +++ b/InvenTree/InvenTree/tests.py @@ -481,6 +481,16 @@ class TestSettings(TestCase): self.run_reload() self.assertEqual(user_count(), 1) + # create user manually + self.user_mdl.objects.create_user('testuser', 'test@testing.com', 'password') + self.assertEqual(user_count(), 2) + # check it will not be created again + self.env.set('INVENTREE_ADMIN_USER', 'testuser') + self.env.set('INVENTREE_ADMIN_EMAIL', 'test@testing.com') + self.env.set('INVENTREE_ADMIN_PASSWORD', 'password') + self.run_reload() + self.assertEqual(user_count(), 2) + # make sure to clean up settings.TESTING_ENV = False From da885456b7c4980a34eef18f65fe342b5f75a2ea Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 22:47:41 +0200 Subject: [PATCH 056/135] tests for template --- InvenTree/plugin/test_helpers.py | 23 +++++++++++++++++++++++ InvenTree/templates/sample/sample.html | 1 + 2 files changed, 24 insertions(+) create mode 100644 InvenTree/plugin/test_helpers.py create mode 100644 InvenTree/templates/sample/sample.html diff --git a/InvenTree/plugin/test_helpers.py b/InvenTree/plugin/test_helpers.py new file mode 100644 index 0000000000..9ed0ff16fe --- /dev/null +++ b/InvenTree/plugin/test_helpers.py @@ -0,0 +1,23 @@ +"""Unit tests for helpers.py""" + +from django.test import TestCase + +from .helpers import render_template + + +class HelperTests(TestCase): + """Tests for helpers""" + + def test_render_template(self): + """Check if render_template helper works""" + class ErrorSource: + slug = 'sampleplg' + + # working sample + response = render_template(ErrorSource(), 'sample/sample.html', {'abc': 123} ) + self.assertEqual(response, '

123

') + + # Wrong sample + response = render_template(ErrorSource(), 'sample/wrongsample.html', {'abc': 123} ) + self.assertTrue('lert alert-block alert-danger' in response) + self.assertTrue('Template file sample/wrongsample.html' in response) diff --git a/InvenTree/templates/sample/sample.html b/InvenTree/templates/sample/sample.html new file mode 100644 index 0000000000..0db05e3152 --- /dev/null +++ b/InvenTree/templates/sample/sample.html @@ -0,0 +1 @@ +

{{abc}}

\ No newline at end of file From 8a91fc362b18683af388794e7fc909d842d4289d Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 23:05:08 +0200 Subject: [PATCH 057/135] test plugin model more --- InvenTree/plugin/test_api.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/InvenTree/plugin/test_api.py b/InvenTree/plugin/test_api.py index f04457f3f8..31fe61b469 100644 --- a/InvenTree/plugin/test_api.py +++ b/InvenTree/plugin/test_api.py @@ -124,3 +124,22 @@ class PluginDetailAPITest(InvenTreeAPITestCase): '_save': 'Save', }, follow=True) self.assertEqual(response.status_code, 200) + + def test_model(self): + """ + Test the PluginConfig model + """ + from plugin.models import PluginConfig + from plugin import registry + + fixtures = PluginConfig.objects.all() + + # check if plugins were registered + if not fixtures: + registry.reload_plugins() + fixtures = PluginConfig.objects.all() + + plg = fixtures.first() + mixin_dict = plg.mixins() + self.assertIn('base', mixin_dict) + self.assertDictContainsSubset({'base':{'key':'base', 'human_name':'base'}}, mixin_dict) From baca0dc2689c96b92a3e52eadd1dbdb981e8e9ec Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 23:05:22 +0200 Subject: [PATCH 058/135] do not cover error --- InvenTree/plugin/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/plugin/models.py b/InvenTree/plugin/models.py index 1c5dff6b68..90fe447103 100644 --- a/InvenTree/plugin/models.py +++ b/InvenTree/plugin/models.py @@ -59,7 +59,7 @@ class PluginConfig(models.Model): try: return self.plugin._mixinreg - except (AttributeError, ValueError): + except (AttributeError, ValueError): # pragma: no cover return {} # functions From 4397f57acc910100b3b84c423f4e0e2bfdef72c3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 23:11:54 +0200 Subject: [PATCH 059/135] test that a save() action on a plugin is reloading --- InvenTree/plugin/test_api.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/InvenTree/plugin/test_api.py b/InvenTree/plugin/test_api.py index 31fe61b469..c767f0f8c0 100644 --- a/InvenTree/plugin/test_api.py +++ b/InvenTree/plugin/test_api.py @@ -139,7 +139,15 @@ class PluginDetailAPITest(InvenTreeAPITestCase): registry.reload_plugins() fixtures = PluginConfig.objects.all() + # check mixin registry plg = fixtures.first() mixin_dict = plg.mixins() self.assertIn('base', mixin_dict) self.assertDictContainsSubset({'base':{'key':'base', 'human_name':'base'}}, mixin_dict) + + # check reload on save + with self.assertWarns('A reload was triggered'): + plg_inactive = fixtures.filter(active=False).first() + plg_inactive.active = True + plg_inactive.save() + print('done') From 432a9b2e4dfad2d309b85a85ce12442942afaf43 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 23:12:21 +0200 Subject: [PATCH 060/135] add warning if plugin testing - is need to probe for --- InvenTree/plugin/models.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/InvenTree/plugin/models.py b/InvenTree/plugin/models.py index 90fe447103..cacaabb1d8 100644 --- a/InvenTree/plugin/models.py +++ b/InvenTree/plugin/models.py @@ -4,10 +4,12 @@ Plugin model definitions # -*- coding: utf-8 -*- from __future__ import unicode_literals +from logging import warning from django.utils.translation import gettext_lazy as _ from django.db import models from django.contrib.auth.models import User +from django.conf import settings import common.models @@ -97,6 +99,8 @@ class PluginConfig(models.Model): if not reload: if (self.active is False and self.__org_active is True) or \ (self.active is True and self.__org_active is False): + if settings.PLUGIN_TESTING: + warning('A reload was triggered') registry.reload_plugins() return ret From 912d0625f06222b2d0ed81c855a6f9596a9cb3fa Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 23:12:27 +0200 Subject: [PATCH 061/135] PEP fix --- InvenTree/plugin/test_helpers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/plugin/test_helpers.py b/InvenTree/plugin/test_helpers.py index 9ed0ff16fe..5187b30619 100644 --- a/InvenTree/plugin/test_helpers.py +++ b/InvenTree/plugin/test_helpers.py @@ -14,10 +14,10 @@ class HelperTests(TestCase): slug = 'sampleplg' # working sample - response = render_template(ErrorSource(), 'sample/sample.html', {'abc': 123} ) + response = render_template(ErrorSource(), 'sample/sample.html', {'abc': 123}) self.assertEqual(response, '

123

') # Wrong sample - response = render_template(ErrorSource(), 'sample/wrongsample.html', {'abc': 123} ) + response = render_template(ErrorSource(), 'sample/wrongsample.html', {'abc': 123}) self.assertTrue('lert alert-block alert-danger' in response) self.assertTrue('Template file sample/wrongsample.html' in response) From f8e51c087337baed9b32af78ab550d896f70a663 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 23:21:42 +0200 Subject: [PATCH 062/135] fix test --- InvenTree/plugin/models.py | 4 ++-- InvenTree/plugin/test_api.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/InvenTree/plugin/models.py b/InvenTree/plugin/models.py index cacaabb1d8..50e07df0d6 100644 --- a/InvenTree/plugin/models.py +++ b/InvenTree/plugin/models.py @@ -4,7 +4,7 @@ Plugin model definitions # -*- coding: utf-8 -*- from __future__ import unicode_literals -from logging import warning +import warnings from django.utils.translation import gettext_lazy as _ from django.db import models @@ -100,7 +100,7 @@ class PluginConfig(models.Model): if (self.active is False and self.__org_active is True) or \ (self.active is True and self.__org_active is False): if settings.PLUGIN_TESTING: - warning('A reload was triggered') + warnings.warn('A reload was triggered') registry.reload_plugins() return ret diff --git a/InvenTree/plugin/test_api.py b/InvenTree/plugin/test_api.py index c767f0f8c0..8674d933fc 100644 --- a/InvenTree/plugin/test_api.py +++ b/InvenTree/plugin/test_api.py @@ -146,7 +146,7 @@ class PluginDetailAPITest(InvenTreeAPITestCase): self.assertDictContainsSubset({'base':{'key':'base', 'human_name':'base'}}, mixin_dict) # check reload on save - with self.assertWarns('A reload was triggered'): + with self.assertWarns(Warning('A reload was triggered')): plg_inactive = fixtures.filter(active=False).first() plg_inactive.active = True plg_inactive.save() From 2c83728f712b9057db25c1495561036025bd6f54 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 23:27:42 +0200 Subject: [PATCH 063/135] also check message --- InvenTree/plugin/test_api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/plugin/test_api.py b/InvenTree/plugin/test_api.py index 8674d933fc..5bb680a68a 100644 --- a/InvenTree/plugin/test_api.py +++ b/InvenTree/plugin/test_api.py @@ -146,8 +146,8 @@ class PluginDetailAPITest(InvenTreeAPITestCase): self.assertDictContainsSubset({'base':{'key':'base', 'human_name':'base'}}, mixin_dict) # check reload on save - with self.assertWarns(Warning('A reload was triggered')): + with self.assertWarns(Warning) as cm: plg_inactive = fixtures.filter(active=False).first() plg_inactive.active = True plg_inactive.save() - print('done') + self.assertEqual(cm.warning.args[0], 'A reload was triggered') From c462399fef266156137d9e1f9dd969e375b65a46 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 23:45:17 +0200 Subject: [PATCH 064/135] check if depreciation fires --- InvenTree/plugin/test_plugin.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/InvenTree/plugin/test_plugin.py b/InvenTree/plugin/test_plugin.py index f93ad3c67c..a863217fb1 100644 --- a/InvenTree/plugin/test_plugin.py +++ b/InvenTree/plugin/test_plugin.py @@ -74,6 +74,11 @@ class InvenTreePluginTests(TestCase): self.plugin_simple = SimpleInvenTreePlugin() + class OldInvenTreePlugin(InvenTreePlugin): + PLUGIN_NAME = 'OldPlugin' + + self.plugin_old = OldInvenTreePlugin() + class NameInvenTreePlugin(InvenTreePlugin): NAME = 'Aplugin' SLUG = 'a' @@ -148,3 +153,12 @@ class InvenTreePluginTests(TestCase): self.assertEqual(self.plugin.license, None) self.assertEqual(self.plugin_simple.license, None) self.assertEqual(self.plugin_name.license, 'MIT') + + def test_depreciation(self): + """Check if depreciations raise as expected""" + + # check deprecation warning is firing + with self.assertRaises(DeprecationWarning): + self.assertEqual(self.plugin_old.name, 'OldPlugin') + # check default value is used + self.assertEqual(self.plugin_old.get_meta_value('ABC', __default='123'), '123') From 8becad3c0a9243f2a2c66dcf8217407976db0377 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 23:45:45 +0200 Subject: [PATCH 065/135] reset zero behaviour --- InvenTree/plugin/plugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/plugin/plugin.py b/InvenTree/plugin/plugin.py index b008865f5c..aacd68f13a 100644 --- a/InvenTree/plugin/plugin.py +++ b/InvenTree/plugin/plugin.py @@ -25,7 +25,7 @@ class MetaBase: """Base class for a plugins metadata""" # Override the plugin name for each concrete plugin instance - NAME = '' + NAME = None SLUG = None TITLE = None @@ -59,7 +59,7 @@ class MetaBase: """ Name of plugin """ - return self.get_meta_value('NAME', 'PLUGIN_NAME') + return self.get_meta_value('NAME', 'PLUGIN_NAME', '') @property def name(self): From c27422880602797431072a0d2cb4d4a2876da24f Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 23:46:41 +0200 Subject: [PATCH 066/135] fix test --- InvenTree/plugin/test_plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/plugin/test_plugin.py b/InvenTree/plugin/test_plugin.py index a863217fb1..4a55b35d69 100644 --- a/InvenTree/plugin/test_plugin.py +++ b/InvenTree/plugin/test_plugin.py @@ -161,4 +161,4 @@ class InvenTreePluginTests(TestCase): with self.assertRaises(DeprecationWarning): self.assertEqual(self.plugin_old.name, 'OldPlugin') # check default value is used - self.assertEqual(self.plugin_old.get_meta_value('ABC', __default='123'), '123') + self.assertEqual(self.plugin_old.get_meta_value('ABC', 'ABCD', '123'), '123') From da3b7ae7d08e0b6b44fda1bac89e5fe08d8c53cf Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 May 2022 23:57:21 +0200 Subject: [PATCH 067/135] fix assertation typ --- InvenTree/plugin/test_plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/plugin/test_plugin.py b/InvenTree/plugin/test_plugin.py index 4a55b35d69..a8251226ee 100644 --- a/InvenTree/plugin/test_plugin.py +++ b/InvenTree/plugin/test_plugin.py @@ -158,7 +158,7 @@ class InvenTreePluginTests(TestCase): """Check if depreciations raise as expected""" # check deprecation warning is firing - with self.assertRaises(DeprecationWarning): + with self.assertWarns(Warning): self.assertEqual(self.plugin_old.name, 'OldPlugin') # check default value is used self.assertEqual(self.plugin_old.get_meta_value('ABC', 'ABCD', '123'), '123') From c809398bda7d2b29629c6de9cbe1d7a6755e6cd5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 12 May 2022 00:01:23 +0200 Subject: [PATCH 068/135] Add check for IntegrationPluginBase depreciation --- InvenTree/plugin/test_plugin.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/InvenTree/plugin/test_plugin.py b/InvenTree/plugin/test_plugin.py index a8251226ee..a72067cb12 100644 --- a/InvenTree/plugin/test_plugin.py +++ b/InvenTree/plugin/test_plugin.py @@ -9,7 +9,7 @@ from django.test import TestCase from plugin.samples.integration.sample import SampleIntegrationPlugin from plugin.samples.integration.another_sample import WrongIntegrationPlugin, NoIntegrationPlugin import plugin.templatetags.plugin_extras as plugin_tags -from plugin import registry, InvenTreePlugin +from plugin import registry, InvenTreePlugin, IntegrationPluginBase class PluginTagTests(TestCase): @@ -162,3 +162,11 @@ class InvenTreePluginTests(TestCase): self.assertEqual(self.plugin_old.name, 'OldPlugin') # check default value is used self.assertEqual(self.plugin_old.get_meta_value('ABC', 'ABCD', '123'), '123') + + # check usage of the old class fires + class OldPlugin(IntegrationPluginBase): + pass + + with self.assertWarns(DeprecationWarning): + plg = OldPlugin() + self.assertIsInstance(plg, InvenTreePlugin) From a6e889b25eb6e7221f7325f298b4ae4c067c10bb Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 12 May 2022 00:04:29 +0200 Subject: [PATCH 069/135] pep fix --- InvenTree/plugin/test_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/plugin/test_api.py b/InvenTree/plugin/test_api.py index 5bb680a68a..445c371f54 100644 --- a/InvenTree/plugin/test_api.py +++ b/InvenTree/plugin/test_api.py @@ -143,7 +143,7 @@ class PluginDetailAPITest(InvenTreeAPITestCase): plg = fixtures.first() mixin_dict = plg.mixins() self.assertIn('base', mixin_dict) - self.assertDictContainsSubset({'base':{'key':'base', 'human_name':'base'}}, mixin_dict) + self.assertDictContainsSubset({'base' :{'key' :'base', 'human_name': 'base'}}, mixin_dict) # check reload on save with self.assertWarns(Warning) as cm: From 699db12b240a02fd11cd11830961f9d5c2376030 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 12 May 2022 00:04:37 +0200 Subject: [PATCH 070/135] make test more precise --- InvenTree/plugin/test_plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/plugin/test_plugin.py b/InvenTree/plugin/test_plugin.py index a72067cb12..a722704a91 100644 --- a/InvenTree/plugin/test_plugin.py +++ b/InvenTree/plugin/test_plugin.py @@ -158,7 +158,7 @@ class InvenTreePluginTests(TestCase): """Check if depreciations raise as expected""" # check deprecation warning is firing - with self.assertWarns(Warning): + with self.assertWarns(DeprecationWarning): self.assertEqual(self.plugin_old.name, 'OldPlugin') # check default value is used self.assertEqual(self.plugin_old.get_meta_value('ABC', 'ABCD', '123'), '123') From bd42fea5fa3abbf71d2191796a78c6ac868989a9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 12 May 2022 00:07:19 +0200 Subject: [PATCH 071/135] another pep check --- InvenTree/plugin/test_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/plugin/test_api.py b/InvenTree/plugin/test_api.py index 445c371f54..f232773ee1 100644 --- a/InvenTree/plugin/test_api.py +++ b/InvenTree/plugin/test_api.py @@ -143,7 +143,7 @@ class PluginDetailAPITest(InvenTreeAPITestCase): plg = fixtures.first() mixin_dict = plg.mixins() self.assertIn('base', mixin_dict) - self.assertDictContainsSubset({'base' :{'key' :'base', 'human_name': 'base'}}, mixin_dict) + self.assertDictContainsSubset({'base': {'key': 'base', 'human_name': 'base'}}, mixin_dict) # check reload on save with self.assertWarns(Warning) as cm: From 142398976cedbc7e94a46676b77edc18902a89b5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 12 May 2022 00:38:21 +0200 Subject: [PATCH 072/135] test plugin details --- InvenTree/common/tests.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/InvenTree/common/tests.py b/InvenTree/common/tests.py index 8fa0f3b28e..282bae9482 100644 --- a/InvenTree/common/tests.py +++ b/InvenTree/common/tests.py @@ -10,7 +10,8 @@ from django.urls import reverse from InvenTree.api_tester import InvenTreeAPITestCase from InvenTree.helpers import str2bool -from plugin.models import NotificationUserSetting +from plugin.models import NotificationUserSetting, PluginConfig, PluginSetting +from plugin import registry from .models import InvenTreeSetting, InvenTreeUserSetting, WebhookEndpoint, WebhookMessage, NotificationEntry from .api import WebhookView @@ -477,6 +478,24 @@ class PluginSettingsApiTest(InvenTreeAPITestCase): self.get(url, expected_code=200) + def test_valid_plugin_slug(self): + """Test that an valid plugin slug runs through""" + # load plugin configs + fixtures = PluginConfig.objects.all() + if not fixtures: + registry.reload_plugins() + fixtures = PluginConfig.objects.all() + + # get data + url = reverse('api-plugin-setting-detail', kwargs={'plugin': 'sample', 'key': 'API_KEY'}) + response = self.get(url, expected_code=200) + + # check the right setting came through + self.assertTrue(response.data['key'], 'API_KEY') + self.assertTrue(response.data['plugin'], 'sample') + self.assertTrue(response.data['type'], 'string') + self.assertTrue(response.data['description'], 'Key required for accessing external API') + def test_invalid_plugin_slug(self): """Test that an invalid plugin slug returns a 404""" From eaab905bfd395086335eb908fa7063083c21bdcd Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 12 May 2022 00:50:29 +0200 Subject: [PATCH 073/135] reset plugin name default --- InvenTree/plugin/plugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/plugin/plugin.py b/InvenTree/plugin/plugin.py index aacd68f13a..b008865f5c 100644 --- a/InvenTree/plugin/plugin.py +++ b/InvenTree/plugin/plugin.py @@ -25,7 +25,7 @@ class MetaBase: """Base class for a plugins metadata""" # Override the plugin name for each concrete plugin instance - NAME = None + NAME = '' SLUG = None TITLE = None @@ -59,7 +59,7 @@ class MetaBase: """ Name of plugin """ - return self.get_meta_value('NAME', 'PLUGIN_NAME', '') + return self.get_meta_value('NAME', 'PLUGIN_NAME') @property def name(self): From 3d580e732f828768bc364c4f4701e0ef41f8ff16 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 12 May 2022 00:51:02 +0200 Subject: [PATCH 074/135] change test to slug --- InvenTree/plugin/test_plugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/plugin/test_plugin.py b/InvenTree/plugin/test_plugin.py index a722704a91..30054d569b 100644 --- a/InvenTree/plugin/test_plugin.py +++ b/InvenTree/plugin/test_plugin.py @@ -75,7 +75,7 @@ class InvenTreePluginTests(TestCase): self.plugin_simple = SimpleInvenTreePlugin() class OldInvenTreePlugin(InvenTreePlugin): - PLUGIN_NAME = 'OldPlugin' + PLUGIN_SLUG = 'old' self.plugin_old = OldInvenTreePlugin() @@ -159,7 +159,7 @@ class InvenTreePluginTests(TestCase): # check deprecation warning is firing with self.assertWarns(DeprecationWarning): - self.assertEqual(self.plugin_old.name, 'OldPlugin') + self.assertEqual(self.plugin_old.slug, 'old') # check default value is used self.assertEqual(self.plugin_old.get_meta_value('ABC', 'ABCD', '123'), '123') From 4ed795174d16a1d115db28eacec9eff93bfdafec Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 12 May 2022 00:52:28 +0200 Subject: [PATCH 075/135] PEP fix --- InvenTree/common/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/common/tests.py b/InvenTree/common/tests.py index 282bae9482..4a95cad72a 100644 --- a/InvenTree/common/tests.py +++ b/InvenTree/common/tests.py @@ -10,7 +10,7 @@ from django.urls import reverse from InvenTree.api_tester import InvenTreeAPITestCase from InvenTree.helpers import str2bool -from plugin.models import NotificationUserSetting, PluginConfig, PluginSetting +from plugin.models import NotificationUserSetting, PluginConfig from plugin import registry from .models import InvenTreeSetting, InvenTreeUserSetting, WebhookEndpoint, WebhookMessage, NotificationEntry From 151f2cae6f9dd48a54a8d1e0f61758b62fab9307 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 12 May 2022 10:45:30 +1000 Subject: [PATCH 076/135] Do not redirect requests for media / static / api / js files - For these paths, just return a 401 - This is necessary to stop unauthorized calls to the API or to request media files from redirecting to the login page --- InvenTree/InvenTree/middleware.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/InvenTree/InvenTree/middleware.py b/InvenTree/InvenTree/middleware.py index 91cfefc6d6..b6550379e2 100644 --- a/InvenTree/InvenTree/middleware.py +++ b/InvenTree/InvenTree/middleware.py @@ -1,9 +1,12 @@ -from django.shortcuts import HttpResponseRedirect -from django.urls import reverse_lazy, Resolver404 -from django.shortcuts import redirect -from django.urls import include, re_path +# -*- coding: utf-8 -*- + from django.conf import settings from django.contrib.auth.middleware import PersistentRemoteUserMiddleware +from django.http import HttpResponse +from django.shortcuts import HttpResponseRedirect +from django.shortcuts import redirect +from django.urls import reverse_lazy, Resolver404 +from django.urls import include, re_path import logging @@ -82,11 +85,23 @@ class AuthRequiredMiddleware(object): reverse_lazy('admin:logout'), ] - if path not in urls and not path.startswith('/api/'): + # Do not redirect requests to any of these paths + paths_ignore = [ + '/api/', + '/js/', + '/media/', + '/static/', + ] + + if path not in urls and not any([path.startswith(p) for p in paths_ignore]): # Save the 'next' parameter to pass through to the login view return redirect('{}?next={}'.format(reverse_lazy('account_login'), request.path)) + else: + # Return a 401 (Unauthorized) response code for this request + return HttpResponse('Unauthorized', status=401) + response = self.get_response(request) return response From aa9ee15fb46499206033c4e8acb89cbed6f6e47d Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 12 May 2022 10:52:53 +1000 Subject: [PATCH 077/135] Fix CI pipeline for python checks - Recently updated the python binding test framework --- .github/workflows/qc_checks.yaml | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/.github/workflows/qc_checks.yaml b/.github/workflows/qc_checks.yaml index 0aabd064bc..3de8c3d808 100644 --- a/.github/workflows/qc_checks.yaml +++ b/.github/workflows/qc_checks.yaml @@ -124,6 +124,16 @@ jobs: env: wrapper_name: inventree-python + INVENTREE_DB_ENGINE: django.db.backends.sqlite3 + INVENTREE_DB_NAME: ../inventree_unit_test_db.sqlite3 + INVENTREE_MEDIA_ROOT: ../test_inventree_media + INVENTREE_STATIC_ROOT: ../test_inventree_static + INVENTREE_ADMIN_USER: testuser + INVENTREE_ADMIN_PASSWORD: testpassword + INVENTREE_ADMIN_EMAIL: test@test.com + INVENTREE_PYTHON_TEST_SERVER: http://localhost:12345 + INVENTREE_PYTHON_TEST_USERNAME: testuser + INVENTREE_PYTHON_TEST_PASSWORD: testpassword steps: - name: Checkout Code @@ -140,13 +150,14 @@ jobs: git clone --depth 1 https://github.com/inventree/${{ env.wrapper_name }} ./${{ env.wrapper_name }} - name: Start Server run: | - invoke import-records -f ./${{ env.wrapper_name }}/test/test_data.json - invoke server -a 127.0.0.1:8000 & - sleep ${{ env.server_start_sleep }} + invoke delete-data -f + invoke import-fixtures + invoke server -a 127.0.0.1:12345 & - name: Run Tests run: | cd ${{ env.wrapper_name }} - invoke test + invoke check-server + coverage run -m unittest -s test/ coverage: name: Sqlite / coverage From 21d2b54afef6c7f4f03ff1ad9a1fb42ed6ddeafa Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 12 May 2022 11:00:43 +1000 Subject: [PATCH 078/135] Fix CI step --- .github/workflows/qc_checks.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/qc_checks.yaml b/.github/workflows/qc_checks.yaml index 3de8c3d808..b3ad9e24d2 100644 --- a/.github/workflows/qc_checks.yaml +++ b/.github/workflows/qc_checks.yaml @@ -157,7 +157,7 @@ jobs: run: | cd ${{ env.wrapper_name }} invoke check-server - coverage run -m unittest -s test/ + coverage run -m unittest discover -s test/ coverage: name: Sqlite / coverage From 774bfdb9e7b392c21bc181543544cc3b849430b4 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 12 May 2022 11:22:34 +1000 Subject: [PATCH 079/135] Adds APIDownloadMixin class to implement common behaviour --- InvenTree/InvenTree/api.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/InvenTree/InvenTree/api.py b/InvenTree/InvenTree/api.py index 171fe414d2..3b440a1f3d 100644 --- a/InvenTree/InvenTree/api.py +++ b/InvenTree/InvenTree/api.py @@ -61,6 +61,44 @@ class NotFoundView(AjaxView): return JsonResponse(data, status=404) +class APIDownloadMixin: + """ + Mixin for enabling a LIST endpoint to be downloaded a file. + + To download the data, add the ?export= to the query string. + + The implementing class must provided a download_queryset method, + e.g. + + def download_queryset(self, queryset, export_format): + dataset = StockItemResource().export(queryset=queryset) + + filedata = dataset.export(export_format) + + filename = 'InvenTree_Stocktake_{date}.{fmt}'.format( + date=datetime.now().strftime("%d-%b-%Y"), + fmt=export_format + ) + + return DownloadFile(filedata, filename) + """ + + def get(self, request, *args, **kwargs): + + export_format = request.query_params.get('export', None) + + if export_format: + queryset = self.filter_queryset(self.get_queryset()) + return self.download_queryset(queryset, export_format) + + else: + # Default to the parent class implementation + return super().get(request, *args, **kwargs) + + def download_queryset(self, queryset, export_format): + raise NotImplementedError("download_queryset method not implemented!") + + class AttachmentMixin: """ Mixin for creating attachment objects, From 650d082eca54f18235a6235d9b5a8facc62706de Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 12 May 2022 11:23:58 +1000 Subject: [PATCH 080/135] Bump API version --- InvenTree/InvenTree/api_version.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/api_version.py b/InvenTree/InvenTree/api_version.py index e391a00bd1..d5699f8de8 100644 --- a/InvenTree/InvenTree/api_version.py +++ b/InvenTree/InvenTree/api_version.py @@ -4,11 +4,16 @@ InvenTree API version information # InvenTree API version -INVENTREE_API_VERSION = 47 +INVENTREE_API_VERSION = 48 """ Increment this API version number whenever there is a significant change to the API that any clients need to know about +v48 -> 2022-05-12 : https://github.com/inventree/InvenTree/pull/2977 + - Adds "export to file" functionality for PurchaseOrder API endpoint + - Adds "export to file" functionality for SalesOrder API endpoint + - Adds "export to file" functionality for BuildOrder API endpoint + v47 -> 2022-05-10 : https://github.com/inventree/InvenTree/pull/2964 - Fixes barcode API error response when scanning a StockItem which does not exist - Fixes barcode API error response when scanning a StockLocation which does not exist From a77d4b97b46e8e1529324436f01fde8ab7d8dfac Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 12 May 2022 11:29:33 +1000 Subject: [PATCH 081/135] Refactor stock_list endpoint to use the new mixin --- InvenTree/stock/api.py | 39 ++++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index a42b6a2869..96a893e914 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -33,7 +33,7 @@ from company.serializers import CompanySerializer, SupplierPartSerializer from InvenTree.helpers import str2bool, isNull, extract_serial_numbers from InvenTree.helpers import DownloadFile -from InvenTree.api import AttachmentMixin +from InvenTree.api import AttachmentMixin, APIDownloadMixin from InvenTree.filters import InvenTreeOrderingFilter from order.models import PurchaseOrder @@ -505,7 +505,7 @@ class StockFilter(rest_filters.FilterSet): updated_after = rest_filters.DateFilter(label='Updated after', field_name='updated', lookup_expr='gte') -class StockList(generics.ListCreateAPIView): +class StockList(APIDownloadMixin, generics.ListCreateAPIView): """ API endpoint for list view of Stock objects - GET: Return a list of all StockItem objects (with optional query filters) @@ -646,6 +646,22 @@ class StockList(generics.ListCreateAPIView): return Response(response_data, status=status.HTTP_201_CREATED, headers=self.get_success_headers(serializer.data)) + def download_queryset(self, queryset, export_format): + """ + Download this queryset as a file. + Uses the APIDownloadMixin mixin class + """ + dataset = StockItemResource().export(queryset=queryset) + + filedata = dataset.export(export_format) + + filename = 'InvenTree_StockItems_{date}.{fmt}'.format( + date=datetime.now().strftime("%d-%b-%Y"), + fmt=export_format + ) + + return DownloadFile(filedata, filename) + def list(self, request, *args, **kwargs): """ Override the 'list' method, as the StockLocation objects @@ -658,25 +674,6 @@ class StockList(generics.ListCreateAPIView): params = request.query_params - # Check if we wish to export the queried data to a file. - # If so, skip pagination! - export_format = params.get('export', None) - - if export_format: - export_format = str(export_format).strip().lower() - - if export_format in ['csv', 'tsv', 'xls', 'xlsx']: - dataset = StockItemResource().export(queryset=queryset) - - filedata = dataset.export(export_format) - - filename = 'InvenTree_Stocktake_{date}.{fmt}'.format( - date=datetime.now().strftime("%d-%b-%Y"), - fmt=export_format - ) - - return DownloadFile(filedata, filename) - page = self.paginate_queryset(queryset) if page is not None: From 465e69c254f6cec5e3a7faeb24a0868ca8820331 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 12 May 2022 11:33:17 +1000 Subject: [PATCH 082/135] Refactor exporters for: - Part - PurchaseOrderLineItem --- InvenTree/order/api.py | 28 +++++++++++----------------- InvenTree/part/api.py | 29 +++++++++++------------------ 2 files changed, 22 insertions(+), 35 deletions(-) diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index 2dab7684de..6e74262ac2 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -17,7 +17,7 @@ from company.models import SupplierPart from InvenTree.filters import InvenTreeOrderingFilter from InvenTree.helpers import str2bool, DownloadFile -from InvenTree.api import AttachmentMixin +from InvenTree.api import AttachmentMixin, APIDownloadMixin from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus from order.admin import PurchaseOrderLineItemResource @@ -407,7 +407,7 @@ class PurchaseOrderLineItemFilter(rest_filters.FilterSet): return queryset -class PurchaseOrderLineItemList(generics.ListCreateAPIView): +class PurchaseOrderLineItemList(APIDownloadMixin, generics.ListCreateAPIView): """ API endpoint for accessing a list of PurchaseOrderLineItem objects - GET: Return a list of PurchaseOrder Line Item objects @@ -460,25 +460,19 @@ class PurchaseOrderLineItemList(generics.ListCreateAPIView): return queryset + def download_queryset(self, queryset, export_format): + dataset = PurchaseOrderLineItemResource().export(queryset=queryset) + + filedata = dataset.export(export_format) + + filename = f"InvenTree_PurchaseOrderItems.{export_format}" + + return DownloadFile(filedata, filename) + def list(self, request, *args, **kwargs): queryset = self.filter_queryset(self.get_queryset()) - # Check if we wish to export the queried data to a file - export_format = request.query_params.get('export', None) - - if export_format: - export_format = str(export_format).strip().lower() - - if export_format in ['csv', 'tsv', 'xls', 'xlsx']: - dataset = PurchaseOrderLineItemResource().export(queryset=queryset) - - filedata = dataset.export(export_format) - - filename = f"InvenTree_PurchaseOrderData.{export_format}" - - return DownloadFile(filedata, filename) - page = self.paginate_queryset(queryset) if page is not None: diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index ca9accf9e2..0e0e918665 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -49,7 +49,7 @@ from . import serializers as part_serializers from InvenTree.helpers import str2bool, isNull, increment from InvenTree.helpers import DownloadFile -from InvenTree.api import AttachmentMixin +from InvenTree.api import AttachmentMixin, APIDownloadMixin from InvenTree.status_codes import BuildStatus, PurchaseOrderStatus, SalesOrderStatus @@ -847,7 +847,7 @@ class PartFilter(rest_filters.FilterSet): virtual = rest_filters.BooleanFilter() -class PartList(generics.ListCreateAPIView): +class PartList(APIDownloadMixin, generics.ListCreateAPIView): """ API endpoint for accessing a list of Part objects @@ -897,6 +897,15 @@ class PartList(generics.ListCreateAPIView): return self.serializer_class(*args, **kwargs) + def download_queryset(self, queryset, export_format): + dataset = PartResource().export(queryset=queryset) + + filedata = dataset.export(export_format) + + filename = f"InvenTree_Parts.{export_format}" + + return DownloadFile(filedata, filename) + def list(self, request, *args, **kwargs): """ Overide the 'list' method, as the PartCategory objects are @@ -908,22 +917,6 @@ class PartList(generics.ListCreateAPIView): queryset = self.filter_queryset(self.get_queryset()) - # Check if we wish to export the queried data to a file. - # If so, skip pagination! - export_format = request.query_params.get('export', None) - - if export_format: - export_format = str(export_format).strip().lower() - - if export_format in ['csv', 'tsv', 'xls', 'xlsx']: - dataset = PartResource().export(queryset=queryset) - - filedata = dataset.export(export_format) - - filename = f"InvenTree_Parts.{export_format}" - - return DownloadFile(filedata, filename) - page = self.paginate_queryset(queryset) if page is not None: From 1b1f7634b72af15fff917665878ffcfb5b1173ae Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 12 May 2022 11:41:25 +1000 Subject: [PATCH 083/135] Adds exporter and download button for PurchaseOrder table --- InvenTree/order/admin.py | 20 +++++++++++++++++++- InvenTree/order/api.py | 13 +++++++++++-- InvenTree/part/admin.py | 2 +- InvenTree/templates/js/translated/order.js | 4 +++- 4 files changed, 34 insertions(+), 5 deletions(-) diff --git a/InvenTree/order/admin.py b/InvenTree/order/admin.py index 0de28d5668..8ab8b832ef 100644 --- a/InvenTree/order/admin.py +++ b/InvenTree/order/admin.py @@ -5,8 +5,9 @@ from django.contrib import admin from import_export.admin import ImportExportModelAdmin -from import_export.resources import ModelResource from import_export.fields import Field +from import_export.resources import ModelResource +import import_export.widgets as widgets from .models import PurchaseOrder, PurchaseOrderLineItem, PurchaseOrderExtraLine from .models import SalesOrder, SalesOrderLineItem, SalesOrderExtraLine @@ -92,6 +93,23 @@ class SalesOrderAdmin(ImportExportModelAdmin): autocomplete_fields = ('customer',) +class PurchaseOrderResource(ModelResource): + """ + Class for managing import / export of PurchaseOrder data + """ + + # Add number of line items + line_items = Field(attribute='line_count', widget=widgets.IntegerWidget(), readonly=True) + + # Is this order overdue? + overdue = Field(attribute='is_overdue', widget=widgets.BooleanWidget(), readonly=True) + + class Meta: + model = PurchaseOrder + skip_unchanged = True + clean_model_instances = True + + class PurchaseOrderLineItemResource(ModelResource): """ Class for managing import / export of PurchaseOrderLineItem data """ diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index 6e74262ac2..da037aa8b7 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -20,7 +20,7 @@ from InvenTree.helpers import str2bool, DownloadFile from InvenTree.api import AttachmentMixin, APIDownloadMixin from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus -from order.admin import PurchaseOrderLineItemResource +from order.admin import PurchaseOrderResource, PurchaseOrderLineItemResource import order.models as models import order.serializers as serializers from part.models import Part @@ -110,7 +110,7 @@ class PurchaseOrderFilter(rest_filters.FilterSet): ] -class PurchaseOrderList(generics.ListCreateAPIView): +class PurchaseOrderList(APIDownloadMixin, generics.ListCreateAPIView): """ API endpoint for accessing a list of PurchaseOrder objects - GET: Return list of PurchaseOrder objects (with filters) @@ -160,6 +160,15 @@ class PurchaseOrderList(generics.ListCreateAPIView): return queryset + def download_queryset(self, queryset, export_format): + dataset = PurchaseOrderResource().export(queryset=queryset) + + filedata = dataset.export(export_format) + + filename = f"InvenTree_PurchaseOrders.{export_format}" + + return DownloadFile(filedata, filename) + def filter_queryset(self, queryset): # Perform basic filtering diff --git a/InvenTree/part/admin.py b/InvenTree/part/admin.py index fd0a16adc2..30dad8e995 100644 --- a/InvenTree/part/admin.py +++ b/InvenTree/part/admin.py @@ -4,8 +4,8 @@ from __future__ import unicode_literals from django.contrib import admin from import_export.admin import ImportExportModelAdmin -from import_export.resources import ModelResource from import_export.fields import Field +from import_export.resources import ModelResource import import_export.widgets as widgets from company.models import SupplierPart diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js index 538f37a710..dcd7307628 100644 --- a/InvenTree/templates/js/translated/order.js +++ b/InvenTree/templates/js/translated/order.js @@ -1394,7 +1394,9 @@ function loadPurchaseOrderTable(table, options) { filters[key] = options.params[key]; } - setupFilterList('purchaseorder', $(table)); + var target = '#filter-list-purchaseorder'; + + setupFilterList('purchaseorder', $(table), target, {download: true}); $(table).inventreeTable({ url: '{% url "api-po-list" %}', From d0ddb47b1f4c4a2229f4b2e69316fd45fc86a140 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 12 May 2022 11:44:05 +1000 Subject: [PATCH 084/135] Adds exporter and download button for sales orders --- InvenTree/order/admin.py | 17 +++++++++++++++++ InvenTree/order/api.py | 12 +++++++++++- InvenTree/templates/js/translated/order.js | 4 +++- 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/InvenTree/order/admin.py b/InvenTree/order/admin.py index 8ab8b832ef..a1e1b74256 100644 --- a/InvenTree/order/admin.py +++ b/InvenTree/order/admin.py @@ -135,6 +135,23 @@ class PurchaseOrderExtraLineResource(ModelResource): model = PurchaseOrderExtraLine +class SalesOrderResource(ModelResource): + """ + Class for managing import / export of SalesOrder data + """ + + # Add number of line items + line_items = Field(attribute='line_count', widget=widgets.IntegerWidget(), readonly=True) + + # Is this order overdue? + overdue = Field(attribute='is_overdue', widget=widgets.BooleanWidget(), readonly=True) + + class Meta: + model = SalesOrder + skip_unchanged = True + clean_model_instances = True + + class SalesOrderLineItemResource(ModelResource): """ Class for managing import / export of SalesOrderLineItem data diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index da037aa8b7..7c8a93125f 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -21,6 +21,7 @@ from InvenTree.api import AttachmentMixin, APIDownloadMixin from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus from order.admin import PurchaseOrderResource, PurchaseOrderLineItemResource +from order.admin import SalesOrderResource import order.models as models import order.serializers as serializers from part.models import Part @@ -583,7 +584,7 @@ class SalesOrderAttachmentDetail(generics.RetrieveUpdateDestroyAPIView, Attachme serializer_class = serializers.SalesOrderAttachmentSerializer -class SalesOrderList(generics.ListCreateAPIView): +class SalesOrderList(APIDownloadMixin, generics.ListCreateAPIView): """ API endpoint for accessing a list of SalesOrder objects. @@ -633,6 +634,15 @@ class SalesOrderList(generics.ListCreateAPIView): return queryset + def download_queryset(self, queryset, export_format): + dataset = SalesOrderResource().export(queryset=queryset) + + filedata = dataset.export(export_format) + + filename = f"InvenTree_SalesOrders.{export_format}" + + return DownloadFile(filedata, filename) + def filter_queryset(self, queryset): """ Perform custom filtering operations on the SalesOrder queryset. diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js index dcd7307628..372dc70a9a 100644 --- a/InvenTree/templates/js/translated/order.js +++ b/InvenTree/templates/js/translated/order.js @@ -2093,7 +2093,9 @@ function loadSalesOrderTable(table, options) { options.url = options.url || '{% url "api-so-list" %}'; - setupFilterList('salesorder', $(table)); + var target = '#filter-list-salesorder'; + + setupFilterList('salesorder', $(table), target, {download: true}); $(table).inventreeTable({ url: options.url, From c89547f58c98c6e1338d25df0ee8a04615508579 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 12 May 2022 12:44:15 +1000 Subject: [PATCH 085/135] Adds exporter and download functionality for BuildOrder table --- InvenTree/InvenTree/api.py | 2 +- InvenTree/build/admin.py | 49 +++++++++++++++++++++- InvenTree/build/api.py | 18 ++++++-- InvenTree/part/api.py | 1 - InvenTree/templates/js/translated/build.js | 2 +- 5 files changed, 63 insertions(+), 9 deletions(-) diff --git a/InvenTree/InvenTree/api.py b/InvenTree/InvenTree/api.py index 3b440a1f3d..e468e8d1cf 100644 --- a/InvenTree/InvenTree/api.py +++ b/InvenTree/InvenTree/api.py @@ -87,7 +87,7 @@ class APIDownloadMixin: export_format = request.query_params.get('export', None) - if export_format: + if export_format and export_format in ['csv', 'tsv', 'xls', 'xlsx']: queryset = self.filter_queryset(self.get_queryset()) return self.download_queryset(queryset, export_format) diff --git a/InvenTree/build/admin.py b/InvenTree/build/admin.py index a612ad8460..55d7a1e2d2 100644 --- a/InvenTree/build/admin.py +++ b/InvenTree/build/admin.py @@ -2,9 +2,54 @@ from __future__ import unicode_literals from django.contrib import admin -from import_export.admin import ImportExportModelAdmin -from .models import Build, BuildItem +from import_export.admin import ImportExportModelAdmin +from import_export.fields import Field +from import_export.resources import ModelResource +import import_export.widgets as widgets + +from build.models import Build, BuildItem + +import part.models + + +class BuildResource(ModelResource): + """Class for managing import/export of Build data""" + # For some reason, we need to specify the fields individually for this ModelResource, + # but we don't for other ones. + # TODO: 2022-05-12 - Need to investigate why this is the case! + + pk = Field(attribute='pk') + + reference = Field(attribute='reference') + + title = Field(attribute='title') + + part = Field(attribute='part', widget=widgets.ForeignKeyWidget(part.models.Part)) + + part_name = Field(attribute='part__full_name', readonly=True) + + overdue = Field(attribute='is_overdue', readonly=True, widget=widgets.BooleanWidget()) + + completed = Field(attribute='completed', readonly=True) + + quantity = Field(attribute='quantity') + + status = Field(attribute='status') + + batch = Field(attribute='batch') + + notes = Field(attribute='notes') + + class Meta: + models = Build + skip_unchanged = True + report_skipped = False + clean_model_instances = True + exclude = [ + 'lft', 'rght', 'tree_id', 'level', + ] + class BuildAdmin(ImportExportModelAdmin): diff --git a/InvenTree/build/api.py b/InvenTree/build/api.py index e32b404ae2..4453232823 100644 --- a/InvenTree/build/api.py +++ b/InvenTree/build/api.py @@ -12,13 +12,15 @@ from rest_framework import filters, generics from django_filters.rest_framework import DjangoFilterBackend from django_filters import rest_framework as rest_filters -from InvenTree.api import AttachmentMixin -from InvenTree.helpers import str2bool, isNull +from InvenTree.api import AttachmentMixin, APIDownloadMixin +from InvenTree.helpers import str2bool, isNull, DownloadFile from InvenTree.filters import InvenTreeOrderingFilter from InvenTree.status_codes import BuildStatus -from .models import Build, BuildItem, BuildOrderAttachment +import build.admin import build.serializers +from build.models import Build, BuildItem, BuildOrderAttachment + from users.models import Owner @@ -71,7 +73,7 @@ class BuildFilter(rest_filters.FilterSet): return queryset -class BuildList(generics.ListCreateAPIView): +class BuildList(APIDownloadMixin, generics.ListCreateAPIView): """ API endpoint for accessing a list of Build objects. - GET: Return list of objects (with filters) @@ -123,6 +125,14 @@ class BuildList(generics.ListCreateAPIView): return queryset + def download_queryset(self, queryset, export_format): + dataset = build.admin.BuildResource().export(queryset=queryset) + + filedata = dataset.export(export_format) + filename = f"InvenTree_BuildOrders.{export_format}" + + return DownloadFile(filedata, filename) + def filter_queryset(self, queryset): queryset = super().filter_queryset(queryset) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 0e0e918665..622ca38669 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -901,7 +901,6 @@ class PartList(APIDownloadMixin, generics.ListCreateAPIView): dataset = PartResource().export(queryset=queryset) filedata = dataset.export(export_format) - filename = f"InvenTree_Parts.{export_format}" return DownloadFile(filedata, filename) diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index 94c2780a28..814f2a3247 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -2355,7 +2355,7 @@ function loadBuildTable(table, options) { var filterTarget = options.filterTarget || null; - setupFilterList('build', table, filterTarget); + setupFilterList('build', table, filterTarget, {download: true}); $(table).inventreeTable({ method: 'get', From 8edc0cc8938484bd3e01821ce8b1160568b91b36 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 12 May 2022 12:47:25 +1000 Subject: [PATCH 086/135] PEP fixes --- InvenTree/build/admin.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/InvenTree/build/admin.py b/InvenTree/build/admin.py index 55d7a1e2d2..43909d2197 100644 --- a/InvenTree/build/admin.py +++ b/InvenTree/build/admin.py @@ -16,7 +16,7 @@ import part.models class BuildResource(ModelResource): """Class for managing import/export of Build data""" # For some reason, we need to specify the fields individually for this ModelResource, - # but we don't for other ones. + # but we don't for other ones. # TODO: 2022-05-12 - Need to investigate why this is the case! pk = Field(attribute='pk') @@ -51,7 +51,6 @@ class BuildResource(ModelResource): ] - class BuildAdmin(ImportExportModelAdmin): exclude = [ From 47ddafb72890ecfbf4480b8a2605687f6221c0e3 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 12 May 2022 15:15:51 +1000 Subject: [PATCH 087/135] Fix edge case when converting stock item to variant - If the stock item had been created as part of a Build Order, and subsequently "converted" to a variant part, the conversion operation will fail - Patch allows the build reference to be linked based on either the base part, or any conversion options --- InvenTree/stock/models.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 53e1321e1a..a46d43b007 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -556,7 +556,14 @@ class StockItem(MPTTModel): # If the item points to a build, check that the Part references match if self.build: - if not self.part == self.build.part: + + if self.part == self.build.part: + # Part references match exactly + pass + elif self.part in self.build.part.get_conversion_options(): + # Part reference is one of the valid conversion options for the build output + pass + else: raise ValidationError({ 'build': _("Build reference does not point to the same part object") }) From 59cf9825fe9d11adbc124a758dc4e505a1828e94 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 12 May 2022 15:16:26 +1000 Subject: [PATCH 088/135] Update comment --- InvenTree/part/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 492da8f5de..96ffa581f4 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -2233,7 +2233,7 @@ class Part(MPTTModel): for child in children: parts.append(child) - # Immediate parent + # Immediate parent, and siblings if self.variant_of: parts.append(self.variant_of) From e112d555d403be244871c04bd5fd9b9d0288fe22 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 12 May 2022 16:45:27 +1000 Subject: [PATCH 089/135] Simplify the various settings objects, to improve retrieval of 'parameters' from the base class - Remove the GenericReferencedSettingsClass mixin - Each subclass defines a very simple get_kwargs() method - Now, at object level *and* class level we can perform lookup of settings and actually get proper data back - Adds "model" option to setting (precursor of things to come) --- InvenTree/common/models.py | 163 +++++++----------- InvenTree/common/serializers.py | 5 + InvenTree/plugin/models.py | 43 +++-- .../plugin/samples/integration/sample.py | 5 + .../templates/InvenTree/settings/setting.html | 3 + 5 files changed, 105 insertions(+), 114 deletions(-) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 11157763cb..37a6289d75 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -136,6 +136,19 @@ class BaseInvenTreeSetting(models.Model): return settings + def get_kwargs(self): + """ + Construct kwargs for doing class-based settings lookup, + depending on *which* class we are. + + This is necessary to abtract the settings object + from the implementing class (e.g plugins) + + Subclasses should override this function to ensure the kwargs are correctly set. + """ + + return {} + @classmethod def get_setting_definition(cls, key, **kwargs): """ @@ -319,11 +332,11 @@ class BaseInvenTreeSetting(models.Model): value = setting.value # Cast to boolean if necessary - if setting.is_bool(**kwargs): + if setting.is_bool(): value = InvenTree.helpers.str2bool(value) # Cast to integer if necessary - if setting.is_int(**kwargs): + if setting.is_int(): try: value = int(value) except (ValueError, TypeError): @@ -390,19 +403,19 @@ class BaseInvenTreeSetting(models.Model): @property def name(self): - return self.__class__.get_setting_name(self.key) + return self.__class__.get_setting_name(self.key, **self.get_kwargs()) @property def default_value(self): - return self.__class__.get_setting_default(self.key) + return self.__class__.get_setting_default(self.key, **self.get_kwargs()) @property def description(self): - return self.__class__.get_setting_description(self.key) + return self.__class__.get_setting_description(self.key, **self.get_kwargs()) @property def units(self): - return self.__class__.get_setting_units(self.key) + return self.__class__.get_setting_units(self.key, **self.get_kwargs()) def clean(self, **kwargs): """ @@ -512,12 +525,12 @@ class BaseInvenTreeSetting(models.Model): except self.DoesNotExist: pass - def choices(self, **kwargs): + def choices(self): """ Return the available choices for this setting (or None if no choices are defined) """ - return self.__class__.get_setting_choices(self.key, **kwargs) + return self.__class__.get_setting_choices(self.key, **self.get_kwargs()) def valid_options(self): """ @@ -531,14 +544,14 @@ class BaseInvenTreeSetting(models.Model): return [opt[0] for opt in choices] - def is_choice(self, **kwargs): + def is_choice(self): """ Check if this setting is a "choice" field """ - return self.__class__.get_setting_choices(self.key, **kwargs) is not None + return self.__class__.get_setting_choices(self.key, **self.get_kwargs()) is not None - def as_choice(self, **kwargs): + def as_choice(self): """ Render this setting as the "display" value of a choice field, e.g. if the choices are: @@ -547,7 +560,7 @@ class BaseInvenTreeSetting(models.Model): then display 'A4 paper' """ - choices = self.get_setting_choices(self.key, **kwargs) + choices = self.get_setting_choices(self.key, **self.get_kwargs()) if not choices: return self.value @@ -558,12 +571,28 @@ class BaseInvenTreeSetting(models.Model): return self.value - def is_bool(self, **kwargs): + def is_model(self): + """ + Check if this setting references a model instance in the database + """ + + return self.model_name() is not None + + def model_name(self): + """ + Return the model name associated with this setting + """ + + setting = self.get_setting_definition(self.key, **self.get_kwargs()) + + return setting.get('model', None) + + def is_bool(self): """ Check if this setting is required to be a boolean value """ - validator = self.__class__.get_setting_validator(self.key, **kwargs) + validator = self.__class__.get_setting_validator(self.key, **self.get_kwargs()) return self.__class__.validator_is_bool(validator) @@ -576,17 +605,20 @@ class BaseInvenTreeSetting(models.Model): return InvenTree.helpers.str2bool(self.value) - def setting_type(self, **kwargs): + def setting_type(self): """ Return the field type identifier for this setting object """ - if self.is_bool(**kwargs): + if self.is_bool(): return 'boolean' - elif self.is_int(**kwargs): + elif self.is_int(): return 'integer' + elif self.is_model(): + return 'model' + else: return 'string' @@ -603,12 +635,12 @@ class BaseInvenTreeSetting(models.Model): return False - def is_int(self, **kwargs): + def is_int(self,): """ Check if the setting is required to be an integer value: """ - validator = self.__class__.get_setting_validator(self.key, **kwargs) + validator = self.__class__.get_setting_validator(self.key, **self.get_kwargs()) return self.__class__.validator_is_int(validator) @@ -651,88 +683,7 @@ class BaseInvenTreeSetting(models.Model): @property def protected(self): - return self.__class__.is_protected(self.key) - - -class GenericReferencedSettingClass: - """ - This mixin can be used to add reference keys to static properties - - Sample: - ```python - class SampleSetting(GenericReferencedSettingClass, common.models.BaseInvenTreeSetting): - class Meta: - unique_together = [ - ('sample', 'key'), - ] - - REFERENCE_NAME = 'sample' - - @classmethod - def get_setting_definition(cls, key, **kwargs): - # mysampledict contains the dict with all settings for this SettingClass - this could also be a dynamic lookup - - kwargs['settings'] = mysampledict - return super().get_setting_definition(key, **kwargs) - - sample = models.charKey( # the name for this field is the additonal key and must be set in the Meta class an REFERENCE_NAME - max_length=256, - verbose_name=_('sample') - ) - ``` - """ - - REFERENCE_NAME = None - - def _get_reference(self): - """ - Returns dict that can be used as an argument for kwargs calls. - Helps to make overriden calls generic for simple reuse. - - Usage: - ```python - some_random_function(argument0, kwarg1=value1, **self._get_reference()) - ``` - """ - return { - self.REFERENCE_NAME: getattr(self, self.REFERENCE_NAME) - } - - """ - We override the following class methods, - so that we can pass the modified key instance as an additional argument - """ - - def clean(self, **kwargs): - - kwargs[self.REFERENCE_NAME] = getattr(self, self.REFERENCE_NAME) - - super().clean(**kwargs) - - def is_bool(self, **kwargs): - - kwargs[self.REFERENCE_NAME] = getattr(self, self.REFERENCE_NAME) - - return super().is_bool(**kwargs) - - @property - def name(self): - return self.__class__.get_setting_name(self.key, **self._get_reference()) - - @property - def default_value(self): - return self.__class__.get_setting_default(self.key, **self._get_reference()) - - @property - def description(self): - return self.__class__.get_setting_description(self.key, **self._get_reference()) - - @property - def units(self): - return self.__class__.get_setting_units(self.key, **self._get_reference()) - - def choices(self): - return self.__class__.get_setting_choices(self.key, **self._get_reference()) + return self.__class__.is_protected(self.key, **self.get_kwargs()) def settings_group_options(): @@ -1558,6 +1509,16 @@ class InvenTreeUserSetting(BaseInvenTreeSetting): return self.__class__.get_setting(self.key, user=self.user) + def get_kwargs(self): + """ + Explicit kwargs required to uniquely identify a particular setting object, + in addition to the 'key' parameter + """ + + return { + 'user': self.user, + } + class PriceBreak(models.Model): """ diff --git a/InvenTree/common/serializers.py b/InvenTree/common/serializers.py index 9d637c3e39..27fc15bca5 100644 --- a/InvenTree/common/serializers.py +++ b/InvenTree/common/serializers.py @@ -28,6 +28,8 @@ class SettingsSerializer(InvenTreeModelSerializer): choices = serializers.SerializerMethodField() + model_name = serializers.CharField(read_only=True) + def get_choices(self, obj): """ Returns the choices available for a given item @@ -75,6 +77,7 @@ class GlobalSettingsSerializer(SettingsSerializer): 'description', 'type', 'choices', + 'model_name', ] @@ -96,6 +99,7 @@ class UserSettingsSerializer(SettingsSerializer): 'user', 'type', 'choices', + 'model_name', ] @@ -124,6 +128,7 @@ class GenericReferencedSettingSerializer(SettingsSerializer): 'description', 'type', 'choices', + 'model_name', ] # set Meta class diff --git a/InvenTree/plugin/models.py b/InvenTree/plugin/models.py index 0624693abc..1620bed230 100644 --- a/InvenTree/plugin/models.py +++ b/InvenTree/plugin/models.py @@ -102,7 +102,7 @@ class PluginConfig(models.Model): return ret -class PluginSetting(common.models.GenericReferencedSettingClass, common.models.BaseInvenTreeSetting): +class PluginSetting(common.models.BaseInvenTreeSetting): """ This model represents settings for individual plugins """ @@ -112,7 +112,13 @@ class PluginSetting(common.models.GenericReferencedSettingClass, common.models.B ('plugin', 'key'), ] - REFERENCE_NAME = 'plugin' + plugin = models.ForeignKey( + PluginConfig, + related_name='settings', + null=False, + verbose_name=_('Plugin'), + on_delete=models.CASCADE, + ) @classmethod def get_setting_definition(cls, key, **kwargs): @@ -131,7 +137,7 @@ class PluginSetting(common.models.GenericReferencedSettingClass, common.models.B if 'settings' not in kwargs: - plugin = kwargs.pop('plugin', None) + plugin = kwargs.pop('plugin') if plugin: @@ -142,16 +148,18 @@ class PluginSetting(common.models.GenericReferencedSettingClass, common.models.B return super().get_setting_definition(key, **kwargs) - plugin = models.ForeignKey( - PluginConfig, - related_name='settings', - null=False, - verbose_name=_('Plugin'), - on_delete=models.CASCADE, - ) + def get_kwargs(self): + """ + Explicit kwargs required to uniquely identify a particular setting object, + in addition to the 'key' parameter + """ + + return { + 'plugin': self.plugin, + } -class NotificationUserSetting(common.models.GenericReferencedSettingClass, common.models.BaseInvenTreeSetting): +class NotificationUserSetting(common.models.BaseInvenTreeSetting): """ This model represents notification settings for a user """ @@ -161,8 +169,6 @@ class NotificationUserSetting(common.models.GenericReferencedSettingClass, commo ('method', 'user', 'key'), ] - REFERENCE_NAME = 'method' - @classmethod def get_setting_definition(cls, key, **kwargs): from common.notifications import storage @@ -171,6 +177,17 @@ class NotificationUserSetting(common.models.GenericReferencedSettingClass, commo return super().get_setting_definition(key, **kwargs) + def get_kwargs(self): + """ + Explicit kwargs required to uniquely identify a particular setting object, + in addition to the 'key' parameter + """ + + return { + 'method': self.method, + 'user': self.user, + } + method = models.CharField( max_length=255, verbose_name=_('Method'), diff --git a/InvenTree/plugin/samples/integration/sample.py b/InvenTree/plugin/samples/integration/sample.py index 2df3bc116a..af99727ed6 100644 --- a/InvenTree/plugin/samples/integration/sample.py +++ b/InvenTree/plugin/samples/integration/sample.py @@ -65,6 +65,11 @@ class SampleIntegrationPlugin(AppMixin, SettingsMixin, UrlsMixin, NavigationMixi ], 'default': 'A', }, + 'SELECT_COMPANY': { + 'name': 'Company', + 'description': 'Select a company object from the database', + 'model': 'company.Company', + }, } NAVIGATION = [ diff --git a/InvenTree/templates/InvenTree/settings/setting.html b/InvenTree/templates/InvenTree/settings/setting.html index 0bc099f8a2..55c323faec 100644 --- a/InvenTree/templates/InvenTree/settings/setting.html +++ b/InvenTree/templates/InvenTree/settings/setting.html @@ -22,6 +22,9 @@ {{ setting.description }} + {% if setting.model_name %} + Model name: {{ setting.model_name }} + {% endif %} {% if setting.is_bool %}
From a81ea01e8e250d8b47a884b7419603c0a07b1109 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 12 May 2022 17:28:55 +1000 Subject: [PATCH 090/135] Model introspection - Find the class registered to the model (or log an error) - Pass the api_url through to the frontend --- InvenTree/common/models.py | 61 +++++++++++++++++-- InvenTree/common/serializers.py | 5 ++ InvenTree/plugin/models.py | 2 +- .../plugin/samples/integration/sample.py | 7 ++- .../templates/InvenTree/settings/setting.html | 3 - InvenTree/templates/js/dynamic/settings.js | 15 +++++ 6 files changed, 84 insertions(+), 9 deletions(-) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 37a6289d75..a13bbec071 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -17,15 +17,16 @@ import base64 from secrets import compare_digest from datetime import datetime, timedelta +from django.apps import apps from django.db import models, transaction +from django.db.utils import IntegrityError, OperationalError +from django.conf import settings from django.contrib.auth.models import User, Group from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType -from django.db.utils import IntegrityError, OperationalError -from django.conf import settings +from django.contrib.humanize.templatetags.humanize import naturaltime from django.urls import reverse from django.utils.timezone import now -from django.contrib.humanize.templatetags.humanize import naturaltime from djmoney.settings import CURRENCY_CHOICES from djmoney.contrib.exchange.models import convert_money @@ -587,6 +588,58 @@ class BaseInvenTreeSetting(models.Model): return setting.get('model', None) + def model_class(self): + """ + Return the model class associated with this setting, if (and only if): + + - It has a defined 'model' parameter + - The 'model' parameter is of the form app.model + - The 'model' parameter has matches a known app model + """ + + model_name = self.model_name() + + if not model_name: + return None + + try: + (app, mdl) = model_name.strip().split('.') + except ValueError: + logger.error(f"Invalid 'model' parameter for setting {self.key} : '{model_name}'") + return None + + app_models = apps.all_models.get(app, None) + + if app_models is None: + logger.error(f"Error retrieving model class '{model_name}' for setting '{self.key}' - no app named '{app}'") + return None + + model = app_models.get(mdl, None) + + if model is None: + logger.error(f"Error retrieving model class '{model_name}' for setting '{self.key}' - no model named '{mdl}'") + return None + + # Looks like we have found a model! + return model + + def api_url(self): + """ + Return the API url associated with the linked model, + if provided, and valid! + """ + + model_class = self.model_class() + + if model_class: + # If a valid class has been found, see if it has registered an API URL + try: + return model_class.get_api_url() + except: + pass + + return None + def is_bool(self): """ Check if this setting is required to be a boolean value @@ -617,7 +670,7 @@ class BaseInvenTreeSetting(models.Model): return 'integer' elif self.is_model(): - return 'model' + return 'related field' else: return 'string' diff --git a/InvenTree/common/serializers.py b/InvenTree/common/serializers.py index 27fc15bca5..8dd0f5bcee 100644 --- a/InvenTree/common/serializers.py +++ b/InvenTree/common/serializers.py @@ -30,6 +30,8 @@ class SettingsSerializer(InvenTreeModelSerializer): model_name = serializers.CharField(read_only=True) + api_url = serializers.CharField(read_only=True) + def get_choices(self, obj): """ Returns the choices available for a given item @@ -78,6 +80,7 @@ class GlobalSettingsSerializer(SettingsSerializer): 'type', 'choices', 'model_name', + 'api_url', ] @@ -100,6 +103,7 @@ class UserSettingsSerializer(SettingsSerializer): 'type', 'choices', 'model_name', + 'api_url', ] @@ -129,6 +133,7 @@ class GenericReferencedSettingSerializer(SettingsSerializer): 'type', 'choices', 'model_name', + 'api_url', ] # set Meta class diff --git a/InvenTree/plugin/models.py b/InvenTree/plugin/models.py index 1620bed230..18320cc34b 100644 --- a/InvenTree/plugin/models.py +++ b/InvenTree/plugin/models.py @@ -137,7 +137,7 @@ class PluginSetting(common.models.BaseInvenTreeSetting): if 'settings' not in kwargs: - plugin = kwargs.pop('plugin') + plugin = kwargs.pop('plugin', None) if plugin: diff --git a/InvenTree/plugin/samples/integration/sample.py b/InvenTree/plugin/samples/integration/sample.py index af99727ed6..a3a26e7609 100644 --- a/InvenTree/plugin/samples/integration/sample.py +++ b/InvenTree/plugin/samples/integration/sample.py @@ -68,7 +68,12 @@ class SampleIntegrationPlugin(AppMixin, SettingsMixin, UrlsMixin, NavigationMixi 'SELECT_COMPANY': { 'name': 'Company', 'description': 'Select a company object from the database', - 'model': 'company.Company', + 'model': 'company.company', + }, + 'SELECT_PART': { + 'name': 'Part', + 'description': 'Select a part object from the database', + 'model': 'part.part', }, } diff --git a/InvenTree/templates/InvenTree/settings/setting.html b/InvenTree/templates/InvenTree/settings/setting.html index 55c323faec..0bc099f8a2 100644 --- a/InvenTree/templates/InvenTree/settings/setting.html +++ b/InvenTree/templates/InvenTree/settings/setting.html @@ -22,9 +22,6 @@ {{ setting.description }} - {% if setting.model_name %} - Model name: {{ setting.model_name }} - {% endif %} {% if setting.is_bool %}
diff --git a/InvenTree/templates/js/dynamic/settings.js b/InvenTree/templates/js/dynamic/settings.js index 21eb9df5e2..5b52c2a015 100644 --- a/InvenTree/templates/js/dynamic/settings.js +++ b/InvenTree/templates/js/dynamic/settings.js @@ -71,9 +71,24 @@ function editSetting(key, options={}) { help_text: response.description, type: response.type, choices: response.choices, + value: response.value, } }; + // Foreign key lookup available! + if (response.type == 'related field') { + + if (response.model_name && response.api_url) { + fields.value.type = 'related field'; + fields.value.model = response.model_name.split('.').at(-1); + fields.value.api_url = response.api_url; + } else { + // Unknown / unsupported model type, default to 'text' field + fields.value.type = 'text'; + console.warn(`Unsupported model type: '${response.model_name}' for setting '${response.key}'`); + } + } + constructChangeForm(fields, { url: url, method: 'PATCH', From c4fa72e54c7ef0fa2651549a1f65a96fae9a7d52 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 12 May 2022 17:30:52 +1000 Subject: [PATCH 091/135] PEP style fixes --- InvenTree/common/models.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index a13bbec071..2ec83c962c 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -144,7 +144,7 @@ class BaseInvenTreeSetting(models.Model): This is necessary to abtract the settings object from the implementing class (e.g plugins) - + Subclasses should override this function to ensure the kwargs are correctly set. """ @@ -601,7 +601,7 @@ class BaseInvenTreeSetting(models.Model): if not model_name: return None - + try: (app, mdl) = model_name.strip().split('.') except ValueError: @@ -637,7 +637,7 @@ class BaseInvenTreeSetting(models.Model): return model_class.get_api_url() except: pass - + return None def is_bool(self): From a7a80da928fff9b61639dd5388aa4debf0208e4e Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 13 May 2022 01:22:51 +0200 Subject: [PATCH 092/135] Add unittests for auth stack Fixes #2980 --- InvenTree/InvenTree/test_middleware.py | 49 ++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 InvenTree/InvenTree/test_middleware.py diff --git a/InvenTree/InvenTree/test_middleware.py b/InvenTree/InvenTree/test_middleware.py new file mode 100644 index 0000000000..fbf08eb172 --- /dev/null +++ b/InvenTree/InvenTree/test_middleware.py @@ -0,0 +1,49 @@ +"""Tests for middleware functions""" + +from django.test import TestCase + +from django.contrib.auth import get_user_model +from django.urls import reverse + + +class MiddlewareTests(TestCase): + """Test for middleware functions""" + + def check_path(self, url, code=200, **kwargs): + response = self.client.get(url, HTTP_ACCEPT='application/json', **kwargs) + self.assertEqual(response.status_code, code) + return response + + def setUp(self): + super().setUp() + + # Create a user + user = get_user_model() + + self.user = user.objects.create_user(username='username', email='user@email.com', password='password') + self.client.login(username='username', password='password') + + def test_AuthRequiredMiddleware(self): + """Test the auth middleware""" + + # test that /api/ routes go through + self.check_path(reverse('api-inventree-info')) + + # logout + self.client.logout() + + # check that static files go through + self.check_path('/static/admin/fonts/LICENSE.txt') + + # check that account things go through + self.check_path(reverse('account_login')) + + # logout goes diretly to login + self.check_path(reverse('account_logout')) + + # check that frontend code is redirected to login + response = self.check_path(reverse('stats'), 302) + self.assertEqual(response.url, '/accounts/login/?next=/stats/') + + # check that a 401 is raised + self.check_path(reverse('settings.js'), 401) From 6b550e05474c6a8ca6730638a4d2770f9a3da0f8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 13 May 2022 01:23:12 +0200 Subject: [PATCH 093/135] Tests for token Auth --- InvenTree/InvenTree/test_middleware.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/InvenTree/InvenTree/test_middleware.py b/InvenTree/InvenTree/test_middleware.py index fbf08eb172..8728955b1c 100644 --- a/InvenTree/InvenTree/test_middleware.py +++ b/InvenTree/InvenTree/test_middleware.py @@ -47,3 +47,20 @@ class MiddlewareTests(TestCase): # check that a 401 is raised self.check_path(reverse('settings.js'), 401) + + def test_token_auth(self): + """Test auth with token auth""" + # get token + response = self.client.get(reverse('api-token'), format='json', data={}) + token = response.data['token'] + + # logout + self.client.logout() + # this should raise a 401 + self.check_path(reverse('settings.js'), 401) + + # request with token + self.check_path(reverse('settings.js'), HTTP_Authorization= f'Token {token}') + + # should still fail without token + self.check_path(reverse('settings.js'), 401) From 80a2dad34e6f4f041a04479f3b5f3f6f4d887a08 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 13 May 2022 01:23:25 +0200 Subject: [PATCH 094/135] remove dead code --- InvenTree/InvenTree/middleware.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/InvenTree/InvenTree/middleware.py b/InvenTree/InvenTree/middleware.py index b6550379e2..eca078e163 100644 --- a/InvenTree/InvenTree/middleware.py +++ b/InvenTree/InvenTree/middleware.py @@ -71,10 +71,6 @@ class AuthRequiredMiddleware(object): # No authorization was found for the request if not authorized: - # A logout request will redirect the user to the login screen - if request.path_info == reverse_lazy('account_logout'): - return HttpResponseRedirect(reverse_lazy('account_login')) - path = request.path_info # List of URL endpoints we *do not* want to redirect to From 3712daca55e956463e3e9b017982d16896eb93c7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 13 May 2022 01:27:40 +0200 Subject: [PATCH 095/135] remove duplicate code we are already checking this stuff from line 640 --- InvenTree/users/models.py | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index 7c5c406687..7ed689f4a9 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -648,36 +648,6 @@ class Owner(models.Model): owner_type=content_type_id) except Owner.DoesNotExist: pass - else: - # Check whether user_or_group is a Group instance - try: - group = Group.objects.get(pk=user_or_group.id) - except Group.DoesNotExist: - group = None - - if group: - try: - owner = Owner.objects.get(owner_id=user_or_group.id, - owner_type=content_type_id_list[0]) - except Owner.DoesNotExist: - pass - - return owner - - # Check whether user_or_group is a User instance - try: - user = user_model.objects.get(pk=user_or_group.id) - except user_model.DoesNotExist: - user = None - - if user: - try: - owner = Owner.objects.get(owner_id=user_or_group.id, - owner_type=content_type_id_list[1]) - except Owner.DoesNotExist: - pass - - return owner return owner From 53712c2d6c9b70f9ab25fe14b2e8d1c432d9cc60 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 13 May 2022 01:29:32 +0200 Subject: [PATCH 096/135] PEP fix --- InvenTree/InvenTree/middleware.py | 1 - InvenTree/InvenTree/test_middleware.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/InvenTree/InvenTree/middleware.py b/InvenTree/InvenTree/middleware.py index eca078e163..5e122d4689 100644 --- a/InvenTree/InvenTree/middleware.py +++ b/InvenTree/InvenTree/middleware.py @@ -3,7 +3,6 @@ from django.conf import settings from django.contrib.auth.middleware import PersistentRemoteUserMiddleware from django.http import HttpResponse -from django.shortcuts import HttpResponseRedirect from django.shortcuts import redirect from django.urls import reverse_lazy, Resolver404 from django.urls import include, re_path diff --git a/InvenTree/InvenTree/test_middleware.py b/InvenTree/InvenTree/test_middleware.py index 8728955b1c..2bb459bba1 100644 --- a/InvenTree/InvenTree/test_middleware.py +++ b/InvenTree/InvenTree/test_middleware.py @@ -60,7 +60,7 @@ class MiddlewareTests(TestCase): self.check_path(reverse('settings.js'), 401) # request with token - self.check_path(reverse('settings.js'), HTTP_Authorization= f'Token {token}') + self.check_path(reverse('settings.js'), HTTP_Authorization=f'Token {token}') # should still fail without token self.check_path(reverse('settings.js'), 401) From bf2b9d2beb732876f2a49367059fa8cd4b53fd54 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Fri, 13 May 2022 19:37:09 +0200 Subject: [PATCH 097/135] Update test_middleware.py --- InvenTree/InvenTree/test_middleware.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/test_middleware.py b/InvenTree/InvenTree/test_middleware.py index 2bb459bba1..41c148515b 100644 --- a/InvenTree/InvenTree/test_middleware.py +++ b/InvenTree/InvenTree/test_middleware.py @@ -33,7 +33,7 @@ class MiddlewareTests(TestCase): self.client.logout() # check that static files go through - self.check_path('/static/admin/fonts/LICENSE.txt') + self.check_path('/static/admin/css/login.css') # check that account things go through self.check_path(reverse('account_login')) From f3bf12641592f5c7924f794e65400483ddffa120 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 14 May 2022 00:11:18 +0200 Subject: [PATCH 098/135] maybe this ressource can be found in the cloud --- InvenTree/InvenTree/test_middleware.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/test_middleware.py b/InvenTree/InvenTree/test_middleware.py index 41c148515b..a622d01193 100644 --- a/InvenTree/InvenTree/test_middleware.py +++ b/InvenTree/InvenTree/test_middleware.py @@ -33,7 +33,7 @@ class MiddlewareTests(TestCase): self.client.logout() # check that static files go through - self.check_path('/static/admin/css/login.css') + self.check_path('/static/css/inventree.css') # check that account things go through self.check_path(reverse('account_login')) From 2483b746cf1e9a11b2f9bbe3cab36a7fb0cdd89a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 May 2022 00:08:46 +0200 Subject: [PATCH 099/135] remove static test --- InvenTree/InvenTree/test_middleware.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/InvenTree/InvenTree/test_middleware.py b/InvenTree/InvenTree/test_middleware.py index a622d01193..2e5944200e 100644 --- a/InvenTree/InvenTree/test_middleware.py +++ b/InvenTree/InvenTree/test_middleware.py @@ -32,9 +32,6 @@ class MiddlewareTests(TestCase): # logout self.client.logout() - # check that static files go through - self.check_path('/static/css/inventree.css') - # check that account things go through self.check_path(reverse('account_login')) From f8d3aedb1dec23c556a633954213efa594850b5d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 May 2022 00:14:54 +0200 Subject: [PATCH 100/135] Remove static label tests Fixes #2989 --- InvenTree/label/tests.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/InvenTree/label/tests.py b/InvenTree/label/tests.py index 53724a36fc..4cbe591561 100644 --- a/InvenTree/label/tests.py +++ b/InvenTree/label/tests.py @@ -86,13 +86,3 @@ class LabelTest(InvenTreeAPITestCase): with self.assertRaises(ValidationError): validateFilterString(bad_filter_string, model=StockItem) - - def test_label_rendering(self): - """Test label rendering""" - - labels = PartLabel.objects.all() - part = PartLabel.objects.first() - - for label in labels: - url = reverse('api-part-label-print', kwargs={'pk': label.pk}) - self.get(f'{url}?parts={part.pk}', expected_code=200) From a1a1f4debea8b01c253922fd3ca15a48393b9332 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 May 2022 00:18:31 +0200 Subject: [PATCH 101/135] PEP fix --- InvenTree/label/tests.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/InvenTree/label/tests.py b/InvenTree/label/tests.py index 4cbe591561..ad1aaba9c8 100644 --- a/InvenTree/label/tests.py +++ b/InvenTree/label/tests.py @@ -7,13 +7,12 @@ import os from django.conf import settings from django.apps import apps -from django.urls import reverse from django.core.exceptions import ValidationError from InvenTree.helpers import validateFilterString from InvenTree.api_tester import InvenTreeAPITestCase -from .models import StockItemLabel, StockLocationLabel, PartLabel +from .models import StockItemLabel, StockLocationLabel from stock.models import StockItem From 40fa86152e3ac67fea14ca88b86dbc351feafaea Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 May 2022 00:44:26 +0200 Subject: [PATCH 102/135] Add test for wrong token --- InvenTree/InvenTree/test_middleware.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/InvenTree/InvenTree/test_middleware.py b/InvenTree/InvenTree/test_middleware.py index 2e5944200e..6b8e8ec658 100644 --- a/InvenTree/InvenTree/test_middleware.py +++ b/InvenTree/InvenTree/test_middleware.py @@ -59,5 +59,8 @@ class MiddlewareTests(TestCase): # request with token self.check_path(reverse('settings.js'), HTTP_Authorization=f'Token {token}') + # Request with broken token + self.check_path(reverse('settings.js'), 401, HTTP_Authorization=f'Token abcd123') + # should still fail without token self.check_path(reverse('settings.js'), 401) From 2ae5fcf6a8fac89f284a23c564a252eb4fc41858 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 May 2022 00:46:56 +0200 Subject: [PATCH 103/135] PEP fix --- InvenTree/InvenTree/test_middleware.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/test_middleware.py b/InvenTree/InvenTree/test_middleware.py index 6b8e8ec658..bced2eb079 100644 --- a/InvenTree/InvenTree/test_middleware.py +++ b/InvenTree/InvenTree/test_middleware.py @@ -60,7 +60,7 @@ class MiddlewareTests(TestCase): self.check_path(reverse('settings.js'), HTTP_Authorization=f'Token {token}') # Request with broken token - self.check_path(reverse('settings.js'), 401, HTTP_Authorization=f'Token abcd123') + self.check_path(reverse('settings.js'), 401, HTTP_Authorization='Token abcd123') # should still fail without token self.check_path(reverse('settings.js'), 401) From d3cc698500d7c974aa23b988f97615cb01c41af0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 May 2022 01:05:49 +0200 Subject: [PATCH 104/135] merge tests together --- InvenTree/common/tests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/common/tests.py b/InvenTree/common/tests.py index 4a95cad72a..57f2979530 100644 --- a/InvenTree/common/tests.py +++ b/InvenTree/common/tests.py @@ -496,9 +496,9 @@ class PluginSettingsApiTest(InvenTreeAPITestCase): self.assertTrue(response.data['type'], 'string') self.assertTrue(response.data['description'], 'Key required for accessing external API') - def test_invalid_plugin_slug(self): - """Test that an invalid plugin slug returns a 404""" + # Failure mode tests + # Non - exsistant plugin url = reverse('api-plugin-setting-detail', kwargs={'plugin': 'doesnotexist', 'key': 'doesnotmatter'}) response = self.get(url, expected_code=404) From 48d458698a904e9e364e9023041a7b53f38f3498 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 May 2022 01:06:56 +0200 Subject: [PATCH 105/135] reduce lines --- InvenTree/common/tests.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/InvenTree/common/tests.py b/InvenTree/common/tests.py index 57f2979530..b4a301692d 100644 --- a/InvenTree/common/tests.py +++ b/InvenTree/common/tests.py @@ -500,9 +500,7 @@ class PluginSettingsApiTest(InvenTreeAPITestCase): # Non - exsistant plugin url = reverse('api-plugin-setting-detail', kwargs={'plugin': 'doesnotexist', 'key': 'doesnotmatter'}) - response = self.get(url, expected_code=404) - self.assertIn("Plugin 'doesnotexist' not installed", str(response.data)) def test_invalid_setting_key(self): From 38cb557bafb7bb3915f0d3092b6318776fe63860 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 May 2022 01:07:35 +0200 Subject: [PATCH 106/135] Add test for wrong key --- InvenTree/common/tests.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/InvenTree/common/tests.py b/InvenTree/common/tests.py index b4a301692d..eba05639a6 100644 --- a/InvenTree/common/tests.py +++ b/InvenTree/common/tests.py @@ -503,6 +503,11 @@ class PluginSettingsApiTest(InvenTreeAPITestCase): response = self.get(url, expected_code=404) self.assertIn("Plugin 'doesnotexist' not installed", str(response.data)) + # Wrong key + url = reverse('api-plugin-setting-detail', kwargs={'plugin': 'sample', 'key': 'doesnotexsist'}) + response = self.get(url, expected_code=404) + self.assertIn("Plugin 'sample' has no setting matching 'doesnotexsist'", str(response.data)) + def test_invalid_setting_key(self): """Test that an invalid setting key returns a 404""" ... From 43179bbfd5a517dfaa147f6bbc4c6d7d8c3b98f1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 May 2022 01:07:50 +0200 Subject: [PATCH 107/135] remove coverage where it does not make sense --- InvenTree/plugin/api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/InvenTree/plugin/api.py b/InvenTree/plugin/api.py index 64c6f43d57..f3710e4835 100644 --- a/InvenTree/plugin/api.py +++ b/InvenTree/plugin/api.py @@ -144,7 +144,8 @@ class PluginSettingDetail(generics.RetrieveUpdateAPIView): plugin = registry.get_plugin(plugin_slug) if plugin is None: - raise NotFound(detail=f"Plugin '{plugin_slug}' not found") + # This only occurs if the plugin mechanism broke + raise NotFound(detail=f"Plugin '{plugin_slug}' not found") # pragma: no cover settings = getattr(plugin, 'SETTINGS', {}) From 39543555afd817962df5ce83f631891607837779 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 May 2022 01:10:23 +0200 Subject: [PATCH 108/135] Add test for call_funciton --- InvenTree/plugin/samples/integration/test_scheduled_task.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/InvenTree/plugin/samples/integration/test_scheduled_task.py b/InvenTree/plugin/samples/integration/test_scheduled_task.py index 5be1dce250..9573d2fef5 100644 --- a/InvenTree/plugin/samples/integration/test_scheduled_task.py +++ b/InvenTree/plugin/samples/integration/test_scheduled_task.py @@ -47,6 +47,9 @@ class ExampleScheduledTaskPluginTests(TestCase): """check if a function can be called without errors""" self.assertEqual(call_function('schedule', 'member_func'), False) + # Check with wrong key + self.assertEqual(call_function('does_not_exsist', 'member_func'), '') + class ScheduledTaskPluginTests(TestCase): """ Tests for ScheduledTaskPluginTests mixin base """ From 6660069f45adc7a39a0f6433983c93a8ef70aaac Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 May 2022 01:10:46 +0200 Subject: [PATCH 109/135] add docs --- InvenTree/plugin/samples/integration/test_scheduled_task.py | 1 + 1 file changed, 1 insertion(+) diff --git a/InvenTree/plugin/samples/integration/test_scheduled_task.py b/InvenTree/plugin/samples/integration/test_scheduled_task.py index 9573d2fef5..ef9fa04c5a 100644 --- a/InvenTree/plugin/samples/integration/test_scheduled_task.py +++ b/InvenTree/plugin/samples/integration/test_scheduled_task.py @@ -45,6 +45,7 @@ class ExampleScheduledTaskPluginTests(TestCase): def test_calling(self): """check if a function can be called without errors""" + # Check with right parameters self.assertEqual(call_function('schedule', 'member_func'), False) # Check with wrong key From 76f92003fd2339bddf75be2b4603744bbdbb7abf Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 May 2022 01:15:00 +0200 Subject: [PATCH 110/135] Fix assertation --- InvenTree/plugin/samples/integration/test_scheduled_task.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/plugin/samples/integration/test_scheduled_task.py b/InvenTree/plugin/samples/integration/test_scheduled_task.py index ef9fa04c5a..a232324958 100644 --- a/InvenTree/plugin/samples/integration/test_scheduled_task.py +++ b/InvenTree/plugin/samples/integration/test_scheduled_task.py @@ -49,7 +49,7 @@ class ExampleScheduledTaskPluginTests(TestCase): self.assertEqual(call_function('schedule', 'member_func'), False) # Check with wrong key - self.assertEqual(call_function('does_not_exsist', 'member_func'), '') + self.assertEqual(call_function('does_not_exsist', 'member_func'), None) class ScheduledTaskPluginTests(TestCase): From 1cb51d17820ba54bba53799903f05af6bfbba179 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 May 2022 01:29:50 +0200 Subject: [PATCH 111/135] Do not cover custom install dirs --- InvenTree/plugin/serializers.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/InvenTree/plugin/serializers.py b/InvenTree/plugin/serializers.py index 2f3ccee4e2..b3c0471635 100644 --- a/InvenTree/plugin/serializers.py +++ b/InvenTree/plugin/serializers.py @@ -96,8 +96,10 @@ class PluginConfigInstallSerializer(serializers.Serializer): install_name.append(f'{packagename}@{url}') else: install_name.append(url) - else: + else: # pragma: no cover # using a custom package repositories + # This is only for pypa compliant directory services (all current are tested above) + # and not covered by tests. install_name.append('-i') install_name.append(url) install_name.append(packagename) From 640242eca358d2857c268d7f04d64181327eff18 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 May 2022 01:32:52 +0200 Subject: [PATCH 112/135] Add tests for plugin installs form url with pkgn --- InvenTree/plugin/test_api.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/InvenTree/plugin/test_api.py b/InvenTree/plugin/test_api.py index f232773ee1..58911a9bd2 100644 --- a/InvenTree/plugin/test_api.py +++ b/InvenTree/plugin/test_api.py @@ -45,6 +45,14 @@ class PluginDetailAPITest(InvenTreeAPITestCase): }, expected_code=201).data self.assertEqual(data['success'], True) + # valid - github url and packagename + data = self.post(url, { + 'confirm': True, + 'url': self.PKG_URL, + 'packagename': 'minimal', + }, expected_code=201).data + self.assertEqual(data['success'], True) + # invalid tries # no input self.post(url, {}, expected_code=400) From abac26725bef05176d926a34e98949cf33de76e3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 May 2022 01:47:44 +0200 Subject: [PATCH 113/135] use returns for checks --- InvenTree/plugin/registry.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/InvenTree/plugin/registry.py b/InvenTree/plugin/registry.py index da7fb90dbb..6f8c9e0442 100644 --- a/InvenTree/plugin/registry.py +++ b/InvenTree/plugin/registry.py @@ -227,7 +227,7 @@ class PluginsRegistry: if settings.PLUGIN_FILE_CHECKED: logger.info('Plugin file was already checked') - return + return True try: output = str(subprocess.check_output(['pip', 'install', '-U', '-r', settings.PLUGIN_FILE], cwd=os.path.dirname(settings.BASE_DIR)), 'utf-8') @@ -239,6 +239,7 @@ class PluginsRegistry: # do not run again settings.PLUGIN_FILE_CHECKED = True + return 'first_run' # endregion From 9609d8ae0913720516b253ec82a0be4b8844ac40 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 May 2022 01:59:07 +0200 Subject: [PATCH 114/135] Add tests for PLUGIN_ON_STARTUP --- InvenTree/InvenTree/tests.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py index 707960cb28..52fb55cbf0 100644 --- a/InvenTree/InvenTree/tests.py +++ b/InvenTree/InvenTree/tests.py @@ -451,6 +451,11 @@ class TestSettings(TestCase): self.user_mdl = get_user_model() self.env = EnvironmentVarGuard() + # Create a user for auth + user = get_user_model() + self.user = user.objects.create_superuser('testuser', 'test@testing.com', 'password') + self.client.login(username='testuser', password='password') + def run_reload(self): from plugin import registry @@ -494,6 +499,22 @@ class TestSettings(TestCase): # make sure to clean up settings.TESTING_ENV = False + def test_initial_install(self): + """Test if install of plugins on startup works""" + from plugin import registry + + # Check an install run + response = registry.install_plugin_file() + self.assertEqual(response, 'first_run') + + # Set dynamic setting to True and rerun to launch install + InvenTreeSetting.set_setting('PLUGIN_ON_STARTUP', True, self.user) + registry.reload_plugins() + + # Check that there was anotehr run + response = registry.install_plugin_file() + self.assertEqual(response, True) + def test_helpers_cfg_file(self): # normal run - not configured self.assertIn('InvenTree/InvenTree/config.yaml', config.get_config_file()) From aeaf15b37437fcdb27a3bfff6beee8a26e517339 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 May 2022 02:25:30 +0200 Subject: [PATCH 115/135] Test action error messages --- InvenTree/plugin/base/action/test_action.py | 30 +++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/InvenTree/plugin/base/action/test_action.py b/InvenTree/plugin/base/action/test_action.py index 97768e94a0..9de672cd6f 100644 --- a/InvenTree/plugin/base/action/test_action.py +++ b/InvenTree/plugin/base/action/test_action.py @@ -1,6 +1,7 @@ """ Unit tests for action plugins """ from django.test import TestCase +from django.contrib.auth import get_user_model from plugin import InvenTreePlugin from plugin.mixins import ActionMixin @@ -62,3 +63,32 @@ class ActionMixinTests(TestCase): "result": self.ACTION_RETURN + 'result', "info": self.ACTION_RETURN + 'info', }) + + +class APITests(TestCase): + """ Tests for action api """ + + def setUp(self): + # Create a user for auth + user = get_user_model() + self.test_user = user.objects.create_user('testuser', 'test@testing.com', 'password') + self.client.login(username='testuser', password='password') + + def test_post_errors(self): + """Check the possible errors with post""" + + # Test empty request + response = self.client.post('/api/action/') + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.data, + {'error': 'No action specified'} + ) + + # Test non-exsisting action + response = self.client.post('/api/action/', data={'action': "nonexsisting"}) + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.data, + {'error': 'No matching action found', 'action': 'nonexsisting'} + ) From 8c6061d3b754adc3ac1bad0710b8bf0f7882713c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 May 2022 02:32:26 +0200 Subject: [PATCH 116/135] move failing test to seperate test --- InvenTree/plugin/base/integration/test_mixins.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/InvenTree/plugin/base/integration/test_mixins.py b/InvenTree/plugin/base/integration/test_mixins.py index b87583f301..d3912a355a 100644 --- a/InvenTree/plugin/base/integration/test_mixins.py +++ b/InvenTree/plugin/base/integration/test_mixins.py @@ -204,6 +204,8 @@ class APICallMixinTest(BaseMixinDefinition, TestCase): self.assertTrue(result) self.assertIn('data', result,) + def test_function_errors(self): + """Test function errors""" # wrongly defined plugins should not load with self.assertRaises(ValueError): self.mixin_wrong.has_api_call() From 3e6b37bf55740b1da79ff5f3f38abb6bceebfc7f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 May 2022 02:35:06 +0200 Subject: [PATCH 117/135] Add test without simple_response --- InvenTree/plugin/base/integration/test_mixins.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/InvenTree/plugin/base/integration/test_mixins.py b/InvenTree/plugin/base/integration/test_mixins.py index d3912a355a..4ce4455706 100644 --- a/InvenTree/plugin/base/integration/test_mixins.py +++ b/InvenTree/plugin/base/integration/test_mixins.py @@ -163,11 +163,11 @@ class APICallMixinTest(BaseMixinDefinition, TestCase): API_URL_SETTING = 'API_URL' API_TOKEN_SETTING = 'API_TOKEN' - def get_external_url(self): + def get_external_url(self, simple:bool = True): ''' returns data from the sample endpoint ''' - return self.api_call('api/users/2') + return self.api_call('api/users/2', simple_response=simple) self.mixin = MixinCls() class WrongCLS(APICallMixin, InvenTreePlugin): @@ -204,6 +204,11 @@ class APICallMixinTest(BaseMixinDefinition, TestCase): self.assertTrue(result) self.assertIn('data', result,) + # api_call without response + result = self.mixin.get_external_url(False) + self.assertTrue(result) + self.assertEqual(result.reason, 'OK') + def test_function_errors(self): """Test function errors""" # wrongly defined plugins should not load From 442341de50f92a86b5a798dfacf034d0c12b10c9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 May 2022 02:39:55 +0200 Subject: [PATCH 118/135] split into more tests --- InvenTree/plugin/base/integration/test_mixins.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/InvenTree/plugin/base/integration/test_mixins.py b/InvenTree/plugin/base/integration/test_mixins.py index 4ce4455706..dbe96c928f 100644 --- a/InvenTree/plugin/base/integration/test_mixins.py +++ b/InvenTree/plugin/base/integration/test_mixins.py @@ -178,7 +178,8 @@ class APICallMixinTest(BaseMixinDefinition, TestCase): API_URL_SETTING = 'test' self.mixin_wrong2 = WrongCLS2() - def test_function(self): + def test_base_setup(self): + """Test that the base settings work""" # check init self.assertTrue(self.mixin.has_api_call) # api_url @@ -188,6 +189,8 @@ class APICallMixinTest(BaseMixinDefinition, TestCase): headers = self.mixin.api_headers self.assertEqual(headers, {'Bearer': '', 'Content-Type': 'application/json'}) + def test_args(self): + """Test that building up args work""" # api_build_url_args # 1 arg result = self.mixin.api_build_url_args({'a': 'b'}) @@ -199,6 +202,8 @@ class APICallMixinTest(BaseMixinDefinition, TestCase): result = self.mixin.api_build_url_args({'a': 'b', 'c': ['d', 'e', 'f', ]}) self.assertEqual(result, '?a=b&c=d,e,f') + def test_function(self): + """Test that api calls work""" # api_call result = self.mixin.get_external_url() self.assertTrue(result) From 039dbbbe7ea6455f3587b862e7920705291ca941 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 May 2022 02:40:23 +0200 Subject: [PATCH 119/135] fix doctest --- InvenTree/plugin/base/integration/test_mixins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/plugin/base/integration/test_mixins.py b/InvenTree/plugin/base/integration/test_mixins.py index dbe96c928f..fbce6c70a9 100644 --- a/InvenTree/plugin/base/integration/test_mixins.py +++ b/InvenTree/plugin/base/integration/test_mixins.py @@ -209,7 +209,7 @@ class APICallMixinTest(BaseMixinDefinition, TestCase): self.assertTrue(result) self.assertIn('data', result,) - # api_call without response + # api_call without json conversion result = self.mixin.get_external_url(False) self.assertTrue(result) self.assertEqual(result.reason, 'OK') From 7c0dea69b08b456125263eef444af8dc237379ba Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 May 2022 02:42:40 +0200 Subject: [PATCH 120/135] add test for api_call with full url --- InvenTree/plugin/base/integration/test_mixins.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/InvenTree/plugin/base/integration/test_mixins.py b/InvenTree/plugin/base/integration/test_mixins.py index fbce6c70a9..183735686f 100644 --- a/InvenTree/plugin/base/integration/test_mixins.py +++ b/InvenTree/plugin/base/integration/test_mixins.py @@ -214,6 +214,10 @@ class APICallMixinTest(BaseMixinDefinition, TestCase): self.assertTrue(result) self.assertEqual(result.reason, 'OK') + # api_call with full url + result = self.mixin.api_call('https://reqres.in/api/users/2', endpoint_is_url=True) + self.assertTrue(result) + def test_function_errors(self): """Test function errors""" # wrongly defined plugins should not load From 7abdffae0df16131787b5407c15785568bced119 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 May 2022 02:48:59 +0200 Subject: [PATCH 121/135] Add post test --- InvenTree/plugin/base/integration/test_mixins.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/InvenTree/plugin/base/integration/test_mixins.py b/InvenTree/plugin/base/integration/test_mixins.py index 183735686f..4045d79b2d 100644 --- a/InvenTree/plugin/base/integration/test_mixins.py +++ b/InvenTree/plugin/base/integration/test_mixins.py @@ -218,6 +218,15 @@ class APICallMixinTest(BaseMixinDefinition, TestCase): result = self.mixin.api_call('https://reqres.in/api/users/2', endpoint_is_url=True) self.assertTrue(result) + # api_call with post and data + result = self.mixin.api_call( + 'api/users/', + data={"name": "morpheus", "job": "leader"}, + method='POST' + ) + self.assertTrue(result) + self.assertEqual(result['name'], 'morpheus') + def test_function_errors(self): """Test function errors""" # wrongly defined plugins should not load From eb7e9f4703b2cbf4b406a33e9182b05a35ee45e3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 May 2022 02:49:08 +0200 Subject: [PATCH 122/135] test url args in action --- InvenTree/plugin/base/integration/test_mixins.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/InvenTree/plugin/base/integration/test_mixins.py b/InvenTree/plugin/base/integration/test_mixins.py index 4045d79b2d..b9a66062e3 100644 --- a/InvenTree/plugin/base/integration/test_mixins.py +++ b/InvenTree/plugin/base/integration/test_mixins.py @@ -227,6 +227,11 @@ class APICallMixinTest(BaseMixinDefinition, TestCase): self.assertTrue(result) self.assertEqual(result['name'], 'morpheus') + # api_call with filter + result = self.mixin.api_call('api/users', url_args={"page": 2}) + self.assertTrue(result) + self.assertEqual(result['page'], 2) + def test_function_errors(self): """Test function errors""" # wrongly defined plugins should not load From b3f65735bfe9152c31015913427085c74d48d74b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 May 2022 02:53:12 +0200 Subject: [PATCH 123/135] fix test assertations --- InvenTree/InvenTree/tests.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py index 52fb55cbf0..8b2f4c133a 100644 --- a/InvenTree/InvenTree/tests.py +++ b/InvenTree/InvenTree/tests.py @@ -453,8 +453,8 @@ class TestSettings(TestCase): # Create a user for auth user = get_user_model() - self.user = user.objects.create_superuser('testuser', 'test@testing.com', 'password') - self.client.login(username='testuser', password='password') + self.user = user.objects.create_superuser('testuser1', 'test1@testing.com', 'password1') + self.client.login(username='testuser1', password='password1') def run_reload(self): from plugin import registry @@ -472,29 +472,29 @@ class TestSettings(TestCase): # nothing set self.run_reload() - self.assertEqual(user_count(), 0) + self.assertEqual(user_count(), 1) # not enough set self.env.set('INVENTREE_ADMIN_USER', 'admin') # set username self.run_reload() - self.assertEqual(user_count(), 0) + self.assertEqual(user_count(), 1) # enough set self.env.set('INVENTREE_ADMIN_USER', 'admin') # set username self.env.set('INVENTREE_ADMIN_EMAIL', 'info@example.com') # set email self.env.set('INVENTREE_ADMIN_PASSWORD', 'password123') # set password self.run_reload() - self.assertEqual(user_count(), 1) + self.assertEqual(user_count(), 2) # create user manually self.user_mdl.objects.create_user('testuser', 'test@testing.com', 'password') - self.assertEqual(user_count(), 2) + self.assertEqual(user_count(), 3) # check it will not be created again self.env.set('INVENTREE_ADMIN_USER', 'testuser') self.env.set('INVENTREE_ADMIN_EMAIL', 'test@testing.com') self.env.set('INVENTREE_ADMIN_PASSWORD', 'password') self.run_reload() - self.assertEqual(user_count(), 2) + self.assertEqual(user_count(), 3) # make sure to clean up settings.TESTING_ENV = False From e68cd009f66a499d4455c823a8036c431d31c6c0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 May 2022 02:54:21 +0200 Subject: [PATCH 124/135] fix input --- InvenTree/plugin/base/integration/test_mixins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/plugin/base/integration/test_mixins.py b/InvenTree/plugin/base/integration/test_mixins.py index b9a66062e3..d4338e3ac6 100644 --- a/InvenTree/plugin/base/integration/test_mixins.py +++ b/InvenTree/plugin/base/integration/test_mixins.py @@ -228,7 +228,7 @@ class APICallMixinTest(BaseMixinDefinition, TestCase): self.assertEqual(result['name'], 'morpheus') # api_call with filter - result = self.mixin.api_call('api/users', url_args={"page": 2}) + result = self.mixin.api_call('api/users', url_args={'page': '2'}) self.assertTrue(result) self.assertEqual(result['page'], 2) From 68940deb5b88176119fa186f3e4477defcc1a996 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 May 2022 02:56:05 +0200 Subject: [PATCH 125/135] rename test --- InvenTree/plugin/base/integration/test_mixins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/plugin/base/integration/test_mixins.py b/InvenTree/plugin/base/integration/test_mixins.py index d4338e3ac6..3464827bcc 100644 --- a/InvenTree/plugin/base/integration/test_mixins.py +++ b/InvenTree/plugin/base/integration/test_mixins.py @@ -202,7 +202,7 @@ class APICallMixinTest(BaseMixinDefinition, TestCase): result = self.mixin.api_build_url_args({'a': 'b', 'c': ['d', 'e', 'f', ]}) self.assertEqual(result, '?a=b&c=d,e,f') - def test_function(self): + def test_api_call(self): """Test that api calls work""" # api_call result = self.mixin.get_external_url() From 777e3f42df93af9859bc1a286068631f17cb36d0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 May 2022 02:57:23 +0200 Subject: [PATCH 126/135] fix test --- InvenTree/plugin/base/integration/test_mixins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/plugin/base/integration/test_mixins.py b/InvenTree/plugin/base/integration/test_mixins.py index 3464827bcc..76726450d1 100644 --- a/InvenTree/plugin/base/integration/test_mixins.py +++ b/InvenTree/plugin/base/integration/test_mixins.py @@ -240,4 +240,4 @@ class APICallMixinTest(BaseMixinDefinition, TestCase): # cover wrong token setting with self.assertRaises(ValueError): - self.mixin_wrong.has_api_call() + self.mixin_wrong2.has_api_call() From 4fa23cbe4d8155f50be0c8d8a4e34dc8b66d9fe6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 May 2022 02:58:01 +0200 Subject: [PATCH 127/135] split up tests --- InvenTree/plugin/base/integration/test_mixins.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/InvenTree/plugin/base/integration/test_mixins.py b/InvenTree/plugin/base/integration/test_mixins.py index 76726450d1..e92cca1f5f 100644 --- a/InvenTree/plugin/base/integration/test_mixins.py +++ b/InvenTree/plugin/base/integration/test_mixins.py @@ -129,16 +129,18 @@ class NavigationMixinTest(BaseMixinDefinition, TestCase): def test_function(self): # check right configuration self.assertEqual(self.mixin.navigation, [{'name': 'aa', 'link': 'plugin:test:test_view'}, ]) + + # navigation name + self.assertEqual(self.mixin.navigation_name, 'abcd1') + self.assertEqual(self.nothing_mixin.navigation_name, '') + + def test_fail(self): # check wrong links fails with self.assertRaises(NotImplementedError): class NavigationCls(NavigationMixin, InvenTreePlugin): NAVIGATION = ['aa', 'aa'] NavigationCls() - # navigation name - self.assertEqual(self.mixin.navigation_name, 'abcd1') - self.assertEqual(self.nothing_mixin.navigation_name, '') - class APICallMixinTest(BaseMixinDefinition, TestCase): MIXIN_HUMAN_NAME = 'API calls' From 43b1d250161800e20a0986b2fc996ab547403a37 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 May 2022 03:06:35 +0200 Subject: [PATCH 128/135] PEP fix --- InvenTree/plugin/base/integration/test_mixins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/plugin/base/integration/test_mixins.py b/InvenTree/plugin/base/integration/test_mixins.py index e92cca1f5f..c9d32f8abb 100644 --- a/InvenTree/plugin/base/integration/test_mixins.py +++ b/InvenTree/plugin/base/integration/test_mixins.py @@ -165,7 +165,7 @@ class APICallMixinTest(BaseMixinDefinition, TestCase): API_URL_SETTING = 'API_URL' API_TOKEN_SETTING = 'API_TOKEN' - def get_external_url(self, simple:bool = True): + def get_external_url(self, simple: bool = True): ''' returns data from the sample endpoint ''' From 5048d51558c2f1831475c1c97187ae1945aa7e15 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 May 2022 03:19:47 +0200 Subject: [PATCH 129/135] Refactor printer into own base dir --- InvenTree/label/api.py | 2 +- InvenTree/plugin/base/event/events.py | 45 --------------------- InvenTree/plugin/base/label/__init__.py | 0 InvenTree/plugin/base/label/label.py | 52 +++++++++++++++++++++++++ InvenTree/plugin/base/label/mixins.py | 37 ++++++++++++++++++ InvenTree/plugin/mixins/__init__.py | 3 +- 6 files changed, 92 insertions(+), 47 deletions(-) create mode 100644 InvenTree/plugin/base/label/__init__.py create mode 100644 InvenTree/plugin/base/label/label.py create mode 100644 InvenTree/plugin/base/label/mixins.py diff --git a/InvenTree/label/api.py b/InvenTree/label/api.py index c7a824d8c8..cb4b939157 100644 --- a/InvenTree/label/api.py +++ b/InvenTree/label/api.py @@ -156,7 +156,7 @@ class LabelPrintMixin: # Offload a background task to print the provided label offload_task( - 'plugin.events.print_label', + 'plugin.base.label.label.print_label', plugin.plugin_slug(), image, label_instance=label_instance, diff --git a/InvenTree/plugin/base/event/events.py b/InvenTree/plugin/base/event/events.py index eb94f3b0e2..e23fc7fbdc 100644 --- a/InvenTree/plugin/base/event/events.py +++ b/InvenTree/plugin/base/event/events.py @@ -14,8 +14,6 @@ from django.db import transaction from django.db.models.signals import post_save, post_delete from django.dispatch.dispatcher import receiver -import common.notifications - from InvenTree.ready import canAppAccessDatabase, isImportingData from InvenTree.tasks import offload_task @@ -194,46 +192,3 @@ def after_delete(sender, instance, **kwargs): f'{table}.deleted', model=sender.__name__, ) - - -def print_label(plugin_slug, label_image, label_instance=None, user=None): - """ - Print label with the provided plugin. - - This task is nominally handled by the background worker. - - If the printing fails (throws an exception) then the user is notified. - - Arguments: - 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) - - if plugin is None: - logger.error(f"Could not find matching plugin for '{plugin_slug}'") - return - - try: - plugin.print_label(label_image, width=label_instance.width, height=label_instance.height) - except Exception as e: - # Plugin threw an error - notify the user who attempted to print - - ctx = { - 'name': _('Label printing failed'), - 'message': str(e), - } - - logger.error(f"Label printing failed: Sending notification to user '{user}'") - - # Throw an error against the plugin instance - common.notifications.trigger_notifaction( - plugin.plugin_config(), - 'label.printing_failed', - targets=[user], - context=ctx, - delivery_methods=[common.notifications.UIMessageNotification] - ) diff --git a/InvenTree/plugin/base/label/__init__.py b/InvenTree/plugin/base/label/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/InvenTree/plugin/base/label/label.py b/InvenTree/plugin/base/label/label.py new file mode 100644 index 0000000000..52e0b57c1e --- /dev/null +++ b/InvenTree/plugin/base/label/label.py @@ -0,0 +1,52 @@ +"""Functions to print a label to a mixin printer""" +import logging + +from django.utils.translation import gettext_lazy as _ + +from plugin.registry import registry + + +logger = logging.getLogger('inventree') + + +def print_label(plugin_slug, label_image, label_instance=None, user=None): + """ + Print label with the provided plugin. + + This task is nominally handled by the background worker. + + If the printing fails (throws an exception) then the user is notified. + + Arguments: + 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) + + if plugin is None: + logger.error(f"Could not find matching plugin for '{plugin_slug}'") + return + + try: + plugin.print_label(label_image, width=label_instance.width, height=label_instance.height) + except Exception as e: + # Plugin threw an error - notify the user who attempted to print + + ctx = { + 'name': _('Label printing failed'), + 'message': str(e), + } + + logger.error(f"Label printing failed: Sending notification to user '{user}'") + + # Throw an error against the plugin instance + common.notifications.trigger_notifaction( + plugin.plugin_config(), + 'label.printing_failed', + targets=[user], + context=ctx, + delivery_methods=[common.notifications.UIMessageNotification] + ) diff --git a/InvenTree/plugin/base/label/mixins.py b/InvenTree/plugin/base/label/mixins.py new file mode 100644 index 0000000000..1e7b67dd3f --- /dev/null +++ b/InvenTree/plugin/base/label/mixins.py @@ -0,0 +1,37 @@ +"""Plugin mixin classes for label plugins""" + + +class LabelPrintingMixin: + """ + Mixin which enables direct printing of stock labels. + + Each plugin must provide a NAME attribute, which is used to uniquely identify the printer. + + The plugin must also implement the print_label() function + """ + + class MixinMeta: + """ + Meta options for this mixin + """ + MIXIN_NAME = 'Label printing' + + def __init__(self): # pragma: no cover + super().__init__() + self.add_mixin('labels', True, __class__) + + def print_label(self, label, **kwargs): + """ + Callback to print a single label + + Arguments: + label: A black-and-white pillow Image object + + kwargs: + length: The length of the label (in mm) + width: The width of the label (in mm) + + """ + + # Unimplemented (to be implemented by the particular plugin class) + ... # pragma: no cover diff --git a/InvenTree/plugin/mixins/__init__.py b/InvenTree/plugin/mixins/__init__.py index aac3b2b6d2..de8ad4bc03 100644 --- a/InvenTree/plugin/mixins/__init__.py +++ b/InvenTree/plugin/mixins/__init__.py @@ -2,13 +2,14 @@ Utility class to enable simpler imports """ -from ..base.integration.mixins import APICallMixin, AppMixin, LabelPrintingMixin, SettingsMixin, ScheduleMixin, UrlsMixin, NavigationMixin, PanelMixin +from ..base.integration.mixins import APICallMixin, AppMixin, SettingsMixin, ScheduleMixin, UrlsMixin, NavigationMixin, PanelMixin from common.notifications import SingleNotificationMethod, BulkNotificationMethod from ..base.action.mixins import ActionMixin from ..base.barcodes.mixins import BarcodeMixin from ..base.event.mixins import EventMixin +from ..base.label.mixins import LabelPrintingMixin __all__ = [ 'APICallMixin', From 976cbfbb8b511adccabb7c3e3c566f34eb70a132 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 May 2022 03:20:11 +0200 Subject: [PATCH 130/135] remove old definition --- InvenTree/plugin/base/integration/mixins.py | 36 --------------------- 1 file changed, 36 deletions(-) diff --git a/InvenTree/plugin/base/integration/mixins.py b/InvenTree/plugin/base/integration/mixins.py index 257493e156..8c702fcf01 100644 --- a/InvenTree/plugin/base/integration/mixins.py +++ b/InvenTree/plugin/base/integration/mixins.py @@ -369,42 +369,6 @@ class AppMixin: return True -class LabelPrintingMixin: - """ - Mixin which enables direct printing of stock labels. - - Each plugin must provide a NAME attribute, which is used to uniquely identify the printer. - - The plugin must also implement the print_label() function - """ - - class MixinMeta: - """ - Meta options for this mixin - """ - MIXIN_NAME = 'Label printing' - - def __init__(self): # pragma: no cover - super().__init__() - self.add_mixin('labels', True, __class__) - - def print_label(self, label, **kwargs): - """ - Callback to print a single label - - Arguments: - label: A black-and-white pillow Image object - - kwargs: - length: The length of the label (in mm) - width: The width of the label (in mm) - - """ - - # Unimplemented (to be implemented by the particular plugin class) - ... # pragma: no cover - - class APICallMixin: """ Mixin that enables easier API calls for a plugin From 7a102dcd8ebb9c9428e3970445006a4a74855de2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 May 2022 03:22:43 +0200 Subject: [PATCH 131/135] PEP fix --- InvenTree/plugin/base/event/events.py | 2 -- InvenTree/plugin/base/label/label.py | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/InvenTree/plugin/base/event/events.py b/InvenTree/plugin/base/event/events.py index e23fc7fbdc..59dfdce86a 100644 --- a/InvenTree/plugin/base/event/events.py +++ b/InvenTree/plugin/base/event/events.py @@ -7,8 +7,6 @@ from __future__ import unicode_literals import logging -from django.utils.translation import gettext_lazy as _ - from django.conf import settings from django.db import transaction from django.db.models.signals import post_save, post_delete diff --git a/InvenTree/plugin/base/label/label.py b/InvenTree/plugin/base/label/label.py index 52e0b57c1e..7f29ba70f4 100644 --- a/InvenTree/plugin/base/label/label.py +++ b/InvenTree/plugin/base/label/label.py @@ -4,6 +4,7 @@ import logging from django.utils.translation import gettext_lazy as _ from plugin.registry import registry +import common.notifications logger = logging.getLogger('inventree') From 025c6f9b82f86eada8ed86247688b1322e3f5d73 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 May 2022 04:08:45 +0200 Subject: [PATCH 132/135] raise proper error --- InvenTree/plugin/base/label/mixins.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/InvenTree/plugin/base/label/mixins.py b/InvenTree/plugin/base/label/mixins.py index 1e7b67dd3f..4057ad3bf6 100644 --- a/InvenTree/plugin/base/label/mixins.py +++ b/InvenTree/plugin/base/label/mixins.py @@ -1,5 +1,6 @@ """Plugin mixin classes for label plugins""" +from plugin.helpers import MixinNotImplementedError class LabelPrintingMixin: """ @@ -34,4 +35,4 @@ class LabelPrintingMixin: """ # Unimplemented (to be implemented by the particular plugin class) - ... # pragma: no cover + MixinNotImplementedError('This Plugin must implement a `print_label` method') From b0439267ea3f3027b88a21e25c7a452009116432 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 May 2022 04:12:57 +0200 Subject: [PATCH 133/135] fix error-type to use custom errors --- InvenTree/plugin/base/integration/mixins.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/plugin/base/integration/mixins.py b/InvenTree/plugin/base/integration/mixins.py index 8c702fcf01..86e3092e4f 100644 --- a/InvenTree/plugin/base/integration/mixins.py +++ b/InvenTree/plugin/base/integration/mixins.py @@ -433,9 +433,9 @@ class APICallMixin: def has_api_call(self): """Is the mixin ready to call external APIs?""" if not bool(self.API_URL_SETTING): - raise ValueError("API_URL_SETTING must be defined") + raise MixinNotImplementedError("API_URL_SETTING must be defined") if not bool(self.API_TOKEN_SETTING): - raise ValueError("API_TOKEN_SETTING must be defined") + raise MixinNotImplementedError("API_TOKEN_SETTING must be defined") return True @property From 897dd115f98c549654530b7d445616430216ac90 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 May 2022 04:14:13 +0200 Subject: [PATCH 134/135] PEP fix --- InvenTree/plugin/base/label/mixins.py | 1 + 1 file changed, 1 insertion(+) diff --git a/InvenTree/plugin/base/label/mixins.py b/InvenTree/plugin/base/label/mixins.py index 4057ad3bf6..b6c4d53f46 100644 --- a/InvenTree/plugin/base/label/mixins.py +++ b/InvenTree/plugin/base/label/mixins.py @@ -2,6 +2,7 @@ from plugin.helpers import MixinNotImplementedError + class LabelPrintingMixin: """ Mixin which enables direct printing of stock labels. From dcbd9d906bd1b55251e52e66c455b32af1fa756b Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Sun, 15 May 2022 04:47:45 +0200 Subject: [PATCH 135/135] Update errors --- InvenTree/plugin/base/integration/test_mixins.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/InvenTree/plugin/base/integration/test_mixins.py b/InvenTree/plugin/base/integration/test_mixins.py index c9d32f8abb..ef3f7062e3 100644 --- a/InvenTree/plugin/base/integration/test_mixins.py +++ b/InvenTree/plugin/base/integration/test_mixins.py @@ -8,6 +8,7 @@ from django.contrib.auth import get_user_model from plugin import InvenTreePlugin from plugin.mixins import AppMixin, SettingsMixin, UrlsMixin, NavigationMixin, APICallMixin from plugin.urls import PLUGIN_BASE +from plugin.helpers import MixinNotImplementedError class BaseMixinDefinition: @@ -237,9 +238,9 @@ class APICallMixinTest(BaseMixinDefinition, TestCase): def test_function_errors(self): """Test function errors""" # wrongly defined plugins should not load - with self.assertRaises(ValueError): + with self.assertRaises(MixinNotImplementedError): self.mixin_wrong.has_api_call() # cover wrong token setting - with self.assertRaises(ValueError): + with self.assertRaises(MixinNotImplementedError): self.mixin_wrong2.has_api_call()