mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-16 20:15:44 +00:00
Plugin API lookup key (#7224)
* Lookup plugin by slug - Adjust plugin API to use plugin key and not (variable) pk value * Fix for plugin table in CUI (legacy interface) * Fix API endpoint layout: - Move special endpoints first - Fix "metadata" endpoint - Allow custom "lookup_field" attribute for MetadataView * Add "active_plugins" count to RegistryStatusView * Updates for PUI - Plugin management now uses slug rather than pk * Bump API version * Remove unused code * Adds index on 'key' field for PluginConfig model * Fix URL structure * Unit test updates * Unit test updates * More unit test fixes
This commit is contained in:
@ -550,6 +550,10 @@ class MetadataView(RetrieveUpdateAPI):
|
||||
"""Return the model type associated with this API instance."""
|
||||
model = self.kwargs.get(self.MODEL_REF, None)
|
||||
|
||||
if 'lookup_field' in self.kwargs:
|
||||
# Set custom lookup field (instead of default 'pk' value) if supplied
|
||||
self.lookup_field = self.kwargs.pop('lookup_field')
|
||||
|
||||
if model is None:
|
||||
raise ValidationError(
|
||||
f"MetadataView called without '{self.MODEL_REF}' parameter"
|
||||
|
@ -1,11 +1,14 @@
|
||||
"""InvenTree API version information."""
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 196
|
||||
INVENTREE_API_VERSION = 197
|
||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||
|
||||
INVENTREE_API_TEXT = """
|
||||
|
||||
v197 - 2024-05-14 : https://github.com/inventree/InvenTree/pull/7224
|
||||
- Refactor the plugin API endpoints to use the plugin "key" for lookup, rather than the PK value
|
||||
|
||||
v196 - 2024-05-05 : https://github.com/inventree/InvenTree/pull/7160
|
||||
- Adds "location" field to BuildOutputComplete API endpoint
|
||||
|
||||
|
@ -620,7 +620,7 @@ class PluginSettingsApiTest(PluginMixin, InvenTreeAPITestCase):
|
||||
|
||||
# get data
|
||||
url = reverse(
|
||||
'api-plugin-setting-detail', kwargs={'plugin': 'sample', 'key': 'API_KEY'}
|
||||
'api-plugin-setting-detail', kwargs={'key': 'sample', 'setting': 'API_KEY'}
|
||||
)
|
||||
response = self.get(url, expected_code=200)
|
||||
|
||||
@ -637,7 +637,7 @@ class PluginSettingsApiTest(PluginMixin, InvenTreeAPITestCase):
|
||||
# Non-existent plugin
|
||||
url = reverse(
|
||||
'api-plugin-setting-detail',
|
||||
kwargs={'plugin': 'doesnotexist', 'key': 'doesnotmatter'},
|
||||
kwargs={'key': 'doesnotexist', 'setting': 'doesnotmatter'},
|
||||
)
|
||||
response = self.get(url, expected_code=404)
|
||||
self.assertIn("Plugin 'doesnotexist' not installed", str(response.data))
|
||||
@ -645,7 +645,7 @@ class PluginSettingsApiTest(PluginMixin, InvenTreeAPITestCase):
|
||||
# Wrong key
|
||||
url = reverse(
|
||||
'api-plugin-setting-detail',
|
||||
kwargs={'plugin': 'sample', 'key': 'doesnotexist'},
|
||||
kwargs={'key': 'sample', 'setting': 'doesnotexist'},
|
||||
)
|
||||
response = self.get(url, expected_code=404)
|
||||
self.assertIn(
|
||||
|
@ -155,6 +155,7 @@ class PluginDetail(RetrieveUpdateDestroyAPI):
|
||||
|
||||
queryset = PluginConfig.objects.all()
|
||||
serializer_class = PluginSerializers.PluginConfigSerializer
|
||||
lookup_field = 'key'
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
"""Handle DELETE request for a PluginConfig instance.
|
||||
@ -200,6 +201,7 @@ class PluginUninstall(UpdateAPI):
|
||||
queryset = PluginConfig.objects.all()
|
||||
serializer_class = PluginSerializers.PluginUninstallSerializer
|
||||
permission_classes = [IsSuperuser]
|
||||
lookup_field = 'key'
|
||||
|
||||
def perform_update(self, serializer):
|
||||
"""Uninstall the plugin."""
|
||||
@ -219,6 +221,7 @@ class PluginActivate(UpdateAPI):
|
||||
queryset = PluginConfig.objects.all()
|
||||
serializer_class = PluginSerializers.PluginActivateSerializer
|
||||
permission_classes = [IsSuperuser]
|
||||
lookup_field = 'key'
|
||||
|
||||
def get_object(self):
|
||||
"""Returns the object for the view."""
|
||||
@ -320,10 +323,10 @@ class PluginAllSettingList(APIView):
|
||||
@extend_schema(
|
||||
responses={200: PluginSerializers.PluginSettingSerializer(many=True)}
|
||||
)
|
||||
def get(self, request, pk):
|
||||
def get(self, request, key):
|
||||
"""Get all settings for a plugin config."""
|
||||
# look up the plugin
|
||||
plugin = check_plugin(None, pk)
|
||||
plugin = check_plugin(key, None)
|
||||
|
||||
settings = getattr(plugin, 'settings', {})
|
||||
|
||||
@ -352,21 +355,21 @@ 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
|
||||
"""
|
||||
key = self.kwargs['key']
|
||||
setting_key = self.kwargs['setting']
|
||||
|
||||
# Look up plugin
|
||||
plugin = check_plugin(
|
||||
plugin_slug=self.kwargs.get('plugin'), plugin_pk=self.kwargs.get('pk')
|
||||
)
|
||||
plugin = check_plugin(self.kwargs.pop('key', None), None)
|
||||
|
||||
settings = getattr(plugin, 'settings', {})
|
||||
|
||||
if key not in settings:
|
||||
if setting_key not in settings:
|
||||
raise NotFound(
|
||||
detail=f"Plugin '{plugin.slug}' has no setting matching '{key}'"
|
||||
detail=f"Plugin '{plugin.slug}' has no setting matching '{setting_key}'"
|
||||
)
|
||||
|
||||
return PluginSetting.get_setting_object(key, plugin=plugin.plugin_config())
|
||||
return PluginSetting.get_setting_object(
|
||||
setting_key, plugin=plugin.plugin_config()
|
||||
)
|
||||
|
||||
# Staff permission required
|
||||
permission_classes = [GlobalSettingsPermissions]
|
||||
@ -384,7 +387,7 @@ class RegistryStatusView(APIView):
|
||||
|
||||
@extend_schema(responses={200: PluginSerializers.PluginRegistryStatusSerializer()})
|
||||
def get(self, request):
|
||||
"""Show registry status information."""
|
||||
"""Show plugin registry status information."""
|
||||
error_list = []
|
||||
|
||||
for stage, errors in registry.errors.items():
|
||||
@ -397,7 +400,8 @@ class RegistryStatusView(APIView):
|
||||
})
|
||||
|
||||
result = PluginSerializers.PluginRegistryStatusSerializer({
|
||||
'registry_errors': error_list
|
||||
'registry_errors': error_list,
|
||||
'active_plugins': PluginConfig.objects.filter(active=True).count(),
|
||||
}).data
|
||||
|
||||
return Response(result)
|
||||
@ -410,31 +414,34 @@ plugin_api_urls = [
|
||||
path(
|
||||
'plugins/',
|
||||
include([
|
||||
# Plugin settings URLs
|
||||
# Plugin management
|
||||
path('reload/', PluginReload.as_view(), name='api-plugin-reload'),
|
||||
path('install/', PluginInstall.as_view(), name='api-plugin-install'),
|
||||
# Registry status
|
||||
path(
|
||||
'status/',
|
||||
RegistryStatusView.as_view(),
|
||||
name='api-plugin-registry-status',
|
||||
),
|
||||
path(
|
||||
'settings/',
|
||||
include([
|
||||
re_path(
|
||||
r'^(?P<plugin>[-\w]+)/(?P<key>\w+)/',
|
||||
PluginSettingDetail.as_view(),
|
||||
name='api-plugin-setting-detail',
|
||||
), # Used for admin interface
|
||||
path(
|
||||
'', PluginSettingList.as_view(), name='api-plugin-setting-list'
|
||||
),
|
||||
)
|
||||
]),
|
||||
),
|
||||
# Detail views for a single PluginConfig item
|
||||
# Lookup for individual plugins (based on 'key', not 'pk')
|
||||
path(
|
||||
'<int:pk>/',
|
||||
'<str:key>/',
|
||||
include([
|
||||
path(
|
||||
'settings/',
|
||||
include([
|
||||
re_path(
|
||||
r'^(?P<key>\w+)/',
|
||||
r'^(?P<setting>\w+)/',
|
||||
PluginSettingDetail.as_view(),
|
||||
name='api-plugin-setting-detail-pk',
|
||||
name='api-plugin-setting-detail',
|
||||
),
|
||||
path(
|
||||
'',
|
||||
@ -443,6 +450,12 @@ plugin_api_urls = [
|
||||
),
|
||||
]),
|
||||
),
|
||||
path(
|
||||
'metadata/',
|
||||
MetadataView.as_view(),
|
||||
{'model': PluginConfig, 'lookup_field': 'key'},
|
||||
name='api-plugin-metadata',
|
||||
),
|
||||
path(
|
||||
'activate/',
|
||||
PluginActivate.as_view(),
|
||||
@ -456,23 +469,6 @@ plugin_api_urls = [
|
||||
path('', PluginDetail.as_view(), name='api-plugin-detail'),
|
||||
]),
|
||||
),
|
||||
# Metadata
|
||||
path(
|
||||
'metadata/',
|
||||
MetadataView.as_view(),
|
||||
{'model': PluginConfig},
|
||||
name='api-plugin-metadata',
|
||||
),
|
||||
# Plugin management
|
||||
path('reload/', PluginReload.as_view(), name='api-plugin-reload'),
|
||||
path('install/', PluginInstall.as_view(), name='api-plugin-install'),
|
||||
# Registry status
|
||||
path(
|
||||
'status/',
|
||||
RegistryStatusView.as_view(),
|
||||
name='api-plugin-registry-status',
|
||||
),
|
||||
# Anything else
|
||||
path('', PluginList.as_view(), name='api-plugin-list'),
|
||||
]),
|
||||
),
|
||||
|
@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.2.12 on 2024-05-14 22:40
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('plugin', '0008_pluginconfig_package_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='pluginconfig',
|
||||
name='key',
|
||||
field=models.CharField(db_index=True, help_text='Key of plugin', max_length=255, unique=True, verbose_name='Key'),
|
||||
),
|
||||
]
|
@ -31,7 +31,11 @@ class PluginConfig(InvenTree.models.MetadataMixin, models.Model):
|
||||
verbose_name_plural = _('Plugin Configurations')
|
||||
|
||||
key = models.CharField(
|
||||
unique=True, max_length=255, verbose_name=_('Key'), help_text=_('Key of plugin')
|
||||
unique=True,
|
||||
db_index=True,
|
||||
max_length=255,
|
||||
verbose_name=_('Key'),
|
||||
help_text=_('Key of plugin'),
|
||||
)
|
||||
|
||||
name = models.CharField(
|
||||
|
@ -261,4 +261,10 @@ class PluginRegistryErrorSerializer(serializers.Serializer):
|
||||
class PluginRegistryStatusSerializer(serializers.Serializer):
|
||||
"""Serializer for plugin registry status."""
|
||||
|
||||
class Meta:
|
||||
"""Meta for serializer."""
|
||||
|
||||
fields = ['active_plugins', 'registry_errors']
|
||||
|
||||
active_plugins = serializers.IntegerField(read_only=True)
|
||||
registry_errors = serializers.ListField(child=PluginRegistryErrorSerializer())
|
||||
|
@ -97,12 +97,10 @@ class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase):
|
||||
assert plgs is not None
|
||||
self.assertEqual(plgs.active, active)
|
||||
|
||||
url = reverse('api-plugin-detail-activate', kwargs={'key': test_plg.key})
|
||||
|
||||
# Should not work - not a superuser
|
||||
response = self.client.post(
|
||||
reverse('api-plugin-detail-activate', kwargs={'pk': test_plg.pk}),
|
||||
{},
|
||||
follow=True,
|
||||
)
|
||||
response = self.client.post(url, {}, follow=True)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
# Make user superuser
|
||||
@ -115,11 +113,7 @@ class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase):
|
||||
|
||||
# Activate plugin with detail url
|
||||
assert_plugin_active(self, False)
|
||||
response = self.client.patch(
|
||||
reverse('api-plugin-detail-activate', kwargs={'pk': test_plg.pk}),
|
||||
{},
|
||||
follow=True,
|
||||
)
|
||||
response = self.client.patch(url, {}, follow=True)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
assert_plugin_active(self, True)
|
||||
|
||||
@ -129,11 +123,7 @@ class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase):
|
||||
|
||||
# Activate plugin
|
||||
assert_plugin_active(self, False)
|
||||
response = self.client.patch(
|
||||
reverse('api-plugin-detail-activate', kwargs={'pk': test_plg.pk}),
|
||||
{},
|
||||
follow=True,
|
||||
)
|
||||
response = self.client.patch(url, {}, follow=True)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
assert_plugin_active(self, True)
|
||||
|
||||
@ -237,7 +227,7 @@ class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase):
|
||||
cfg = PluginConfig.objects.filter(key='sample').first()
|
||||
assert cfg is not None
|
||||
|
||||
url = reverse('api-plugin-detail-activate', kwargs={'pk': cfg.pk})
|
||||
url = reverse('api-plugin-detail-activate', kwargs={'key': cfg.key})
|
||||
self.client.patch(url, {}, expected_code=200)
|
||||
|
||||
# Valid plugin settings endpoints
|
||||
@ -246,7 +236,8 @@ class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase):
|
||||
for key in valid_settings:
|
||||
response = self.get(
|
||||
reverse(
|
||||
'api-plugin-setting-detail', kwargs={'plugin': 'sample', 'key': key}
|
||||
'api-plugin-setting-detail',
|
||||
kwargs={'key': 'sample', 'setting': key},
|
||||
)
|
||||
)
|
||||
|
||||
@ -256,7 +247,7 @@ class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase):
|
||||
response = self.get(
|
||||
reverse(
|
||||
'api-plugin-setting-detail',
|
||||
kwargs={'plugin': 'sample', 'key': 'INVALID_SETTING'},
|
||||
kwargs={'key': 'sample', 'setting': 'INVALID_SETTING'},
|
||||
),
|
||||
expected_code=404,
|
||||
)
|
||||
@ -265,7 +256,7 @@ class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase):
|
||||
response = self.get(
|
||||
reverse(
|
||||
'api-plugin-setting-detail',
|
||||
kwargs={'plugin': 'sample', 'key': 'PROTECTED_SETTING'},
|
||||
kwargs={'key': 'sample', 'setting': 'PROTECTED_SETTING'},
|
||||
),
|
||||
expected_code=200,
|
||||
)
|
||||
@ -276,7 +267,7 @@ class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase):
|
||||
response = self.patch(
|
||||
reverse(
|
||||
'api-plugin-setting-detail',
|
||||
kwargs={'plugin': 'sample', 'key': 'NUMERICAL_SETTING'},
|
||||
kwargs={'key': 'sample', 'setting': 'NUMERICAL_SETTING'},
|
||||
),
|
||||
{'value': 456},
|
||||
expected_code=200,
|
||||
@ -288,7 +279,7 @@ class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase):
|
||||
response = self.get(
|
||||
reverse(
|
||||
'api-plugin-setting-detail',
|
||||
kwargs={'plugin': 'sample', 'key': 'NUMERICAL_SETTING'},
|
||||
kwargs={'key': 'sample', 'setting': 'NUMERICAL_SETTING'},
|
||||
),
|
||||
expected_code=200,
|
||||
)
|
||||
|
@ -114,9 +114,9 @@ function loadPluginTable(table, options={}) {
|
||||
// Check if custom plugins are enabled for this instance
|
||||
if (options.custom && !row.is_builtin && row.is_installed) {
|
||||
if (row.active) {
|
||||
buttons += makeIconButton('fa-stop-circle icon-red', 'btn-plugin-disable', row.pk, '{% trans "Disable Plugin" %}');
|
||||
buttons += makeIconButton('fa-stop-circle icon-red', 'btn-plugin-disable', row.key, '{% trans "Disable Plugin" %}');
|
||||
} else {
|
||||
buttons += makeIconButton('fa-play-circle icon-green', 'btn-plugin-enable', row.pk, '{% trans "Enable Plugin" %}');
|
||||
buttons += makeIconButton('fa-play-circle icon-green', 'btn-plugin-enable', row.key, '{% trans "Enable Plugin" %}');
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user