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
diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py
index 053ba05264..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
@@ -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\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/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 8a2dfbd6a7..6dd51bdff1 100644
--- a/InvenTree/common/api.py
+++ b/InvenTree/common/api.py
@@ -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\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\d+)/', GlobalSettingsDetail.as_view(), name='api-global-setting-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..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
@@ -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):
"""
diff --git a/InvenTree/common/serializers.py b/InvenTree/common/serializers.py
index 99ac03cdfd..4a27e3f30e 100644
--- a/InvenTree/common/serializers.py
+++ b/InvenTree/common/serializers.py
@@ -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',
+ ]
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/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)
diff --git a/InvenTree/common/views.py b/InvenTree/common/views.py
index 6cac8bbb19..4b8310ddd2 100644
--- a/InvenTree/common/views.py
+++ b/InvenTree/common/views.py
@@ -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
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 cb21e1fefc..8201dc8374 100644
--- a/InvenTree/templates/js/dynamic/settings.js
+++ b/InvenTree/templates/js/dynamic/settings.js
@@ -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);
+ }
+ });
+}
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" %}';
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,
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 = '';