mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge pull request #2944 from SchrodingersGat/settings-by-key
Settings by key
This commit is contained in:
commit
77eb978c25
@ -4,11 +4,15 @@ InvenTree API version information
|
||||
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 44
|
||||
INVENTREE_API_VERSION = 45
|
||||
|
||||
"""
|
||||
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
||||
|
||||
v45 -> 2022-05-08 : https://github.com/inventree/InvenTree/pull/2944
|
||||
- Settings are now accessed via the API using their unique key, not their PK
|
||||
- This allows the settings to be accessed without prior knowledge of the PK
|
||||
|
||||
v44 -> 2022-05-04 : https://github.com/inventree/InvenTree/pull/2931
|
||||
- Converting more server-side rendered forms to the API
|
||||
- Exposes more core functionality to API endpoints
|
||||
|
@ -158,9 +158,22 @@ class GlobalSettingsDetail(generics.RetrieveUpdateAPIView):
|
||||
- User must have 'staff' status to view / edit
|
||||
"""
|
||||
|
||||
lookup_field = 'key'
|
||||
queryset = common.models.InvenTreeSetting.objects.all()
|
||||
serializer_class = common.serializers.GlobalSettingsSerializer
|
||||
|
||||
def get_object(self):
|
||||
"""
|
||||
Attempt to find a global setting object with the provided key.
|
||||
"""
|
||||
|
||||
key = self.kwargs['key']
|
||||
|
||||
if key not in common.models.InvenTreeSetting.SETTINGS.keys():
|
||||
raise NotFound()
|
||||
|
||||
return common.models.InvenTreeSetting.get_setting_object(key)
|
||||
|
||||
permission_classes = [
|
||||
GlobalSettingsPermissions,
|
||||
]
|
||||
@ -213,9 +226,22 @@ class UserSettingsDetail(generics.RetrieveUpdateAPIView):
|
||||
- User can only view / edit settings their own settings objects
|
||||
"""
|
||||
|
||||
lookup_field = 'key'
|
||||
queryset = common.models.InvenTreeUserSetting.objects.all()
|
||||
serializer_class = common.serializers.UserSettingsSerializer
|
||||
|
||||
def get_object(self):
|
||||
"""
|
||||
Attempt to find a user setting object with the provided key.
|
||||
"""
|
||||
|
||||
key = self.kwargs['key']
|
||||
|
||||
if key not in common.models.InvenTreeUserSetting.SETTINGS.keys():
|
||||
raise NotFound()
|
||||
|
||||
return common.models.InvenTreeUserSetting.get_setting_object(key, user=self.request.user)
|
||||
|
||||
permission_classes = [
|
||||
UserSettingsPermissions,
|
||||
]
|
||||
@ -378,7 +404,7 @@ settings_api_urls = [
|
||||
# User settings
|
||||
re_path(r'^user/', include([
|
||||
# User Settings Detail
|
||||
re_path(r'^(?P<pk>\d+)/', UserSettingsDetail.as_view(), name='api-user-setting-detail'),
|
||||
re_path(r'^(?P<key>\w+)/', UserSettingsDetail.as_view(), name='api-user-setting-detail'),
|
||||
|
||||
# User Settings List
|
||||
re_path(r'^.*$', UserSettingsList.as_view(), name='api-user-setting-list'),
|
||||
@ -396,7 +422,7 @@ settings_api_urls = [
|
||||
# Global settings
|
||||
re_path(r'^global/', include([
|
||||
# Global Settings Detail
|
||||
re_path(r'^(?P<pk>\d+)/', GlobalSettingsDetail.as_view(), name='api-global-setting-detail'),
|
||||
re_path(r'^(?P<key>\w+)/', GlobalSettingsDetail.as_view(), name='api-global-setting-detail'),
|
||||
|
||||
# Global Settings List
|
||||
re_path(r'^.*$', GlobalSettingsList.as_view(), name='api-global-setting-list'),
|
||||
|
@ -186,7 +186,7 @@ class GlobalSettingsApiTest(InvenTreeAPITestCase):
|
||||
# Check default value
|
||||
self.assertEqual(setting.value, 'My company name')
|
||||
|
||||
url = reverse('api-global-setting-detail', kwargs={'pk': setting.pk})
|
||||
url = reverse('api-global-setting-detail', kwargs={'key': setting.key})
|
||||
|
||||
# Test getting via the API
|
||||
for val in ['test', '123', 'My company nam3']:
|
||||
@ -212,6 +212,47 @@ class GlobalSettingsApiTest(InvenTreeAPITestCase):
|
||||
setting.refresh_from_db()
|
||||
self.assertEqual(setting.value, val)
|
||||
|
||||
def test_api_detail(self):
|
||||
"""Test that we can access the detail view for a setting based on the <key>"""
|
||||
|
||||
# These keys are invalid, and should return 404
|
||||
for key in ["apple", "carrot", "dog"]:
|
||||
response = self.get(
|
||||
reverse('api-global-setting-detail', kwargs={'key': key}),
|
||||
expected_code=404,
|
||||
)
|
||||
|
||||
key = 'INVENTREE_INSTANCE'
|
||||
url = reverse('api-global-setting-detail', kwargs={'key': key})
|
||||
|
||||
InvenTreeSetting.objects.filter(key=key).delete()
|
||||
|
||||
# Check that we can access a setting which has not previously been created
|
||||
self.assertFalse(InvenTreeSetting.objects.filter(key=key).exists())
|
||||
|
||||
# Access via the API, and the default value should be received
|
||||
response = self.get(url, expected_code=200)
|
||||
|
||||
self.assertEqual(response.data['value'], 'InvenTree server')
|
||||
|
||||
# Now, the object should have been created in the DB
|
||||
self.patch(
|
||||
url,
|
||||
{
|
||||
'value': 'My new title',
|
||||
},
|
||||
expected_code=200,
|
||||
)
|
||||
|
||||
setting = InvenTreeSetting.objects.get(key=key)
|
||||
|
||||
self.assertEqual(setting.value, 'My new title')
|
||||
|
||||
# And retrieving via the API now returns the updated value
|
||||
response = self.get(url, expected_code=200)
|
||||
|
||||
self.assertEqual(response.data['value'], 'My new title')
|
||||
|
||||
|
||||
class UserSettingsApiTest(InvenTreeAPITestCase):
|
||||
"""
|
||||
@ -226,6 +267,34 @@ class UserSettingsApiTest(InvenTreeAPITestCase):
|
||||
|
||||
self.get(url, expected_code=200)
|
||||
|
||||
def test_user_setting_invalid(self):
|
||||
"""Test a user setting with an invalid key"""
|
||||
|
||||
url = reverse('api-user-setting-detail', kwargs={'key': 'DONKEY'})
|
||||
|
||||
self.get(url, expected_code=404)
|
||||
|
||||
def test_user_setting_init(self):
|
||||
"""Test we can retrieve a setting which has not yet been initialized"""
|
||||
|
||||
key = 'HOMEPAGE_PART_LATEST'
|
||||
|
||||
# Ensure it does not actually exist in the database
|
||||
self.assertFalse(InvenTreeUserSetting.objects.filter(key=key).exists())
|
||||
|
||||
url = reverse('api-user-setting-detail', kwargs={'key': key})
|
||||
|
||||
response = self.get(url, expected_code=200)
|
||||
|
||||
self.assertEqual(response.data['value'], 'True')
|
||||
|
||||
self.patch(url, {'value': 'False'}, expected_code=200)
|
||||
|
||||
setting = InvenTreeUserSetting.objects.get(key=key, user=self.user)
|
||||
|
||||
self.assertEqual(setting.value, 'False')
|
||||
self.assertEqual(setting.to_native_value(), False)
|
||||
|
||||
def test_user_setting_boolean(self):
|
||||
"""
|
||||
Test a boolean user setting value
|
||||
@ -241,7 +310,7 @@ class UserSettingsApiTest(InvenTreeAPITestCase):
|
||||
self.assertEqual(setting.to_native_value(), True)
|
||||
|
||||
# Fetch via API
|
||||
url = reverse('api-user-setting-detail', kwargs={'pk': setting.pk})
|
||||
url = reverse('api-user-setting-detail', kwargs={'key': setting.key})
|
||||
|
||||
response = self.get(url, expected_code=200)
|
||||
|
||||
@ -300,7 +369,7 @@ class UserSettingsApiTest(InvenTreeAPITestCase):
|
||||
user=self.user
|
||||
)
|
||||
|
||||
url = reverse('api-user-setting-detail', kwargs={'pk': setting.pk})
|
||||
url = reverse('api-user-setting-detail', kwargs={'key': setting.key})
|
||||
|
||||
# Check default value
|
||||
self.assertEqual(setting.value, 'YYYY-MM-DD')
|
||||
@ -339,7 +408,7 @@ class UserSettingsApiTest(InvenTreeAPITestCase):
|
||||
user=self.user
|
||||
)
|
||||
|
||||
url = reverse('api-user-setting-detail', kwargs={'pk': setting.pk})
|
||||
url = reverse('api-user-setting-detail', kwargs={'key': setting.key})
|
||||
|
||||
# Check default value for this setting
|
||||
self.assertEqual(setting.value, 10)
|
||||
@ -396,12 +465,35 @@ class NotificationUserSettingsApiTest(InvenTreeAPITestCase):
|
||||
class PluginSettingsApiTest(InvenTreeAPITestCase):
|
||||
"""Tests for the plugin settings API"""
|
||||
|
||||
def test_plugin_list(self):
|
||||
"""List installed plugins via API"""
|
||||
url = reverse('api-plugin-list')
|
||||
|
||||
self.get(url, expected_code=200)
|
||||
|
||||
def test_api_list(self):
|
||||
"""Test list URL"""
|
||||
url = reverse('api-plugin-setting-list')
|
||||
|
||||
self.get(url, expected_code=200)
|
||||
|
||||
def test_invalid_plugin_slug(self):
|
||||
"""Test that an invalid plugin slug returns a 404"""
|
||||
|
||||
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):
|
||||
"""Test that an invalid setting key returns a 404"""
|
||||
...
|
||||
|
||||
def test_uninitialized_setting(self):
|
||||
"""Test that requesting an uninitialized setting creates the setting"""
|
||||
...
|
||||
|
||||
|
||||
class WebhookMessageTests(TestCase):
|
||||
def setUp(self):
|
||||
|
@ -71,10 +71,9 @@ class LabelPrintMixin:
|
||||
|
||||
plugin_key = request.query_params.get('plugin', None)
|
||||
|
||||
for slug, plugin in registry.plugins.items():
|
||||
|
||||
if slug == plugin_key and plugin.mixin_enabled('labels'):
|
||||
plugin = registry.get_plugin(plugin_key)
|
||||
|
||||
if plugin:
|
||||
config = plugin.plugin_config()
|
||||
|
||||
if config and config.active:
|
||||
|
@ -10,11 +10,15 @@ from django.urls import include, re_path
|
||||
from rest_framework import generics
|
||||
from rest_framework import status
|
||||
from rest_framework import permissions
|
||||
from rest_framework.exceptions import NotFound
|
||||
from rest_framework.response import Response
|
||||
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
|
||||
from common.api import GlobalSettingsPermissions
|
||||
from plugin.models import PluginConfig, PluginSetting
|
||||
import plugin.serializers as PluginSerializers
|
||||
from plugin.registry import registry
|
||||
|
||||
|
||||
class PluginList(generics.ListAPIView):
|
||||
@ -98,6 +102,15 @@ class PluginSettingList(generics.ListAPIView):
|
||||
GlobalSettingsPermissions,
|
||||
]
|
||||
|
||||
filter_backends = [
|
||||
DjangoFilterBackend,
|
||||
]
|
||||
|
||||
filter_fields = [
|
||||
'plugin__active',
|
||||
'plugin__key',
|
||||
]
|
||||
|
||||
|
||||
class PluginSettingDetail(generics.RetrieveUpdateAPIView):
|
||||
"""
|
||||
@ -109,6 +122,34 @@ class PluginSettingDetail(generics.RetrieveUpdateAPIView):
|
||||
queryset = PluginSetting.objects.all()
|
||||
serializer_class = PluginSerializers.PluginSettingSerializer
|
||||
|
||||
def get_object(self):
|
||||
"""
|
||||
Lookup the plugin setting object, based on the URL.
|
||||
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']
|
||||
|
||||
# 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")
|
||||
|
||||
# Get the list of settings available for the specified plugin
|
||||
plugin = registry.get_plugin(plugin_slug)
|
||||
|
||||
if plugin is None:
|
||||
raise NotFound(detail=f"Plugin '{plugin_slug}' not found")
|
||||
|
||||
settings = getattr(plugin, 'SETTINGS', {})
|
||||
|
||||
if key not in settings:
|
||||
raise NotFound(detail=f"Plugin '{plugin_slug}' has no setting matching '{key}'")
|
||||
|
||||
return PluginSetting.get_setting_object(key, plugin=plugin)
|
||||
|
||||
# Staff permission required
|
||||
permission_classes = [
|
||||
GlobalSettingsPermissions,
|
||||
@ -119,7 +160,7 @@ plugin_api_urls = [
|
||||
|
||||
# Plugin settings URLs
|
||||
re_path(r'^settings/', include([
|
||||
re_path(r'^(?P<pk>\d+)/', 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'),
|
||||
re_path(r'^.*$', PluginSettingList.as_view(), name='api-plugin-setting-list'),
|
||||
])),
|
||||
|
||||
|
@ -63,6 +63,17 @@ class PluginsRegistry:
|
||||
# mixins
|
||||
self.mixins_settings = {}
|
||||
|
||||
def get_plugin(self, slug):
|
||||
"""
|
||||
Lookup plugin by slug (unique key).
|
||||
"""
|
||||
|
||||
if slug not in self.plugins:
|
||||
logger.warning(f"Plugin registry has no record of plugin '{slug}'")
|
||||
return None
|
||||
|
||||
return self.plugins[slug]
|
||||
|
||||
def call_plugin_function(self, slug, func, *args, **kwargs):
|
||||
"""
|
||||
Call a member function (named by 'func') of the plugin named by 'slug'.
|
||||
@ -73,7 +84,10 @@ class PluginsRegistry:
|
||||
Instead, any error messages are returned to the worker.
|
||||
"""
|
||||
|
||||
plugin = self.plugins[slug]
|
||||
plugin = self.get_plugin(slug)
|
||||
|
||||
if not plugin:
|
||||
return
|
||||
|
||||
plugin_func = getattr(plugin, func)
|
||||
|
||||
|
@ -138,7 +138,7 @@ class PluginSettingSerializer(GenericReferencedSettingSerializer):
|
||||
'plugin',
|
||||
]
|
||||
|
||||
plugin = serializers.PrimaryKeyRelatedField(read_only=True)
|
||||
plugin = serializers.CharField(source='plugin.key', read_only=True)
|
||||
|
||||
|
||||
class NotificationUserSettingSerializer(GenericReferencedSettingSerializer):
|
||||
|
@ -24,7 +24,7 @@
|
||||
<td>
|
||||
{% if setting.is_bool %}
|
||||
<div class='form-check form-switch'>
|
||||
<input class='form-check-input boolean-setting' fieldname='{{ setting.key.upper }}' pk='{{ setting.pk }}' setting='{{ setting.key.upper }}' id='setting-value-{{ setting.key.upper }}' type='checkbox' {% if setting.as_bool %}checked=''{% endif %} {% if plugin %}plugin='{{ plugin.pk }}'{% endif %}{% if user_setting %}user='{{request.user.id}}'{% endif %}{% if notification_setting %}notification='{{request.user.id}}'{% endif %}>
|
||||
<input class='form-check-input boolean-setting' fieldname='{{ setting.key.upper }}' pk='{{ setting.pk }}' setting='{{ setting.key.upper }}' id='setting-value-{{ setting.key.upper }}' type='checkbox' {% if setting.as_bool %}checked=''{% endif %} {% if plugin %}plugin='{{ plugin.slug }}'{% endif %}{% if user_setting %}user='{{request.user.id}}'{% endif %}{% if notification_setting %}notification='{{request.user.id}}'{% endif %}>
|
||||
</div>
|
||||
{% else %}
|
||||
<div id='setting-{{ setting.pk }}'>
|
||||
@ -41,7 +41,7 @@
|
||||
</span>
|
||||
{{ setting.units }}
|
||||
<div class='btn-group float-right'>
|
||||
<button class='btn btn-outline-secondary btn-small btn-edit-setting' pk='{{ setting.pk }}' setting='{{ setting.key.upper }}' title='{% trans "Edit setting" %}' {% if plugin %}plugin='{{ plugin.pk }}'{% endif %}{% if user_setting %}user='{{request.user.id}}'{% endif %}>
|
||||
<button class='btn btn-outline-secondary btn-small btn-edit-setting' pk='{{ setting.pk }}' setting='{{ setting.key.upper }}' title='{% trans "Edit setting" %}' {% if plugin %}plugin='{{ plugin.slug }}'{% endif %}{% if user_setting %}user='{{request.user.id}}'{% endif %}>
|
||||
<span class='fas fa-edit icon-green'></span>
|
||||
</button>
|
||||
</div>
|
||||
|
@ -66,8 +66,8 @@
|
||||
// Callback for when boolean settings are edited
|
||||
$('table').find('.boolean-setting').change(function() {
|
||||
|
||||
var setting = $(this).attr('setting');
|
||||
var pk = $(this).attr('pk');
|
||||
var setting = $(this).attr('setting');
|
||||
var plugin = $(this).attr('plugin');
|
||||
var user = $(this).attr('user');
|
||||
var notification = $(this).attr('notification');
|
||||
@ -75,12 +75,12 @@ $('table').find('.boolean-setting').change(function() {
|
||||
var checked = this.checked;
|
||||
|
||||
// Global setting by default
|
||||
var url = `/api/settings/global/${pk}/`;
|
||||
var url = `/api/settings/global/${setting}/`;
|
||||
|
||||
if (plugin) {
|
||||
url = `/api/plugin/settings/${pk}/`;
|
||||
url = `/api/plugin/settings/${plugin}/${setting}/`;
|
||||
} else if (user) {
|
||||
url = `/api/settings/user/${pk}/`;
|
||||
url = `/api/settings/user/${setting}/`;
|
||||
} else if (notification) {
|
||||
url = `/api/settings/notification/${pk}/`;
|
||||
}
|
||||
@ -105,9 +105,9 @@ $('table').find('.boolean-setting').change(function() {
|
||||
// Callback for when non-boolean settings are edited
|
||||
$('table').find('.btn-edit-setting').click(function() {
|
||||
var setting = $(this).attr('setting');
|
||||
var pk = $(this).attr('pk');
|
||||
var plugin = $(this).attr('plugin');
|
||||
var is_global = true;
|
||||
var notification = $(this).attr('notification');
|
||||
|
||||
if ($(this).attr('user')){
|
||||
is_global = false;
|
||||
@ -117,15 +117,19 @@ $('table').find('.btn-edit-setting').click(function() {
|
||||
|
||||
if (plugin != null) {
|
||||
title = '{% trans "Edit Plugin Setting" %}';
|
||||
} else if (notification) {
|
||||
title = '{% trans "Edit Notification Setting" %}';
|
||||
setting = $(this).attr('pk');
|
||||
} else if (is_global) {
|
||||
title = '{% trans "Edit Global Setting" %}';
|
||||
} else {
|
||||
title = '{% trans "Edit User Setting" %}';
|
||||
}
|
||||
|
||||
editSetting(pk, {
|
||||
editSetting(setting, {
|
||||
plugin: plugin,
|
||||
global: is_global,
|
||||
notification: notification,
|
||||
title: title,
|
||||
});
|
||||
});
|
||||
|
@ -29,23 +29,28 @@ const plugins_enabled = false;
|
||||
{% endif %}
|
||||
|
||||
/*
|
||||
* Edit a setting value
|
||||
* Interactively edit a setting value.
|
||||
* Launches a modal dialog form to adjut the value of the setting.
|
||||
*/
|
||||
function editSetting(pk, options={}) {
|
||||
function editSetting(key, options={}) {
|
||||
|
||||
// Is this a global setting or a user setting?
|
||||
var global = options.global || false;
|
||||
|
||||
var plugin = options.plugin;
|
||||
|
||||
var notification = options.notification;
|
||||
|
||||
var url = '';
|
||||
|
||||
if (plugin) {
|
||||
url = `/api/plugin/settings/${pk}/`;
|
||||
url = `/api/plugin/settings/${plugin}/${key}/`;
|
||||
} else if (notification) {
|
||||
url = `/api/settings/notification/${pk}/`;
|
||||
} else if (global) {
|
||||
url = `/api/settings/global/${pk}/`;
|
||||
url = `/api/settings/global/${key}/`;
|
||||
} else {
|
||||
url = `/api/settings/user/${pk}/`;
|
||||
url = `/api/settings/user/${key}/`;
|
||||
}
|
||||
|
||||
var reload_required = false;
|
||||
|
Loading…
Reference in New Issue
Block a user