mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
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:
parent
83eaa6ef79
commit
1fce1fa695
@ -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:
|
||||||
|
@ -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'),
|
||||||
])),
|
])),
|
||||||
|
@ -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',
|
||||||
|
@ -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")
|
||||||
|
Loading…
Reference in New Issue
Block a user