mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge pull request #2275 from SchrodingersGat/settings-via-api
Settings via api
This commit is contained in:
commit
558c2cc275
1
.github/workflows/html.yaml
vendored
1
.github/workflows/html.yaml
vendored
@ -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
|
||||
|
@ -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
|
||||
|
||||
@ -53,7 +51,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)),
|
||||
@ -85,9 +83,6 @@ settings_urls = [
|
||||
|
||||
url(r'^category/', SettingCategorySelectView.as_view(), name='settings-category'),
|
||||
|
||||
url(r'^(?P<pk>\d+)/edit/user', UserSettingEdit.as_view(), name='user-setting-edit'),
|
||||
url(r'^(?P<pk>\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'),
|
||||
]
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
},
|
||||
@ -867,6 +921,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):
|
||||
"""
|
||||
@ -1077,6 +1139,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
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -1,6 +1,7 @@
|
||||
{% load inventree_extras %}
|
||||
|
||||
/* exported
|
||||
editSetting,
|
||||
user_settings,
|
||||
global_settings,
|
||||
*/
|
||||
@ -18,3 +19,83 @@ const global_settings = {
|
||||
{{ key }}: {% primitive_to_javascript value %},
|
||||
{% 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) {
|
||||
|
||||
if (response.choices && response.choices.length > 0) {
|
||||
response.type = 'choice';
|
||||
}
|
||||
|
||||
// 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: options.title,
|
||||
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;
|
||||
},
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
error: function(xhr) {
|
||||
showApiError(xhr, url);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -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" %}';
|
||||
|
@ -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,
|
||||
|
@ -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 = '';
|
||||
|
Loading…
Reference in New Issue
Block a user