mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-30 20:55:42 +00:00 
			
		
		
		
	merge
This commit is contained in:
		| @@ -5,5 +5,149 @@ 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, permissions | ||||
|  | ||||
| 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 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' | ||||
|         """ | ||||
|  | ||||
|         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): | ||||
|     """ | ||||
|     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 | ||||
|  | ||||
|  | ||||
| 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): | ||||
|  | ||||
|         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<pk>\d+)/', UserSettingsDetail.as_view(), name='api-user-setting-detail'), | ||||
|  | ||||
|         # User Settings List | ||||
|         url(r'^.*$', UserSettingsList.as_view(), name='api-user-setting-list'), | ||||
|     ])), | ||||
|  | ||||
|     # Global settings | ||||
|     url(r'^global/', include([ | ||||
|         # Global Settings Detail | ||||
|         url(r'^(?P<pk>\d+)/', GlobalSettingsDetail.as_view(), name='api-global-setting-detail'), | ||||
|  | ||||
|         # Global Settings List | ||||
|         url(r'^.*$', GlobalSettingsList.as_view(), name='api-global-setting-list'), | ||||
|     ])) | ||||
|  | ||||
| ] | ||||
|   | ||||
| @@ -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 | ||||
| @@ -45,6 +58,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): | ||||
|         """ | ||||
| @@ -343,6 +366,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) | ||||
|  | ||||
| @@ -409,6 +437,18 @@ 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 | ||||
| @@ -427,6 +467,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): | ||||
|  | ||||
| @@ -531,7 +585,7 @@ class InvenTreeSetting(BaseInvenTreeSetting): | ||||
|         'INVENTREE_BASE_URL': { | ||||
|             'name': _('Base URL'), | ||||
|             'description': _('Base URL for server instance'), | ||||
|             'validator': URLValidator(), | ||||
|             'validator': EmptyURLValidator(), | ||||
|             'default': '', | ||||
|         }, | ||||
|  | ||||
| @@ -850,7 +904,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 | ||||
|         }, | ||||
| @@ -873,6 +927,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): | ||||
|     """ | ||||
| @@ -1083,6 +1145,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): | ||||
|     """ | ||||
|   | ||||
| @@ -1,3 +1,85 @@ | ||||
| """ | ||||
| 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 SettingsSerializer(InvenTreeModelSerializer): | ||||
|     """ | ||||
|     Base serializer for a settings object | ||||
|     """ | ||||
|  | ||||
|     key = serializers.CharField(read_only=True) | ||||
|  | ||||
|     name = serializers.CharField(read_only=True) | ||||
|  | ||||
|     description = serializers.CharField(read_only=True) | ||||
|  | ||||
|     type = serializers.CharField(source='setting_type', read_only=True) | ||||
|  | ||||
|     choices = serializers.SerializerMethodField() | ||||
|  | ||||
|     def get_choices(self, obj): | ||||
|         """ | ||||
|         Returns the choices available for a given item | ||||
|         """ | ||||
|  | ||||
|         results = [] | ||||
|  | ||||
|         choices = obj.choices() | ||||
|  | ||||
|         if choices: | ||||
|             for choice in choices: | ||||
|                 results.append({ | ||||
|                     'value': choice[0], | ||||
|                     'display_name': choice[1], | ||||
|                 }) | ||||
|  | ||||
|         return results | ||||
|  | ||||
|  | ||||
| class GlobalSettingsSerializer(SettingsSerializer): | ||||
|     """ | ||||
|     Serializer for the InvenTreeSetting model | ||||
|     """ | ||||
|  | ||||
|     class Meta: | ||||
|         model = InvenTreeSetting | ||||
|         fields = [ | ||||
|             'pk', | ||||
|             'key', | ||||
|             'value', | ||||
|             'name', | ||||
|             'description', | ||||
|             'type', | ||||
|             'choices', | ||||
|         ] | ||||
|  | ||||
|  | ||||
| class UserSettingsSerializer(SettingsSerializer): | ||||
|     """ | ||||
|     Serializer for the InvenTreeUserSetting model | ||||
|     """ | ||||
|  | ||||
|     user = serializers.PrimaryKeyRelatedField(read_only=True) | ||||
|  | ||||
|     class Meta: | ||||
|         model = InvenTreeUserSetting | ||||
|         fields = [ | ||||
|             'pk', | ||||
|             'key', | ||||
|             'value', | ||||
|             'name', | ||||
|             'description', | ||||
|             'user', | ||||
|             'type', | ||||
|             'choices', | ||||
|         ] | ||||
|   | ||||
| @@ -1,14 +0,0 @@ | ||||
| {% extends "modal_form.html" %} | ||||
| {% load i18n %} | ||||
|  | ||||
| {% block pre_form_content %} | ||||
|  | ||||
| {{ block.super }} | ||||
| <!-- | ||||
|     <p> | ||||
|         <strong>{{ name }}</strong><br> | ||||
|         {{ description }}<br> | ||||
|         <em>{% trans "Current value" %}: {{ value }}</em> | ||||
|     </p> | ||||
| --> | ||||
| {% endblock %} | ||||
| @@ -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) | ||||
|   | ||||
| @@ -8,138 +8,18 @@ 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.helpers import str2bool | ||||
| from InvenTree.views import AjaxView | ||||
|  | ||||
| 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 | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user