From cb583eb466276032ab31dfea3cd01533fdc73dad Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 9 Nov 2021 15:59:09 +1100 Subject: [PATCH 01/19] Add LIST endpoint for global settngs and user settings - Read only, cannot create new settings - User settings filters against the user making the request --- InvenTree/InvenTree/urls.py | 2 +- InvenTree/common/api.py | 80 ++++++++++++++++++++++ InvenTree/common/models.py | 2 +- InvenTree/common/serializers.py | 55 +++++++++++++++ InvenTree/templates/js/dynamic/settings.js | 1 + 5 files changed, 138 insertions(+), 2 deletions(-) diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index 053ba05264..ee2155cb0d 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -53,7 +53,7 @@ admin.site.site_header = "InvenTree Admin" apipatterns = [ url(r'^barcode/', include(barcode_api_urls)), - url(r'^common/', include(common_api_urls)), + url(r'^settings/', include(common_api_urls)), url(r'^part/', include(part_api_urls)), url(r'^bom/', include(bom_api_urls)), url(r'^company/', include(company_api_urls)), diff --git a/InvenTree/common/api.py b/InvenTree/common/api.py index 8a2dfbd6a7..e995510416 100644 --- a/InvenTree/common/api.py +++ b/InvenTree/common/api.py @@ -5,5 +5,85 @@ Provides a JSON API for common components. # -*- coding: utf-8 -*- from __future__ import unicode_literals +from django.conf.urls import url, include + +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework import filters, generics + +import common.models +import common.serializers + + +class SettingsList(generics.ListAPIView): + + filter_backends = [ + DjangoFilterBackend, + filters.SearchFilter, + filters.OrderingFilter, + ] + + ordering_fields = [ + 'pk', + 'key', + 'name', + ] + + search_fields = [ + 'key', + ] + + +class GlobalSettingsList(SettingsList): + """ + API endpoint for accessing a list of global settings objects + """ + + queryset = common.models.InvenTreeSetting.objects.all() + serializer_class = common.serializers.GlobalSettingsSerializer + + + +class UserSettingsList(SettingsList): + """ + API endpoint for accessing a list of user settings objects + """ + + queryset = common.models.InvenTreeUserSetting.objects.all() + serializer_class = common.serializers.UserSettingsSerializer + + def filter_queryset(self, queryset): + """ + Only list settings which apply to the current user + """ + + try: + user = self.request.user + except AttributeError: + return common.models.InvenTreeUserSetting.objects.none() + + queryset = super().filter_queryset(queryset) + + queryset = queryset.filter(user=user) + + return queryset + + common_api_urls = [ + + # User settings + url(r'^user/', include([ + # User Settings Detail + + # User Settings List + url(r'^.*$', UserSettingsList.as_view(), name='api-user-setting-list'), + ])), + + # Global settings + url(r'^global/', include([ + # Global Settings Detail + + # Global Settings List + url(r'^.*$', GlobalSettingsList.as_view(), name='api-global-setting-list'), + ])) + ] diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 53924f11fa..cb1e9217f2 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -850,7 +850,7 @@ class InvenTreeSetting(BaseInvenTreeSetting): }, 'SIGNUP_GROUP': { 'name': _('Group on signup'), - 'description': _('Group new user are asigned on registration'), + 'description': _('Group to which new users are assigned on registration'), 'default': '', 'choices': settings_group_options }, diff --git a/InvenTree/common/serializers.py b/InvenTree/common/serializers.py index 99ac03cdfd..87db97cc92 100644 --- a/InvenTree/common/serializers.py +++ b/InvenTree/common/serializers.py @@ -1,3 +1,58 @@ """ JSON serializers for common components """ + +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from InvenTree.serializers import InvenTreeModelSerializer + +from rest_framework import serializers + +from common.models import InvenTreeSetting, InvenTreeUserSetting + +class GlobalSettingsSerializer(InvenTreeModelSerializer): + """ + Serializer for the InvenTreeSetting model + """ + + name = serializers.CharField(read_only=True) + + description = serializers.CharField(read_only=True) + + # choices = serializers.CharField(read_only=True, many=True) + + class Meta: + model = InvenTreeSetting + fields = [ + 'pk', + 'key', + 'value', + 'name', + 'description', + # 'type', + ] + + +class UserSettingsSerializer(InvenTreeModelSerializer): + """ + Serializer for the InvenTreeUserSetting model + """ + + name = serializers.CharField(read_only=True) + + description = serializers.CharField(read_only=True) + + # choices = serializers.CharField(read_only=True, many=True) + + class Meta: + model = InvenTreeUserSetting + fields = [ + 'pk', + 'key', + 'value', + 'name', + 'description', + 'user', + # 'type', + ] diff --git a/InvenTree/templates/js/dynamic/settings.js b/InvenTree/templates/js/dynamic/settings.js index cb21e1fefc..b147d6993a 100644 --- a/InvenTree/templates/js/dynamic/settings.js +++ b/InvenTree/templates/js/dynamic/settings.js @@ -18,3 +18,4 @@ const global_settings = { {{ key }}: {% primitive_to_javascript value %}, {% endfor %} }; + From f3b4f7aa28ab6cc701140ea865e405346c5c432c Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 9 Nov 2021 16:07:25 +1100 Subject: [PATCH 02/19] Add "detail" view for global settings objects - Can view and edit (but not delete) - User must have "staff" status to access --- InvenTree/common/api.py | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/InvenTree/common/api.py b/InvenTree/common/api.py index e995510416..a223b0347c 100644 --- a/InvenTree/common/api.py +++ b/InvenTree/common/api.py @@ -8,7 +8,7 @@ from __future__ import unicode_literals from django.conf.urls import url, include from django_filters.rest_framework import DjangoFilterBackend -from rest_framework import filters, generics +from rest_framework import filters, generics, permissions import common.models import common.serializers @@ -42,6 +42,40 @@ class GlobalSettingsList(SettingsList): serializer_class = common.serializers.GlobalSettingsSerializer +class GlobalSettingsPermissions(permissions.BasePermission): + """ + Special permission class to determine if the user is "staff" + """ + + def has_permission(self, request, view): + """ + Check that the requesting user is 'admin' + """ + + print("User:", request.user, request.user.is_staff) + + try: + user = request.user + + return user.is_staff + except AttributeError: + return False + + +class GlobalSettingsDetail(generics.RetrieveUpdateAPIView): + """ + Detail view for an individual "global setting" object. + + - User must have 'staff' status to view / edit + """ + + queryset = common.models.InvenTreeSetting.objects.all() + serializer_class = common.serializers.GlobalSettingsSerializer + + permission_classes = [ + GlobalSettingsPermissions, + ] + class UserSettingsList(SettingsList): """ @@ -81,6 +115,7 @@ common_api_urls = [ # Global settings url(r'^global/', include([ # Global Settings Detail + url(r'^(?P\d+)/', GlobalSettingsDetail.as_view(), name='api-global-setting-detail'), # Global Settings List url(r'^.*$', GlobalSettingsList.as_view(), name='api-global-setting-list'), From 0374c27d7c7d0c34dc78a90d582e18736de2b599 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 9 Nov 2021 16:14:10 +1100 Subject: [PATCH 03/19] Adds "detail" view for user setting - Users can only view / edit their own settings --- InvenTree/InvenTree/version.py | 6 +++++- InvenTree/common/api.py | 36 ++++++++++++++++++++++++++++++++-- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/InvenTree/InvenTree/version.py b/InvenTree/InvenTree/version.py index 48539713f2..935a0bed37 100644 --- a/InvenTree/InvenTree/version.py +++ b/InvenTree/InvenTree/version.py @@ -12,11 +12,15 @@ import common.models INVENTREE_SW_VERSION = "0.6.0 dev" # InvenTree API version -INVENTREE_API_VERSION = 16 +INVENTREE_API_VERSION = 17 """ Increment this API version number whenever there is a significant change to the API that any clients need to know about +v17 -> 2021-11-09 + - Adds API endpoints for GLOBAL and USER settings objects + - Ref: https://github.com/inventree/InvenTree/pull/2275 + v16 -> 2021-10-17 - Adds API endpoint for completing build order outputs diff --git a/InvenTree/common/api.py b/InvenTree/common/api.py index a223b0347c..a89b49bce0 100644 --- a/InvenTree/common/api.py +++ b/InvenTree/common/api.py @@ -52,8 +52,6 @@ class GlobalSettingsPermissions(permissions.BasePermission): Check that the requesting user is 'admin' """ - print("User:", request.user, request.user.is_staff) - try: user = request.user @@ -102,11 +100,45 @@ class UserSettingsList(SettingsList): return queryset +class UserSettingsPermissions(permissions.BasePermission): + """ + Special permission class to determine if the user can view / edit a particular setting + """ + + def has_object_permission(self, request, view, obj): + + print("Checking object permissions:") + print(request.user, obj.user) + + try: + user = request.user + except AttributeError: + return False + + return user == obj.user + + +class UserSettingsDetail(generics.RetrieveUpdateAPIView): + """ + Detail view for an individual "user setting" object + + - User can only view / edit settings their own settings objects + """ + + queryset = common.models.InvenTreeUserSetting.objects.all() + serializer_class = common.serializers.UserSettingsSerializer + + permission_classes = [ + UserSettingsPermissions, + ] + + common_api_urls = [ # User settings url(r'^user/', include([ # User Settings Detail + url(r'^(?P\d+)/', UserSettingsDetail.as_view(), name='api-user-setting-detail'), # User Settings List url(r'^.*$', UserSettingsList.as_view(), name='api-user-setting-list'), From 07851f0b2c121ef59fa98b67d2fd7b01d11430d9 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 9 Nov 2021 17:07:41 +1100 Subject: [PATCH 04/19] Include 'setting type' in API data --- InvenTree/common/api.py | 3 --- InvenTree/common/models.py | 24 ++++++++++++++++++++++++ InvenTree/common/serializers.py | 14 ++++++++++---- 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/InvenTree/common/api.py b/InvenTree/common/api.py index a89b49bce0..fe28cdbe57 100644 --- a/InvenTree/common/api.py +++ b/InvenTree/common/api.py @@ -107,9 +107,6 @@ class UserSettingsPermissions(permissions.BasePermission): def has_object_permission(self, request, view, obj): - print("Checking object permissions:") - print(request.user, obj.user) - try: user = request.user except AttributeError: diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index cb1e9217f2..43de6767bd 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -45,6 +45,16 @@ class BaseInvenTreeSetting(models.Model): class Meta: abstract = True + def save(self, *args, **kwargs): + """ + Enforce validation and clean before saving + """ + + self.clean() + self.validate_unique() + + super().save() + @classmethod def allValues(cls, user=None): """ @@ -427,6 +437,20 @@ class BaseInvenTreeSetting(models.Model): return InvenTree.helpers.str2bool(self.value) + def setting_type(self): + """ + Return the field type identifier for this setting object + """ + + if self.is_bool(): + return 'boolean' + + elif self.is_int(): + return 'integer' + + else: + return 'string' + @classmethod def validator_is_bool(cls, validator): diff --git a/InvenTree/common/serializers.py b/InvenTree/common/serializers.py index 87db97cc92..85908872f8 100644 --- a/InvenTree/common/serializers.py +++ b/InvenTree/common/serializers.py @@ -16,11 +16,13 @@ class GlobalSettingsSerializer(InvenTreeModelSerializer): Serializer for the InvenTreeSetting model """ + key = serializers.CharField(read_only=True) + name = serializers.CharField(read_only=True) description = serializers.CharField(read_only=True) - # choices = serializers.CharField(read_only=True, many=True) + type = serializers.CharField(source='setting_type', read_only=True) class Meta: model = InvenTreeSetting @@ -30,7 +32,7 @@ class GlobalSettingsSerializer(InvenTreeModelSerializer): 'value', 'name', 'description', - # 'type', + 'type', ] @@ -39,11 +41,15 @@ class UserSettingsSerializer(InvenTreeModelSerializer): Serializer for the InvenTreeUserSetting model """ + key = serializers.CharField(read_only=True) + name = serializers.CharField(read_only=True) description = serializers.CharField(read_only=True) - # choices = serializers.CharField(read_only=True, many=True) + user = serializers.PrimaryKeyRelatedField(read_only=True) + + type = serializers.CharField(source='setting_type', read_only=True) class Meta: model = InvenTreeUserSetting @@ -54,5 +60,5 @@ class UserSettingsSerializer(InvenTreeModelSerializer): 'name', 'description', 'user', - # 'type', + 'type', ] From 4433befbdc0c51cea2f35610d3f5f267c0f77ad6 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 9 Nov 2021 17:19:12 +1100 Subject: [PATCH 05/19] Add list of 'choices' (read only) to serializer - Check that the specified value is one of the valid options (if provided) --- InvenTree/common/models.py | 18 ++++++++++++++++++ InvenTree/common/serializers.py | 20 ++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 43de6767bd..a941499f7e 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -353,6 +353,11 @@ class BaseInvenTreeSetting(models.Model): except (ValueError): raise ValidationError(_('Must be an integer value')) + options = self.valid_options() + + if options and self.value not in options: + raise ValidationError(_("Chosen value is not a valid option")) + if validator is not None: self.run_validator(validator) @@ -419,6 +424,19 @@ class BaseInvenTreeSetting(models.Model): return self.__class__.get_setting_choices(self.key) + def valid_options(self): + """ + Return a list of valid options for this setting + """ + + choices = self.choices() + + if not choices: + return None + + return [opt[0] for opt in choices] + + def is_bool(self): """ Check if this setting is required to be a boolean value diff --git a/InvenTree/common/serializers.py b/InvenTree/common/serializers.py index 85908872f8..ed120917cf 100644 --- a/InvenTree/common/serializers.py +++ b/InvenTree/common/serializers.py @@ -24,6 +24,15 @@ class GlobalSettingsSerializer(InvenTreeModelSerializer): type = serializers.CharField(source='setting_type', read_only=True) + choices = serializers.SerializerMethodField() + + def get_choices(self, obj: InvenTreeUserSetting): + """ + Returns the choices available for a given item + """ + + return obj.choices() + class Meta: model = InvenTreeSetting fields = [ @@ -33,6 +42,7 @@ class GlobalSettingsSerializer(InvenTreeModelSerializer): 'name', 'description', 'type', + 'choices', ] @@ -51,6 +61,15 @@ class UserSettingsSerializer(InvenTreeModelSerializer): type = serializers.CharField(source='setting_type', read_only=True) + choices = serializers.SerializerMethodField() + + def get_choices(self, obj: InvenTreeUserSetting): + """ + Returns the choices available for a given item + """ + + return obj.choices() + class Meta: model = InvenTreeUserSetting fields = [ @@ -61,4 +80,5 @@ class UserSettingsSerializer(InvenTreeModelSerializer): 'description', 'user', 'type', + 'choices', ] From 324335a62021cee9c2a4b26f41fec17eddf67f09 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 9 Nov 2021 17:43:36 +1100 Subject: [PATCH 06/19] Convert to "native value" in the serializer --- InvenTree/common/models.py | 16 ++++++++++++++ InvenTree/common/serializers.py | 38 +++++++++++++++------------------ 2 files changed, 33 insertions(+), 21 deletions(-) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index a941499f7e..c3add3b95a 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -909,6 +909,14 @@ class InvenTreeSetting(BaseInvenTreeSetting): help_text=_('Settings key (must be unique - case insensitive'), ) + def to_native_value(self): + """ + Return the "pythonic" value, + e.g. convert "True" to True, and "1" to 1 + """ + + return self.__class__.get_setting(self.key) + class InvenTreeUserSetting(BaseInvenTreeSetting): """ @@ -1119,6 +1127,14 @@ class InvenTreeUserSetting(BaseInvenTreeSetting): 'user__id': kwargs['user'].id } + def to_native_value(self): + """ + Return the "pythonic" value, + e.g. convert "True" to True, and "1" to 1 + """ + + return self.__class__.get_setting(self.key, user=self.user) + class PriceBreak(models.Model): """ diff --git a/InvenTree/common/serializers.py b/InvenTree/common/serializers.py index ed120917cf..8121681bdf 100644 --- a/InvenTree/common/serializers.py +++ b/InvenTree/common/serializers.py @@ -11,9 +11,10 @@ from rest_framework import serializers from common.models import InvenTreeSetting, InvenTreeUserSetting -class GlobalSettingsSerializer(InvenTreeModelSerializer): + +class SettingsSerializer(InvenTreeModelSerializer): """ - Serializer for the InvenTreeSetting model + Base serializer for a settings object """ key = serializers.CharField(read_only=True) @@ -26,13 +27,25 @@ class GlobalSettingsSerializer(InvenTreeModelSerializer): choices = serializers.SerializerMethodField() - def get_choices(self, obj: InvenTreeUserSetting): + def get_choices(self, obj): """ Returns the choices available for a given item """ return obj.choices() + value = serializers.SerializerMethodField() + + def get_value(self, obj): + + return obj.to_native_value() + + +class GlobalSettingsSerializer(SettingsSerializer): + """ + Serializer for the InvenTreeSetting model + """ + class Meta: model = InvenTreeSetting fields = [ @@ -46,30 +59,13 @@ class GlobalSettingsSerializer(InvenTreeModelSerializer): ] -class UserSettingsSerializer(InvenTreeModelSerializer): +class UserSettingsSerializer(SettingsSerializer): """ Serializer for the InvenTreeUserSetting model """ - key = serializers.CharField(read_only=True) - - name = serializers.CharField(read_only=True) - - description = serializers.CharField(read_only=True) - user = serializers.PrimaryKeyRelatedField(read_only=True) - type = serializers.CharField(source='setting_type', read_only=True) - - choices = serializers.SerializerMethodField() - - def get_choices(self, obj: InvenTreeUserSetting): - """ - Returns the choices available for a given item - """ - - return obj.choices() - class Meta: model = InvenTreeUserSetting fields = [ From 5df4374607a002815625c85402ccc1977fd53f3f Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 9 Nov 2021 21:16:37 +1100 Subject: [PATCH 07/19] javascript for editing settings via API --- InvenTree/common/serializers.py | 6 -- InvenTree/templates/js/dynamic/settings.js | 65 ++++++++++++++++++++++ InvenTree/templates/js/translated/forms.js | 21 ++++--- 3 files changed, 78 insertions(+), 14 deletions(-) diff --git a/InvenTree/common/serializers.py b/InvenTree/common/serializers.py index 8121681bdf..e5c6c18b76 100644 --- a/InvenTree/common/serializers.py +++ b/InvenTree/common/serializers.py @@ -34,12 +34,6 @@ class SettingsSerializer(InvenTreeModelSerializer): return obj.choices() - value = serializers.SerializerMethodField() - - def get_value(self, obj): - - return obj.to_native_value() - class GlobalSettingsSerializer(SettingsSerializer): """ diff --git a/InvenTree/templates/js/dynamic/settings.js b/InvenTree/templates/js/dynamic/settings.js index b147d6993a..136812779b 100644 --- a/InvenTree/templates/js/dynamic/settings.js +++ b/InvenTree/templates/js/dynamic/settings.js @@ -19,3 +19,68 @@ const global_settings = { {% endfor %} }; +/* + * Edit a setting value + */ +function editSetting(pk, options={}) { + + // Is this a global setting or a user setting? + var global = options.global || false; + + var url = ''; + + if (global) { + url = `/api/settings/global/${pk}/`; + } else { + url = `/api/settings/user/${pk}/`; + } + + // First, read the settings object from the server + inventreeGet(url, {}, { + success: function(response) { + + // Construct the field + var fields = { + value: { + label: response.name, + help_text: response.description, + type: response.type, + choices: response.choices, + } + }; + + constructChangeForm(fields, { + url: url, + method: 'PATCH', + title: "edit setting", + processResults: function(data, fields, opts) { + + switch (data.type) { + case 'boolean': + // Convert to boolean value + data.value = data.value.toString().toLowerCase() == 'true'; + break; + case 'integer': + // Convert to integer value + data.value = parseInt(data.value.toString()); + break; + default: + break; + } + + return data; + }, + processBeforeUpload: function(data) { + // Convert value to string + data.value = data.value.toString(); + + return data; + } + }); + }, + error: function(xhr) { + showApiError(xhr, url); + } + }); + +} \ No newline at end of file diff --git a/InvenTree/templates/js/translated/forms.js b/InvenTree/templates/js/translated/forms.js index 18ba08d512..2f25fef259 100644 --- a/InvenTree/templates/js/translated/forms.js +++ b/InvenTree/templates/js/translated/forms.js @@ -198,14 +198,6 @@ function constructChangeForm(fields, options) { json: 'application/json', }, success: function(data) { - - // Push existing 'value' to each field - for (const field in data) { - - if (field in fields) { - fields[field].value = data[field]; - } - } // An optional function can be provided to process the returned results, // before they are rendered to the form @@ -218,6 +210,14 @@ function constructChangeForm(fields, options) { } } + // Push existing 'value' to each field + for (const field in data) { + + if (field in fields) { + fields[field].value = data[field]; + } + } + // Store the entire data object options.instance = data; @@ -724,6 +724,11 @@ function submitFormData(fields, options) { data = form_data; } + // Optionally pre-process the data before uploading to the server + if (options.processBeforeUpload) { + data = options.processBeforeUpload(data); + } + // Submit data upload_func( options.url, From eb5b810be07bd852c6e394d62323b425f2f81d27 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 9 Nov 2021 21:29:31 +1100 Subject: [PATCH 08/19] Update displayed setting when API returns success --- .../InvenTree/settings/settings.html | 23 ++++++------------- InvenTree/templates/js/dynamic/settings.js | 13 ++++++++++- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/InvenTree/templates/InvenTree/settings/settings.html b/InvenTree/templates/InvenTree/settings/settings.html index 7599733975..6c37722316 100644 --- a/InvenTree/templates/InvenTree/settings/settings.html +++ b/InvenTree/templates/InvenTree/settings/settings.html @@ -50,26 +50,17 @@ $('table').find('.btn-edit-setting').click(function() { var setting = $(this).attr('setting'); var pk = $(this).attr('pk'); - var url = `/settings/${pk}/edit/`; + + var is_global = true; if ($(this).attr('user')){ - url += `user/`; + is_global = false; } - launchModalForm( - url, - { - success: function(response) { - - if (response.is_bool) { - var enabled = response.value.toLowerCase() == 'true'; - $(`#setting-value-${setting}`).prop('checked', enabled); - } else { - $(`#setting-value-${setting}`).html(response.value); - } - } - } - ); + editSetting(pk, { + global: is_global, + title: is_global ? '{% trans "Edit Global Setting" %}' : '{% trans "Edit User Setting" %}', + }); }); $("#edit-user").on('click', function() { diff --git a/InvenTree/templates/js/dynamic/settings.js b/InvenTree/templates/js/dynamic/settings.js index 136812779b..3c11ea3fd7 100644 --- a/InvenTree/templates/js/dynamic/settings.js +++ b/InvenTree/templates/js/dynamic/settings.js @@ -52,7 +52,7 @@ function editSetting(pk, options={}) { constructChangeForm(fields, { url: url, method: 'PATCH', - title: "edit setting", + title: options.title, processResults: function(data, fields, opts) { switch (data.type) { @@ -75,6 +75,17 @@ function editSetting(pk, options={}) { data.value = data.value.toString(); return data; + }, + onSuccess: function(response) { + + var setting = response.key; + + if (response.type == "boolean") { + var enabled = response.value.toString().toLowerCase() == 'true'; + $(`#setting-value-${setting}`).prop('checked', enabled); + } else { + $(`#setting-value-${setting}`).html(response.value); + } } }); }, From 476d2545ad94700edb66ec94ae1f6a4ae8139f45 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 9 Nov 2021 21:33:32 +1100 Subject: [PATCH 09/19] Remove old server-side view --- InvenTree/InvenTree/urls.py | 5 - .../common/templates/common/edit_setting.html | 14 -- InvenTree/common/views.py | 121 +----------------- 3 files changed, 1 insertion(+), 139 deletions(-) delete mode 100644 InvenTree/common/templates/common/edit_setting.html diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index ee2155cb0d..648600265c 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -42,8 +42,6 @@ from .views import CurrencyRefreshView from .views import AppearanceSelectView, SettingCategorySelectView from .views import DynamicJsView -from common.views import SettingEdit, UserSettingEdit - from .api import InfoView, NotFoundView from .api import ActionPluginView @@ -85,9 +83,6 @@ settings_urls = [ url(r'^category/', SettingCategorySelectView.as_view(), name='settings-category'), - url(r'^(?P\d+)/edit/user', UserSettingEdit.as_view(), name='user-setting-edit'), - url(r'^(?P\d+)/edit/', SettingEdit.as_view(), name='setting-edit'), - # Catch any other urls url(r'^.*$', SettingsView.as_view(template_name='InvenTree/settings/settings.html'), name='settings'), ] diff --git a/InvenTree/common/templates/common/edit_setting.html b/InvenTree/common/templates/common/edit_setting.html deleted file mode 100644 index c479e268a5..0000000000 --- a/InvenTree/common/templates/common/edit_setting.html +++ /dev/null @@ -1,14 +0,0 @@ -{% extends "modal_form.html" %} -{% load i18n %} - -{% block pre_form_content %} - -{{ block.super }} - -{% endblock %} \ No newline at end of file diff --git a/InvenTree/common/views.py b/InvenTree/common/views.py index 6cac8bbb19..c97047f9a8 100644 --- a/InvenTree/common/views.py +++ b/InvenTree/common/views.py @@ -8,138 +8,19 @@ from __future__ import unicode_literals import os from django.utils.translation import ugettext_lazy as _ -from django.forms import CheckboxInput, Select from django.conf import settings from django.core.files.storage import FileSystemStorage from formtools.wizard.views import SessionWizardView from crispy_forms.helper import FormHelper -from InvenTree.views import AjaxUpdateView, AjaxView +from InvenTree.views import AjaxView from InvenTree.helpers import str2bool -from . import models from . import forms from .files import FileManager -class SettingEdit(AjaxUpdateView): - """ - View for editing an InvenTree key:value settings object, - (or creating it if the key does not already exist) - """ - - model = models.InvenTreeSetting - ajax_form_title = _('Change Setting') - form_class = forms.SettingEditForm - ajax_template_name = "common/edit_setting.html" - - def get_context_data(self, **kwargs): - """ - Add extra context information about the particular setting object. - """ - - ctx = super().get_context_data(**kwargs) - - setting = self.get_object() - - ctx['key'] = setting.key - ctx['value'] = setting.value - ctx['name'] = self.model.get_setting_name(setting.key) - ctx['description'] = self.model.get_setting_description(setting.key) - - return ctx - - def get_data(self): - """ - Custom data to return to the client after POST success - """ - - data = {} - - setting = self.get_object() - - data['pk'] = setting.pk - data['key'] = setting.key - data['value'] = setting.value - data['is_bool'] = setting.is_bool() - data['is_int'] = setting.is_int() - - return data - - def get_form(self): - """ - Override default get_form behaviour - """ - - form = super().get_form() - - setting = self.get_object() - - choices = setting.choices() - - if choices is not None: - form.fields['value'].widget = Select(choices=choices) - elif setting.is_bool(): - form.fields['value'].widget = CheckboxInput() - - self.object.value = str2bool(setting.value) - form.fields['value'].value = str2bool(setting.value) - - name = self.model.get_setting_name(setting.key) - - if name: - form.fields['value'].label = name - - description = self.model.get_setting_description(setting.key) - - if description: - form.fields['value'].help_text = description - - return form - - def validate(self, setting, form): - """ - Perform custom validation checks on the form data. - """ - - data = form.cleaned_data - - value = data.get('value', None) - - if setting.choices(): - """ - If a set of choices are provided for a given setting, - the provided value must be one of those choices. - """ - - choices = [choice[0] for choice in setting.choices()] - - if value not in choices: - form.add_error('value', _('Supplied value is not allowed')) - - if setting.is_bool(): - """ - If a setting is defined as a boolean setting, - the provided value must look somewhat like a boolean value! - """ - - if not str2bool(value, test=True) and not str2bool(value, test=False): - form.add_error('value', _('Supplied value must be a boolean')) - - -class UserSettingEdit(SettingEdit): - """ - View for editing an InvenTree key:value user settings object, - (or creating it if the key does not already exist) - """ - - model = models.InvenTreeUserSetting - ajax_form_title = _('Change User Setting') - form_class = forms.SettingEditForm - ajax_template_name = "common/edit_setting.html" - - class MultiStepFormView(SessionWizardView): """ Setup basic methods of multi-step form From 7dce9f3f3bb7861f148625629df3e9d769953152 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 9 Nov 2021 21:37:54 +1100 Subject: [PATCH 10/19] PEP fixes --- InvenTree/common/api.py | 2 +- InvenTree/common/models.py | 1 - InvenTree/common/views.py | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/InvenTree/common/api.py b/InvenTree/common/api.py index fe28cdbe57..6dd51bdff1 100644 --- a/InvenTree/common/api.py +++ b/InvenTree/common/api.py @@ -85,7 +85,7 @@ class UserSettingsList(SettingsList): def filter_queryset(self, queryset): """ - Only list settings which apply to the current user + Only list settings which apply to the current user """ try: diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index c3add3b95a..01d38920d0 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -436,7 +436,6 @@ class BaseInvenTreeSetting(models.Model): return [opt[0] for opt in choices] - def is_bool(self): """ Check if this setting is required to be a boolean value diff --git a/InvenTree/common/views.py b/InvenTree/common/views.py index c97047f9a8..4b8310ddd2 100644 --- a/InvenTree/common/views.py +++ b/InvenTree/common/views.py @@ -15,7 +15,6 @@ from formtools.wizard.views import SessionWizardView from crispy_forms.helper import FormHelper from InvenTree.views import AjaxView -from InvenTree.helpers import str2bool from . import forms from .files import FileManager From 6347345c234ff27b214707f58892b4cbb69f32cd Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 9 Nov 2021 21:40:35 +1100 Subject: [PATCH 11/19] Ignore template files in common --- .github/workflows/html.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/html.yaml b/.github/workflows/html.yaml index 069da7cbb4..d0084ae032 100644 --- a/.github/workflows/html.yaml +++ b/.github/workflows/html.yaml @@ -43,7 +43,6 @@ jobs: run: | npm install markuplint npx markuplint InvenTree/build/templates/build/*.html - npx markuplint InvenTree/common/templates/common/*.html npx markuplint InvenTree/company/templates/company/*.html npx markuplint InvenTree/order/templates/order/*.html npx markuplint InvenTree/part/templates/part/*.html From 11dd187df2af586d0da5fb9ade01d175ff667aa8 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 9 Nov 2021 21:43:47 +1100 Subject: [PATCH 12/19] javascript linting --- InvenTree/templates/js/dynamic/settings.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/InvenTree/templates/js/dynamic/settings.js b/InvenTree/templates/js/dynamic/settings.js index 3c11ea3fd7..ebc4807715 100644 --- a/InvenTree/templates/js/dynamic/settings.js +++ b/InvenTree/templates/js/dynamic/settings.js @@ -1,6 +1,7 @@ {% load inventree_extras %} /* exported + editSettings, user_settings, global_settings, */ @@ -80,7 +81,7 @@ function editSetting(pk, options={}) { var setting = response.key; - if (response.type == "boolean") { + if (response.type == 'boolean') { var enabled = response.value.toString().toLowerCase() == 'true'; $(`#setting-value-${setting}`).prop('checked', enabled); } else { @@ -93,5 +94,4 @@ function editSetting(pk, options={}) { showApiError(xhr, url); } }); - -} \ No newline at end of file +} From 1e80e33634abe4ff62d49d0ed51d440058ef0608 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 9 Nov 2021 21:49:14 +1100 Subject: [PATCH 13/19] typo fix --- InvenTree/templates/js/dynamic/settings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/templates/js/dynamic/settings.js b/InvenTree/templates/js/dynamic/settings.js index ebc4807715..ec74abb131 100644 --- a/InvenTree/templates/js/dynamic/settings.js +++ b/InvenTree/templates/js/dynamic/settings.js @@ -1,7 +1,7 @@ {% load inventree_extras %} /* exported - editSettings, + editSetting, user_settings, global_settings, */ From 721d10a9ec42e6cf19f0f9c6ff7b1fcb113514f3 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 9 Nov 2021 22:14:26 +1100 Subject: [PATCH 14/19] Allow empty URLs --- InvenTree/common/models.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 01d38920d0..d0093eb10e 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -34,6 +34,19 @@ import logging logger = logging.getLogger('inventree') +class EmptyURLValidator(URLValidator): + + def __call__(self, value): + + value = str(value).strip() + + if len(value) == 0: + pass + + else: + super().__call__(value) + + class BaseInvenTreeSetting(models.Model): """ An base InvenTreeSetting object is a key:value pair used for storing @@ -572,7 +585,7 @@ class InvenTreeSetting(BaseInvenTreeSetting): 'INVENTREE_BASE_URL': { 'name': _('Base URL'), 'description': _('Base URL for server instance'), - 'validator': URLValidator(), + 'validator': EmptyURLValidator(), 'default': '', }, From 8d7b73e2a8300e795f6578c6d3730cf771d6276e Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 9 Nov 2021 22:25:42 +1100 Subject: [PATCH 15/19] Adds support for "multiple choice" field --- InvenTree/common/serializers.py | 10 +++++++++- InvenTree/templates/js/dynamic/settings.js | 6 +++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/InvenTree/common/serializers.py b/InvenTree/common/serializers.py index e5c6c18b76..35a1c79879 100644 --- a/InvenTree/common/serializers.py +++ b/InvenTree/common/serializers.py @@ -32,7 +32,15 @@ class SettingsSerializer(InvenTreeModelSerializer): Returns the choices available for a given item """ - return obj.choices() + results = [] + + for choice in obj.choices(): + results.append({ + 'value': choice[0], + 'display_name': choice[1], + }) + + return results class GlobalSettingsSerializer(SettingsSerializer): diff --git a/InvenTree/templates/js/dynamic/settings.js b/InvenTree/templates/js/dynamic/settings.js index ec74abb131..8201dc8374 100644 --- a/InvenTree/templates/js/dynamic/settings.js +++ b/InvenTree/templates/js/dynamic/settings.js @@ -39,7 +39,11 @@ function editSetting(pk, options={}) { // First, read the settings object from the server inventreeGet(url, {}, { success: function(response) { - + + if (response.choices && response.choices.length > 0) { + response.type = 'choice'; + } + // Construct the field var fields = { value: { From e303b5a39bf87ad73179ec33fd171c4fc411c396 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 9 Nov 2021 22:37:50 +1100 Subject: [PATCH 16/19] Catch for null value for settings choices --- InvenTree/common/serializers.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/InvenTree/common/serializers.py b/InvenTree/common/serializers.py index 35a1c79879..4a27e3f30e 100644 --- a/InvenTree/common/serializers.py +++ b/InvenTree/common/serializers.py @@ -34,11 +34,14 @@ class SettingsSerializer(InvenTreeModelSerializer): results = [] - for choice in obj.choices(): - results.append({ - 'value': choice[0], - 'display_name': choice[1], - }) + choices = obj.choices() + + if choices: + for choice in choices: + results.append({ + 'value': choice[0], + 'display_name': choice[1], + }) return results From db31bf91e66a269aefaf851c8a65491b90ca8176 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 9 Nov 2021 22:41:54 +1100 Subject: [PATCH 17/19] Improve display of "no response from server" message --- InvenTree/templates/js/translated/api.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/InvenTree/templates/js/translated/api.js b/InvenTree/templates/js/translated/api.js index d0640408be..15a74a9a71 100644 --- a/InvenTree/templates/js/translated/api.js +++ b/InvenTree/templates/js/translated/api.js @@ -61,7 +61,11 @@ function inventreeGet(url, filters={}, options={}) { }, error: function(xhr, ajaxOptions, thrownError) { console.error('Error on GET at ' + url); - console.error(thrownError); + + if (thrownError) { + console.error('Error: ' + thrownError); + } + if (options.error) { options.error({ error: thrownError @@ -174,7 +178,7 @@ function showApiError(xhr, url) { var title = null; var message = null; - switch (xhr.status) { + switch (xhr.status || 0) { // No response case 0: title = '{% trans "No Response" %}'; From cfb873bb4f3c1b3f1984c82ea87078376d7de880 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 9 Nov 2021 22:47:19 +1100 Subject: [PATCH 18/19] Bug fix for loading part table without enabling grid view --- InvenTree/templates/js/translated/part.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/InvenTree/templates/js/translated/part.js b/InvenTree/templates/js/translated/part.js index 742083bbe4..dc1adf8837 100644 --- a/InvenTree/templates/js/translated/part.js +++ b/InvenTree/templates/js/translated/part.js @@ -992,7 +992,7 @@ function loadPartTable(table, url, options={}) { } }); - var grid_view = inventreeLoad('part-grid-view') == 1; + var grid_view = options.gridView && inventreeLoad('part-grid-view') == 1; $(table).inventreeTable({ url: url, @@ -1020,7 +1020,7 @@ function loadPartTable(table, url, options={}) { $('#view-part-list').removeClass('btn-outline-secondary').addClass('btn-secondary'); } }, - buttons: [ + buttons: options.gridView ? [ { icon: 'fas fa-bars', attributes: { @@ -1053,7 +1053,7 @@ function loadPartTable(table, url, options={}) { ); } } - ], + ] : [], customView: function(data) { var html = ''; From 3dae0c9c1cd63ae9601ea6ead5f6c5049daa1c72 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 10 Nov 2021 08:48:51 +1100 Subject: [PATCH 19/19] Remove old unit test code --- InvenTree/common/test_views.py | 153 --------------------------------- 1 file changed, 153 deletions(-) diff --git a/InvenTree/common/test_views.py b/InvenTree/common/test_views.py index 76a0a4516e..7d7bfde87e 100644 --- a/InvenTree/common/test_views.py +++ b/InvenTree/common/test_views.py @@ -4,156 +4,3 @@ Unit tests for the views associated with the 'common' app # -*- coding: utf-8 -*- from __future__ import unicode_literals - -import json - -from django.test import TestCase -from django.urls import reverse -from django.contrib.auth import get_user_model - -from common.models import InvenTreeSetting - - -class SettingsViewTest(TestCase): - """ - Tests for the settings management views - """ - - fixtures = [ - 'settings', - ] - - def setUp(self): - super().setUp() - - # Create a user (required to access the views / forms) - self.user = get_user_model().objects.create_user( - username='username', - email='me@email.com', - password='password', - ) - - self.client.login(username='username', password='password') - - def get_url(self, pk): - return reverse('setting-edit', args=(pk,)) - - def get_setting(self, title): - - return InvenTreeSetting.get_setting_object(title) - - def get(self, url, status=200): - - response = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - - self.assertEqual(response.status_code, status) - - data = json.loads(response.content) - - return response, data - - def post(self, url, data, valid=None): - - response = self.client.post(url, data, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - - json_data = json.loads(response.content) - - # If a particular status code is required - if valid is not None: - if valid: - self.assertEqual(json_data['form_valid'], True) - else: - self.assertEqual(json_data['form_valid'], False) - - form_errors = json.loads(json_data['form_errors']) - - return json_data, form_errors - - def test_instance_name(self): - """ - Test that we can get the settings view for particular setting objects. - """ - - # Start with something basic - load the settings view for INVENTREE_INSTANCE - setting = self.get_setting('INVENTREE_INSTANCE') - - self.assertIsNotNone(setting) - self.assertEqual(setting.value, 'My very first InvenTree Instance') - - url = self.get_url(setting.pk) - - self.get(url) - - new_name = 'A new instance name!' - - # Change the instance name via the form - data, errors = self.post(url, {'value': new_name}, valid=True) - - name = InvenTreeSetting.get_setting('INVENTREE_INSTANCE') - - self.assertEqual(name, new_name) - - def test_choices(self): - """ - Tests for a setting which has choices - """ - - setting = InvenTreeSetting.get_setting_object('PURCHASEORDER_REFERENCE_PREFIX') - - # Default value! - self.assertEqual(setting.value, 'PO') - - url = self.get_url(setting.pk) - - # Try posting an invalid currency option - data, errors = self.post(url, {'value': 'Purchase Order'}, valid=True) - - def test_binary_values(self): - """ - Test for binary value - """ - - setting = InvenTreeSetting.get_setting_object('PART_COMPONENT') - - self.assertTrue(setting.as_bool()) - - url = self.get_url(setting.pk) - - setting.value = True - setting.save() - - # Try posting some invalid values - # The value should be "cleaned" and stay the same - for value in ['', 'abc', 'cat', 'TRUETRUETRUE']: - self.post(url, {'value': value}, valid=True) - - # Try posting some valid (True) values - for value in [True, 'True', '1', 'yes']: - self.post(url, {'value': value}, valid=True) - self.assertTrue(InvenTreeSetting.get_setting('PART_COMPONENT')) - - # Try posting some valid (False) values - for value in [False, 'False']: - self.post(url, {'value': value}, valid=True) - self.assertFalse(InvenTreeSetting.get_setting('PART_COMPONENT')) - - def test_part_name_format(self): - """ - Try posting some valid and invalid name formats for PART_NAME_FORMAT - """ - setting = InvenTreeSetting.get_setting_object('PART_NAME_FORMAT') - - # test default value - self.assertEqual(setting.value, "{{ part.IPN if part.IPN }}{{ ' | ' if part.IPN }}{{ part.name }}" - "{{ ' | ' if part.revision }}{{ part.revision if part.revision }}") - - url = self.get_url(setting.pk) - - # Try posting an invalid part name format - invalid_values = ['{{asset.IPN}}', '{{part}}', '{{"|"}}', '{{part.falcon}}'] - for invalid_value in invalid_values: - self.post(url, {'value': invalid_value}, valid=False) - - # try posting valid value - new_format = "{{ part.name if part.name }} {{ ' with revision ' if part.revision }} {{ part.revision }}" - self.post(url, {'value': new_format}, valid=True)