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
This commit is contained in:
Matthias Mair 2023-01-27 06:45:14 +01:00 committed by GitHub
parent 83eaa6ef79
commit 1fce1fa695
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 103 additions and 21 deletions

View File

@ -256,7 +256,7 @@ class InvenTreeMetadata(SimpleMetadata):
if isinstance(field, serializers.PrimaryKeyRelatedField): if isinstance(field, serializers.PrimaryKeyRelatedField):
model = field.queryset.model model = field.queryset.model
else: 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 model = None
if model: if model:

View File

@ -17,7 +17,6 @@ from plugin.base.barcodes.api import barcode_api_urls
from plugin.base.locate.api import LocatePluginView from plugin.base.locate.api import LocatePluginView
from plugin.models import PluginConfig, PluginSetting from plugin.models import PluginConfig, PluginSetting
from plugin.plugin import InvenTreePlugin from plugin.plugin import InvenTreePlugin
from plugin.registry import registry
class PluginList(ListAPI): class PluginList(ListAPI):
@ -130,6 +129,12 @@ class PluginActivate(UpdateAPI):
serializer_class = PluginSerializers.PluginConfigEmptySerializer serializer_class = PluginSerializers.PluginConfigEmptySerializer
permission_classes = [IsSuperuser, ] 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): def perform_update(self, serializer):
"""Activate the plugin.""" """Activate the plugin."""
instance = serializer.instance 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. """Check that a plugin for the provided slug exsists and get the config.
Args: Args:
plugin_slug (str): Slug for plugin. plugin_slug (str): Slug for plugin.
plugin_pk (int): Primary key for plugin.
Raises: Raises:
NotFound: If plugin is not installed NotFound: If plugin is not installed
@ -175,22 +181,33 @@ def check_plugin(plugin_slug: str) -> InvenTreePlugin:
Returns: Returns:
InvenTreePlugin: The config object for the provided plugin. InvenTreePlugin: The config object for the provided plugin.
""" """
# Check that the 'plugin' specified is valid! # Make sure that a plugin reference is specified
if not PluginConfig.objects.filter(key=plugin_slug).exists(): if plugin_slug is None and plugin_pk is None:
raise NotFound(detail=f"Plugin '{plugin_slug}' not installed") raise NotFound(detail="Plugin not specified")
# Get the list of settings available for the specified plugin # Define filter
plugin = registry.get_plugin(plugin_slug) 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 # 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 # Check that the plugin is activated
if not plugin.is_active(): if not plugin_cgf.active:
raise NotFound(detail=f"Plugin '{plugin_slug}' is not active") raise NotFound(detail=f"Plugin '{ref}' is not active")
return plugin return plugin_cgf.plugin
class PluginSettingDetail(RetrieveUpdateAPI): class PluginSettingDetail(RetrieveUpdateAPI):
@ -208,16 +225,15 @@ class PluginSettingDetail(RetrieveUpdateAPI):
The URL provides the 'slug' of the plugin, and the 'key' of the setting. 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 Both the 'slug' and 'key' must be valid, else a 404 error is raised
""" """
plugin_slug = self.kwargs['plugin']
key = self.kwargs['key'] key = self.kwargs['key']
# Look up plugin # 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', {}) settings = getattr(plugin, 'settings', {})
if key not in 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) return PluginSetting.get_setting_object(key, plugin=plugin)
@ -234,12 +250,13 @@ plugin_api_urls = [
re_path(r'^plugins/', include([ re_path(r'^plugins/', include([
# Plugin settings URLs # Plugin settings URLs
re_path(r'^settings/', include([ re_path(r'^settings/', include([
re_path(r'^(?P<plugin>\w+)/(?P<key>\w+)/', PluginSettingDetail.as_view(), name='api-plugin-setting-detail'), re_path(r'^(?P<plugin>\w+)/(?P<key>\w+)/', PluginSettingDetail.as_view(), name='api-plugin-setting-detail'), # Used for admin interface
re_path(r'^.*$', PluginSettingList.as_view(), name='api-plugin-setting-list'), re_path(r'^.*$', PluginSettingList.as_view(), name='api-plugin-setting-list'),
])), ])),
# Detail views for a single PluginConfig item # Detail views for a single PluginConfig item
re_path(r'^(?P<pk>\d+)/', include([ re_path(r'^(?P<pk>\d+)/', include([
re_path(r'^settings/(?P<key>\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'^activate/', PluginActivate.as_view(), name='api-plugin-detail-activate'),
re_path(r'^.*$', PluginDetail.as_view(), name='api-plugin-detail'), re_path(r'^.*$', PluginDetail.as_view(), name='api-plugin-detail'),
])), ])),

View File

@ -123,13 +123,15 @@ class PluginConfig(models.Model):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.__org_active = self.active self.__org_active = self.active
# append settings from registry # Append settings from registry
plugin = registry.plugins_full.get(self.key, None) plugin = registry.plugins_full.get(self.key, None)
def get_plugin_meta(name): def get_plugin_meta(name):
if plugin: if not plugin:
return str(getattr(plugin, name, None)) return None
return None if not self.active:
return _('Unvailable')
return str(getattr(plugin, name, None))
self.meta = { self.meta = {
key: get_plugin_meta(key) for key in ['slug', 'human_name', 'description', 'author', key: get_plugin_meta(key) for key in ['slug', 'human_name', 'description', 'author',

View File

@ -2,7 +2,11 @@
from django.urls import reverse from django.urls import reverse
from rest_framework.exceptions import NotFound
from InvenTree.api_tester import InvenTreeAPITestCase, PluginMixin from InvenTree.api_tester import InvenTreeAPITestCase, PluginMixin
from plugin.api import check_plugin
from plugin.models import PluginConfig
class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase): class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase):
@ -86,6 +90,42 @@ class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase):
self.assertEqual(data['confirm'][0].title().upper(), 'Installation not confirmed'.upper()) 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): def test_admin_action(self):
"""Test the PluginConfig action commands.""" """Test the PluginConfig action commands."""
url = reverse('admin:plugin_pluginconfig_changelist') url = reverse('admin:plugin_pluginconfig_changelist')
@ -135,3 +175,26 @@ class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase):
plg_inactive.active = True plg_inactive.active = True
plg_inactive.save() plg_inactive.save()
self.assertEqual(cm.warning.args[0], 'A reload was triggered') 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")