mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-31 05:05:42 +00:00 
			
		
		
		
	Merge branch 'inventree:master' into theme-tests
This commit is contained in:
		| @@ -4,11 +4,19 @@ InvenTree API version information | |||||||
|  |  | ||||||
|  |  | ||||||
| # InvenTree API version | # InvenTree API version | ||||||
| INVENTREE_API_VERSION = 44 | INVENTREE_API_VERSION = 46 | ||||||
|  |  | ||||||
| """ | """ | ||||||
| Increment this API version number whenever there is a significant change to the API that any clients need to know about | Increment this API version number whenever there is a significant change to the API that any clients need to know about | ||||||
|  |  | ||||||
|  | v46 -> 2022-05-09 | ||||||
|  |     - Fixes read permissions on settings API | ||||||
|  |     - Allows non-staff users to read global settings via the API | ||||||
|  |  | ||||||
|  | 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 | v44 -> 2022-05-04 : https://github.com/inventree/InvenTree/pull/2931 | ||||||
|     - Converting more server-side rendered forms to the API |     - Converting more server-side rendered forms to the API | ||||||
|     - Exposes more core functionality to API endpoints |     - Exposes more core functionality to API endpoints | ||||||
|   | |||||||
| @@ -421,7 +421,10 @@ class DataFileUploadSerializer(serializers.Serializer): | |||||||
|         - Fuzzy match |         - Fuzzy match | ||||||
|         """ |         """ | ||||||
|  |  | ||||||
|         column_name = column_name.strip() |         if not column_name: | ||||||
|  |             return None | ||||||
|  |  | ||||||
|  |         column_name = str(column_name).strip() | ||||||
|  |  | ||||||
|         column_name_lower = column_name.lower() |         column_name_lower = column_name.lower() | ||||||
|  |  | ||||||
|   | |||||||
| @@ -146,7 +146,12 @@ class GlobalSettingsPermissions(permissions.BasePermission): | |||||||
|         try: |         try: | ||||||
|             user = request.user |             user = request.user | ||||||
|  |  | ||||||
|  |             if request.method in ['GET', 'HEAD', 'OPTIONS']: | ||||||
|  |                 return True | ||||||
|  |             else: | ||||||
|  |                 # Any other methods require staff access permissions | ||||||
|                 return user.is_staff |                 return user.is_staff | ||||||
|  |  | ||||||
|         except AttributeError:  # pragma: no cover |         except AttributeError:  # pragma: no cover | ||||||
|             return False |             return False | ||||||
|  |  | ||||||
| @@ -158,10 +163,24 @@ class GlobalSettingsDetail(generics.RetrieveUpdateAPIView): | |||||||
|     - User must have 'staff' status to view / edit |     - User must have 'staff' status to view / edit | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|  |     lookup_field = 'key' | ||||||
|     queryset = common.models.InvenTreeSetting.objects.all() |     queryset = common.models.InvenTreeSetting.objects.all() | ||||||
|     serializer_class = common.serializers.GlobalSettingsSerializer |     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 = [ |     permission_classes = [ | ||||||
|  |         permissions.IsAuthenticated, | ||||||
|         GlobalSettingsPermissions, |         GlobalSettingsPermissions, | ||||||
|     ] |     ] | ||||||
|  |  | ||||||
| @@ -213,9 +232,22 @@ class UserSettingsDetail(generics.RetrieveUpdateAPIView): | |||||||
|     - User can only view / edit settings their own settings objects |     - User can only view / edit settings their own settings objects | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|  |     lookup_field = 'key' | ||||||
|     queryset = common.models.InvenTreeUserSetting.objects.all() |     queryset = common.models.InvenTreeUserSetting.objects.all() | ||||||
|     serializer_class = common.serializers.UserSettingsSerializer |     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 = [ |     permission_classes = [ | ||||||
|         UserSettingsPermissions, |         UserSettingsPermissions, | ||||||
|     ] |     ] | ||||||
| @@ -378,7 +410,7 @@ settings_api_urls = [ | |||||||
|     # User settings |     # User settings | ||||||
|     re_path(r'^user/', include([ |     re_path(r'^user/', include([ | ||||||
|         # User Settings Detail |         # 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 |         # User Settings List | ||||||
|         re_path(r'^.*$', UserSettingsList.as_view(), name='api-user-setting-list'), |         re_path(r'^.*$', UserSettingsList.as_view(), name='api-user-setting-list'), | ||||||
| @@ -396,7 +428,7 @@ settings_api_urls = [ | |||||||
|     # Global settings |     # Global settings | ||||||
|     re_path(r'^global/', include([ |     re_path(r'^global/', include([ | ||||||
|         # Global Settings Detail |         # 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 |         # Global Settings List | ||||||
|         re_path(r'^.*$', GlobalSettingsList.as_view(), name='api-global-setting-list'), |         re_path(r'^.*$', GlobalSettingsList.as_view(), name='api-global-setting-list'), | ||||||
|   | |||||||
| @@ -842,6 +842,13 @@ class InvenTreeSetting(BaseInvenTreeSetting): | |||||||
|             'validator': bool, |             'validator': bool, | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|  |         'BARCODE_WEBCAM_SUPPORT': { | ||||||
|  |             'name': _('Barcode Webcam Support'), | ||||||
|  |             'description': _('Allow barcode scanning via webcam in browser'), | ||||||
|  |             'default': True, | ||||||
|  |             'validator': bool, | ||||||
|  |         }, | ||||||
|  |  | ||||||
|         'PART_IPN_REGEX': { |         'PART_IPN_REGEX': { | ||||||
|             'name': _('IPN Regex'), |             'name': _('IPN Regex'), | ||||||
|             'description': _('Regular expression pattern for matching Part IPN') |             'description': _('Regular expression pattern for matching Part IPN') | ||||||
|   | |||||||
| @@ -186,7 +186,7 @@ class GlobalSettingsApiTest(InvenTreeAPITestCase): | |||||||
|         # Check default value |         # Check default value | ||||||
|         self.assertEqual(setting.value, 'My company name') |         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 |         # Test getting via the API | ||||||
|         for val in ['test', '123', 'My company nam3']: |         for val in ['test', '123', 'My company nam3']: | ||||||
| @@ -212,6 +212,47 @@ class GlobalSettingsApiTest(InvenTreeAPITestCase): | |||||||
|             setting.refresh_from_db() |             setting.refresh_from_db() | ||||||
|             self.assertEqual(setting.value, val) |             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): | class UserSettingsApiTest(InvenTreeAPITestCase): | ||||||
|     """ |     """ | ||||||
| @@ -226,6 +267,34 @@ class UserSettingsApiTest(InvenTreeAPITestCase): | |||||||
|  |  | ||||||
|         self.get(url, expected_code=200) |         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): |     def test_user_setting_boolean(self): | ||||||
|         """ |         """ | ||||||
|         Test a boolean user setting value |         Test a boolean user setting value | ||||||
| @@ -241,7 +310,7 @@ class UserSettingsApiTest(InvenTreeAPITestCase): | |||||||
|         self.assertEqual(setting.to_native_value(), True) |         self.assertEqual(setting.to_native_value(), True) | ||||||
|  |  | ||||||
|         # Fetch via API |         # 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) |         response = self.get(url, expected_code=200) | ||||||
|  |  | ||||||
| @@ -300,7 +369,7 @@ class UserSettingsApiTest(InvenTreeAPITestCase): | |||||||
|             user=self.user |             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 |         # Check default value | ||||||
|         self.assertEqual(setting.value, 'YYYY-MM-DD') |         self.assertEqual(setting.value, 'YYYY-MM-DD') | ||||||
| @@ -339,7 +408,7 @@ class UserSettingsApiTest(InvenTreeAPITestCase): | |||||||
|             user=self.user |             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 |         # Check default value for this setting | ||||||
|         self.assertEqual(setting.value, 10) |         self.assertEqual(setting.value, 10) | ||||||
| @@ -396,12 +465,35 @@ class NotificationUserSettingsApiTest(InvenTreeAPITestCase): | |||||||
| class PluginSettingsApiTest(InvenTreeAPITestCase): | class PluginSettingsApiTest(InvenTreeAPITestCase): | ||||||
|     """Tests for the plugin settings API""" |     """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): |     def test_api_list(self): | ||||||
|         """Test list URL""" |         """Test list URL""" | ||||||
|         url = reverse('api-plugin-setting-list') |         url = reverse('api-plugin-setting-list') | ||||||
|  |  | ||||||
|         self.get(url, expected_code=200) |         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): | class WebhookMessageTests(TestCase): | ||||||
|     def setUp(self): |     def setUp(self): | ||||||
|   | |||||||
| @@ -71,10 +71,9 @@ class LabelPrintMixin: | |||||||
|  |  | ||||||
|         plugin_key = request.query_params.get('plugin', None) |         plugin_key = request.query_params.get('plugin', None) | ||||||
|  |  | ||||||
|         for slug, plugin in registry.plugins.items(): |         plugin = registry.get_plugin(plugin_key) | ||||||
|  |  | ||||||
|             if slug == plugin_key and plugin.mixin_enabled('labels'): |  | ||||||
|  |  | ||||||
|  |         if plugin: | ||||||
|             config = plugin.plugin_config() |             config = plugin.plugin_config() | ||||||
|  |  | ||||||
|             if config and config.active: |             if config and config.active: | ||||||
|   | |||||||
| @@ -91,11 +91,23 @@ class TemplateTagTest(TestCase): | |||||||
|  |  | ||||||
|     def test_global_settings(self): |     def test_global_settings(self): | ||||||
|         result = inventree_extras.global_settings() |         result = inventree_extras.global_settings() | ||||||
|         self.assertEqual(len(result), 61) |         self.assertEqual(len(result), len(InvenTreeSetting.SETTINGS)) | ||||||
|  |  | ||||||
|     def test_visible_global_settings(self): |     def test_visible_global_settings(self): | ||||||
|         result = inventree_extras.visible_global_settings() |         result = inventree_extras.visible_global_settings() | ||||||
|         self.assertEqual(len(result), 60) |  | ||||||
|  |         n = len(result) | ||||||
|  |  | ||||||
|  |         n_hidden = 0 | ||||||
|  |         n_visible = 0 | ||||||
|  |  | ||||||
|  |         for val in InvenTreeSetting.SETTINGS.values(): | ||||||
|  |             if val.get('hidden', False): | ||||||
|  |                 n_hidden += 1 | ||||||
|  |             else: | ||||||
|  |                 n_visible += 1 | ||||||
|  |  | ||||||
|  |         self.assertEqual(n, n_visible) | ||||||
|  |  | ||||||
|  |  | ||||||
| class PartTest(TestCase): | class PartTest(TestCase): | ||||||
|   | |||||||
| @@ -10,11 +10,15 @@ from django.urls import include, re_path | |||||||
| from rest_framework import generics | from rest_framework import generics | ||||||
| from rest_framework import status | from rest_framework import status | ||||||
| from rest_framework import permissions | from rest_framework import permissions | ||||||
|  | from rest_framework.exceptions import NotFound | ||||||
| from rest_framework.response import Response | from rest_framework.response import Response | ||||||
|  |  | ||||||
|  | from django_filters.rest_framework import DjangoFilterBackend | ||||||
|  |  | ||||||
| from common.api import GlobalSettingsPermissions | from common.api import GlobalSettingsPermissions | ||||||
| from plugin.models import PluginConfig, PluginSetting | from plugin.models import PluginConfig, PluginSetting | ||||||
| import plugin.serializers as PluginSerializers | import plugin.serializers as PluginSerializers | ||||||
|  | from plugin.registry import registry | ||||||
|  |  | ||||||
|  |  | ||||||
| class PluginList(generics.ListAPIView): | class PluginList(generics.ListAPIView): | ||||||
| @@ -98,6 +102,15 @@ class PluginSettingList(generics.ListAPIView): | |||||||
|         GlobalSettingsPermissions, |         GlobalSettingsPermissions, | ||||||
|     ] |     ] | ||||||
|  |  | ||||||
|  |     filter_backends = [ | ||||||
|  |         DjangoFilterBackend, | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     filter_fields = [ | ||||||
|  |         'plugin__active', | ||||||
|  |         'plugin__key', | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |  | ||||||
| class PluginSettingDetail(generics.RetrieveUpdateAPIView): | class PluginSettingDetail(generics.RetrieveUpdateAPIView): | ||||||
|     """ |     """ | ||||||
| @@ -109,6 +122,34 @@ class PluginSettingDetail(generics.RetrieveUpdateAPIView): | |||||||
|     queryset = PluginSetting.objects.all() |     queryset = PluginSetting.objects.all() | ||||||
|     serializer_class = PluginSerializers.PluginSettingSerializer |     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 |     # Staff permission required | ||||||
|     permission_classes = [ |     permission_classes = [ | ||||||
|         GlobalSettingsPermissions, |         GlobalSettingsPermissions, | ||||||
| @@ -119,7 +160,7 @@ plugin_api_urls = [ | |||||||
|  |  | ||||||
|     # Plugin settings URLs |     # Plugin settings URLs | ||||||
|     re_path(r'^settings/', include([ |     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'), |         re_path(r'^.*$', PluginSettingList.as_view(), name='api-plugin-setting-list'), | ||||||
|     ])), |     ])), | ||||||
|  |  | ||||||
|   | |||||||
| @@ -63,6 +63,17 @@ class PluginsRegistry: | |||||||
|         # mixins |         # mixins | ||||||
|         self.mixins_settings = {} |         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): |     def call_plugin_function(self, slug, func, *args, **kwargs): | ||||||
|         """ |         """ | ||||||
|         Call a member function (named by 'func') of the plugin named by 'slug'. |         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. |         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) |         plugin_func = getattr(plugin, func) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -138,7 +138,7 @@ class PluginSettingSerializer(GenericReferencedSettingSerializer): | |||||||
|         'plugin', |         'plugin', | ||||||
|     ] |     ] | ||||||
|  |  | ||||||
|     plugin = serializers.PrimaryKeyRelatedField(read_only=True) |     plugin = serializers.CharField(source='plugin.key', read_only=True) | ||||||
|  |  | ||||||
|  |  | ||||||
| class NotificationUserSettingSerializer(GenericReferencedSettingSerializer): | class NotificationUserSettingSerializer(GenericReferencedSettingSerializer): | ||||||
|   | |||||||
| @@ -13,6 +13,7 @@ | |||||||
| <table class='table table-striped table-condensed'> | <table class='table table-striped table-condensed'> | ||||||
|     <tbody> |     <tbody> | ||||||
|         {% include "InvenTree/settings/setting.html" with key="BARCODE_ENABLE" icon="fa-qrcode" %} |         {% include "InvenTree/settings/setting.html" with key="BARCODE_ENABLE" icon="fa-qrcode" %} | ||||||
|  |         {% include "InvenTree/settings/setting.html" with key="BARCODE_WEBCAM_SUPPORT" icon="fa-video" %} | ||||||
|     </tbody> |     </tbody> | ||||||
| </table> | </table> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -24,7 +24,7 @@ | |||||||
|     <td> |     <td> | ||||||
|         {% if setting.is_bool %} |         {% if setting.is_bool %} | ||||||
|         <div class='form-check form-switch'> |         <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> |         </div> | ||||||
|         {% else %} |         {% else %} | ||||||
|         <div id='setting-{{ setting.pk }}'> |         <div id='setting-{{ setting.pk }}'> | ||||||
| @@ -41,7 +41,7 @@ | |||||||
|             </span> |             </span> | ||||||
|             {{ setting.units }} |             {{ setting.units }} | ||||||
|             <div class='btn-group float-right'> |             <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> |                     <span class='fas fa-edit icon-green'></span> | ||||||
|                 </button> |                 </button> | ||||||
|             </div> |             </div> | ||||||
|   | |||||||
| @@ -66,8 +66,8 @@ | |||||||
| // Callback for when boolean settings are edited | // Callback for when boolean settings are edited | ||||||
| $('table').find('.boolean-setting').change(function() { | $('table').find('.boolean-setting').change(function() { | ||||||
|  |  | ||||||
|     var setting = $(this).attr('setting'); |  | ||||||
|     var pk = $(this).attr('pk'); |     var pk = $(this).attr('pk'); | ||||||
|  |     var setting = $(this).attr('setting'); | ||||||
|     var plugin = $(this).attr('plugin'); |     var plugin = $(this).attr('plugin'); | ||||||
|     var user = $(this).attr('user'); |     var user = $(this).attr('user'); | ||||||
|     var notification = $(this).attr('notification'); |     var notification = $(this).attr('notification'); | ||||||
| @@ -75,12 +75,12 @@ $('table').find('.boolean-setting').change(function() { | |||||||
|     var checked = this.checked; |     var checked = this.checked; | ||||||
|  |  | ||||||
|     // Global setting by default |     // Global setting by default | ||||||
|     var url = `/api/settings/global/${pk}/`; |     var url = `/api/settings/global/${setting}/`; | ||||||
|  |  | ||||||
|     if (plugin) { |     if (plugin) { | ||||||
|         url = `/api/plugin/settings/${pk}/`; |         url = `/api/plugin/settings/${plugin}/${setting}/`; | ||||||
|     } else if (user) { |     } else if (user) { | ||||||
|         url = `/api/settings/user/${pk}/`; |         url = `/api/settings/user/${setting}/`; | ||||||
|     } else if (notification) { |     } else if (notification) { | ||||||
|         url = `/api/settings/notification/${pk}/`; |         url = `/api/settings/notification/${pk}/`; | ||||||
|     } |     } | ||||||
| @@ -105,9 +105,9 @@ $('table').find('.boolean-setting').change(function() { | |||||||
| // Callback for when non-boolean settings are edited | // Callback for when non-boolean settings are edited | ||||||
| $('table').find('.btn-edit-setting').click(function() { | $('table').find('.btn-edit-setting').click(function() { | ||||||
|     var setting = $(this).attr('setting'); |     var setting = $(this).attr('setting'); | ||||||
|     var pk = $(this).attr('pk'); |  | ||||||
|     var plugin = $(this).attr('plugin'); |     var plugin = $(this).attr('plugin'); | ||||||
|     var is_global = true; |     var is_global = true; | ||||||
|  |     var notification = $(this).attr('notification'); | ||||||
|  |  | ||||||
|     if ($(this).attr('user')){ |     if ($(this).attr('user')){ | ||||||
|         is_global = false; |         is_global = false; | ||||||
| @@ -117,15 +117,19 @@ $('table').find('.btn-edit-setting').click(function() { | |||||||
|  |  | ||||||
|     if (plugin != null) { |     if (plugin != null) { | ||||||
|         title = '{% trans "Edit Plugin Setting" %}'; |         title = '{% trans "Edit Plugin Setting" %}'; | ||||||
|  |     } else if (notification) { | ||||||
|  |         title = '{% trans "Edit Notification Setting" %}'; | ||||||
|  |         setting = $(this).attr('pk'); | ||||||
|     } else if (is_global) { |     } else if (is_global) { | ||||||
|         title = '{% trans "Edit Global Setting" %}'; |         title = '{% trans "Edit Global Setting" %}'; | ||||||
|     } else { |     } else { | ||||||
|         title = '{% trans "Edit User Setting" %}'; |         title = '{% trans "Edit User Setting" %}'; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     editSetting(pk, { |     editSetting(setting, { | ||||||
|         plugin: plugin, |         plugin: plugin, | ||||||
|         global: is_global, |         global: is_global, | ||||||
|  |         notification: notification, | ||||||
|         title: title, |         title: title, | ||||||
|     }); |     }); | ||||||
| }); | }); | ||||||
|   | |||||||
| @@ -29,23 +29,28 @@ const plugins_enabled = false; | |||||||
| {% endif %} | {% 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? |     // Is this a global setting or a user setting? | ||||||
|     var global = options.global || false; |     var global = options.global || false; | ||||||
|  |  | ||||||
|     var plugin = options.plugin; |     var plugin = options.plugin; | ||||||
|  |  | ||||||
|  |     var notification = options.notification; | ||||||
|  |  | ||||||
|     var url = ''; |     var url = ''; | ||||||
|  |  | ||||||
|     if (plugin) { |     if (plugin) { | ||||||
|         url = `/api/plugin/settings/${pk}/`; |         url = `/api/plugin/settings/${plugin}/${key}/`; | ||||||
|  |     } else if (notification) { | ||||||
|  |         url = `/api/settings/notification/${pk}/`; | ||||||
|     } else if (global) { |     } else if (global) { | ||||||
|         url = `/api/settings/global/${pk}/`; |         url = `/api/settings/global/${key}/`; | ||||||
|     } else { |     } else { | ||||||
|         url = `/api/settings/user/${pk}/`; |         url = `/api/settings/user/${key}/`; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     var reload_required = false; |     var reload_required = false; | ||||||
|   | |||||||
| @@ -70,7 +70,7 @@ function onBarcodeScanClicked(e) { | |||||||
| } | } | ||||||
|  |  | ||||||
| function onCameraAvailable(hasCamera, options) { | function onCameraAvailable(hasCamera, options) { | ||||||
|     if ( hasCamera == true ) { |     if (hasCamera && global_settings.BARCODE_WEBCAM_SUPPORT) { | ||||||
|         // Camera is only acccessible if page is served over secure connection |         // Camera is only acccessible if page is served over secure connection | ||||||
|         if ( window.isSecureContext == true ) { |         if ( window.isSecureContext == true ) { | ||||||
|             qrScanner = new QrScanner(document.getElementById('barcode_scan_video'), (result) => { |             qrScanner = new QrScanner(document.getElementById('barcode_scan_video'), (result) => { | ||||||
|   | |||||||
| @@ -16,8 +16,8 @@ | |||||||
| [](https://hub.docker.com/r/inventree/inventree) | [](https://hub.docker.com/r/inventree/inventree) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | [](https://twitter.com/inventreedb) | ||||||
|  | [](https://www.reddit.com/r/InvenTree/) | ||||||
|  |  | ||||||
|  |  | ||||||
| <h4> | <h4> | ||||||
| @@ -169,4 +169,4 @@ Find a full list of used third-party libraries in [our documentation](https://in | |||||||
| <!-- License --> | <!-- License --> | ||||||
| ## :warning: License | ## :warning: License | ||||||
|  |  | ||||||
| Distributed under the [MIT](https://choosealicense.com/licenses/mit/) License. See LICENSE.txt for more information. | Distributed under the [MIT](https://choosealicense.com/licenses/mit/) License. See [LICENSE.txt](https://github.com/inventree/InvenTree/blob/master/LICENSE) for more information. | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user