From 1fce1fa695fc5ce276be00be01c3b111608b6a35 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Fri, 27 Jan 2023 06:45:14 +0100 Subject: [PATCH] Plugin settings refactor (#4185) * make plugin urls def cleaner * rename plugin managment endpoints * Add setting to plugin edpoint * docstring * [FR] Add API endpoint to activate plugins Fixes #4182 * fix API syntax * Fix plugin detail lookup for deactivated plugins * fix API metadata lookup * fix for api url change * use slug as error reference * fix get action * add tests for activating plugins * Add tests for check_plugin --- InvenTree/InvenTree/metadata.py | 2 +- InvenTree/plugin/api.py | 49 ++++++++++++++++--------- InvenTree/plugin/models.py | 10 +++--- InvenTree/plugin/test_api.py | 63 +++++++++++++++++++++++++++++++++ 4 files changed, 103 insertions(+), 21 deletions(-) diff --git a/InvenTree/InvenTree/metadata.py b/InvenTree/InvenTree/metadata.py index c050f8f34b..77398fdf7a 100644 --- a/InvenTree/InvenTree/metadata.py +++ b/InvenTree/InvenTree/metadata.py @@ -256,7 +256,7 @@ class InvenTreeMetadata(SimpleMetadata): if isinstance(field, serializers.PrimaryKeyRelatedField): model = field.queryset.model else: - logger.debug("Could not extract model for:", field_info['label'], '->', field) + logger.debug("Could not extract model for:", field_info.get('label'), '->', field) model = None if model: diff --git a/InvenTree/plugin/api.py b/InvenTree/plugin/api.py index 3c131fafc4..029cf2b20e 100644 --- a/InvenTree/plugin/api.py +++ b/InvenTree/plugin/api.py @@ -17,7 +17,6 @@ from plugin.base.barcodes.api import barcode_api_urls from plugin.base.locate.api import LocatePluginView from plugin.models import PluginConfig, PluginSetting from plugin.plugin import InvenTreePlugin -from plugin.registry import registry class PluginList(ListAPI): @@ -130,6 +129,12 @@ class PluginActivate(UpdateAPI): serializer_class = PluginSerializers.PluginConfigEmptySerializer permission_classes = [IsSuperuser, ] + def get_object(self): + """Returns the object for the view.""" + if self.request.data.get('pk', None): + return self.queryset.get(pk=self.request.data.get('pk')) + return super().get_object() + def perform_update(self, serializer): """Activate the plugin.""" instance = serializer.instance @@ -161,11 +166,12 @@ class PluginSettingList(ListAPI): ] -def check_plugin(plugin_slug: str) -> InvenTreePlugin: +def check_plugin(plugin_slug: str, plugin_pk: int) -> InvenTreePlugin: """Check that a plugin for the provided slug exsists and get the config. Args: plugin_slug (str): Slug for plugin. + plugin_pk (int): Primary key for plugin. Raises: NotFound: If plugin is not installed @@ -175,22 +181,33 @@ def check_plugin(plugin_slug: str) -> InvenTreePlugin: Returns: InvenTreePlugin: The config object for the provided plugin. """ - # Check that the 'plugin' specified is valid! - if not PluginConfig.objects.filter(key=plugin_slug).exists(): - raise NotFound(detail=f"Plugin '{plugin_slug}' not installed") + # Make sure that a plugin reference is specified + if plugin_slug is None and plugin_pk is None: + raise NotFound(detail="Plugin not specified") - # Get the list of settings available for the specified plugin - plugin = registry.get_plugin(plugin_slug) + # Define filter + filter = {} + if plugin_slug: + filter['key'] = plugin_slug + elif plugin_pk: + filter['pk'] = plugin_pk + ref = plugin_slug or plugin_pk - if plugin is None: + # Check that the 'plugin' specified is valid + try: + plugin_cgf = PluginConfig.objects.get(**filter) + except PluginConfig.DoesNotExist: + raise NotFound(detail=f"Plugin '{ref}' not installed") + + if plugin_cgf is None: # This only occurs if the plugin mechanism broke - raise NotFound(detail=f"Plugin '{plugin_slug}' not found") # pragma: no cover + raise NotFound(detail=f"Plugin '{ref}' not found") # pragma: no cover # Check that the plugin is activated - if not plugin.is_active(): - raise NotFound(detail=f"Plugin '{plugin_slug}' is not active") + if not plugin_cgf.active: + raise NotFound(detail=f"Plugin '{ref}' is not active") - return plugin + return plugin_cgf.plugin class PluginSettingDetail(RetrieveUpdateAPI): @@ -208,16 +225,15 @@ class PluginSettingDetail(RetrieveUpdateAPI): The URL provides the 'slug' of the plugin, and the 'key' of the setting. Both the 'slug' and 'key' must be valid, else a 404 error is raised """ - plugin_slug = self.kwargs['plugin'] key = self.kwargs['key'] # Look up plugin - plugin = check_plugin(plugin_slug) + plugin = check_plugin(plugin_slug=self.kwargs.get('plugin'), plugin_pk=self.kwargs.get('pk')) settings = getattr(plugin, 'settings', {}) if key not in settings: - raise NotFound(detail=f"Plugin '{plugin_slug}' has no setting matching '{key}'") + raise NotFound(detail=f"Plugin '{plugin.slug}' has no setting matching '{key}'") return PluginSetting.get_setting_object(key, plugin=plugin) @@ -234,12 +250,13 @@ plugin_api_urls = [ re_path(r'^plugins/', include([ # Plugin settings URLs re_path(r'^settings/', include([ - re_path(r'^(?P\w+)/(?P\w+)/', PluginSettingDetail.as_view(), name='api-plugin-setting-detail'), + re_path(r'^(?P\w+)/(?P\w+)/', PluginSettingDetail.as_view(), name='api-plugin-setting-detail'), # Used for admin interface re_path(r'^.*$', PluginSettingList.as_view(), name='api-plugin-setting-list'), ])), # Detail views for a single PluginConfig item re_path(r'^(?P\d+)/', include([ + re_path(r'^settings/(?P\w+)/', PluginSettingDetail.as_view(), name='api-plugin-setting-detail-pk'), re_path(r'^activate/', PluginActivate.as_view(), name='api-plugin-detail-activate'), re_path(r'^.*$', PluginDetail.as_view(), name='api-plugin-detail'), ])), diff --git a/InvenTree/plugin/models.py b/InvenTree/plugin/models.py index 5af7478a39..d3e8bba1f2 100644 --- a/InvenTree/plugin/models.py +++ b/InvenTree/plugin/models.py @@ -123,13 +123,15 @@ class PluginConfig(models.Model): super().__init__(*args, **kwargs) self.__org_active = self.active - # append settings from registry + # Append settings from registry plugin = registry.plugins_full.get(self.key, None) def get_plugin_meta(name): - if plugin: - return str(getattr(plugin, name, None)) - return None + if not plugin: + return None + if not self.active: + return _('Unvailable') + return str(getattr(plugin, name, None)) self.meta = { key: get_plugin_meta(key) for key in ['slug', 'human_name', 'description', 'author', diff --git a/InvenTree/plugin/test_api.py b/InvenTree/plugin/test_api.py index 7630bb415c..122445973f 100644 --- a/InvenTree/plugin/test_api.py +++ b/InvenTree/plugin/test_api.py @@ -2,7 +2,11 @@ from django.urls import reverse +from rest_framework.exceptions import NotFound + from InvenTree.api_tester import InvenTreeAPITestCase, PluginMixin +from plugin.api import check_plugin +from plugin.models import PluginConfig class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase): @@ -86,6 +90,42 @@ class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase): self.assertEqual(data['confirm'][0].title().upper(), 'Installation not confirmed'.upper()) + def test_plugin_activate(self): + """Test the plugin activate.""" + + test_plg = self.plugin_confs.first() + + def assert_plugin_active(self, active): + self.assertEqual(PluginConfig.objects.all().first().active, active) + + # Should not work - not a superuser + response = self.client.post(reverse('api-plugin-activate'), {}, follow=True) + self.assertEqual(response.status_code, 403) + + # Make user superuser + self.user.is_superuser = True + self.user.save() + + # Deactivate plugin + test_plg.active = False + test_plg.save() + + # Activate plugin with detail url + assert_plugin_active(self, False) + response = self.client.patch(reverse('api-plugin-detail-activate', kwargs={'pk': test_plg.id}), {}, follow=True) + self.assertEqual(response.status_code, 200) + assert_plugin_active(self, True) + + # Deactivate plugin + test_plg.active = False + test_plg.save() + + # Activate plugin + assert_plugin_active(self, False) + response = self.client.patch(reverse('api-plugin-activate'), {'pk': test_plg.pk}, follow=True) + self.assertEqual(response.status_code, 200) + assert_plugin_active(self, True) + def test_admin_action(self): """Test the PluginConfig action commands.""" url = reverse('admin:plugin_pluginconfig_changelist') @@ -135,3 +175,26 @@ class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase): plg_inactive.active = True plg_inactive.save() self.assertEqual(cm.warning.args[0], 'A reload was triggered') + + def test_check_plugin(self): + """Test check_plugin function.""" + + # No argument + with self.assertRaises(NotFound) as exc: + check_plugin(plugin_slug=None, plugin_pk=None) + self.assertEqual(str(exc.exception.detail), 'Plugin not specified') + + # Wrong with slug + with self.assertRaises(NotFound) as exc: + check_plugin(plugin_slug='123abc', plugin_pk=None) + self.assertEqual(str(exc.exception.detail), "Plugin '123abc' not installed") + + # Wrong with pk + with self.assertRaises(NotFound) as exc: + check_plugin(plugin_slug=None, plugin_pk='123') + self.assertEqual(str(exc.exception.detail), "Plugin '123' not installed") + + # Not active + with self.assertRaises(NotFound) as exc: + check_plugin(plugin_slug='inventreebarcode', plugin_pk=None) + self.assertEqual(str(exc.exception.detail), "Plugin 'inventreebarcode' is not active")