2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-18 13:05:42 +00:00

Merge branch 'inventree:master' into matmair/issue2385

This commit is contained in:
Matthias Mair
2022-05-02 21:50:52 +02:00
committed by GitHub
143 changed files with 39337 additions and 29974 deletions

View File

@ -11,7 +11,7 @@ from django.http.response import HttpResponse
from django.utils.decorators import method_decorator
from django.urls import path
from django.views.decorators.csrf import csrf_exempt
from django.conf.urls import url, include
from django.urls import include, re_path
from rest_framework.views import APIView
from rest_framework.response import Response
@ -336,21 +336,21 @@ class NotificationReadAll(generics.RetrieveAPIView):
settings_api_urls = [
# User settings
url(r'^user/', include([
re_path(r'^user/', include([
# User Settings Detail
url(r'^(?P<pk>\d+)/', UserSettingsDetail.as_view(), name='api-user-setting-detail'),
re_path(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'),
re_path(r'^.*$', UserSettingsList.as_view(), name='api-user-setting-list'),
])),
# Global settings
url(r'^global/', include([
re_path(r'^global/', include([
# Global Settings Detail
url(r'^(?P<pk>\d+)/', GlobalSettingsDetail.as_view(), name='api-global-setting-detail'),
re_path(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'),
re_path(r'^.*$', GlobalSettingsList.as_view(), name='api-global-setting-list'),
])),
]
@ -359,18 +359,18 @@ common_api_urls = [
path('webhook/<slug:endpoint>/', WebhookView.as_view(), name='api-webhook'),
# Notifications
url(r'^notifications/', include([
re_path(r'^notifications/', include([
# Individual purchase order detail URLs
url(r'^(?P<pk>\d+)/', include([
url(r'^read/', NotificationRead.as_view(), name='api-notifications-read'),
url(r'^unread/', NotificationUnread.as_view(), name='api-notifications-unread'),
url(r'.*$', NotificationDetail.as_view(), name='api-notifications-detail'),
re_path(r'^(?P<pk>\d+)/', include([
re_path(r'^read/', NotificationRead.as_view(), name='api-notifications-read'),
re_path(r'^unread/', NotificationUnread.as_view(), name='api-notifications-unread'),
re_path(r'.*$', NotificationDetail.as_view(), name='api-notifications-detail'),
])),
# Read all
url(r'^readall/', NotificationReadAll.as_view(), name='api-notifications-readall'),
re_path(r'^readall/', NotificationReadAll.as_view(), name='api-notifications-readall'),
# Notification messages list
url(r'^.*$', NotificationList.as_view(), name='api-notifications-list'),
re_path(r'^.*$', NotificationList.as_view(), name='api-notifications-list'),
])),
]

View File

@ -33,7 +33,7 @@ from djmoney.contrib.exchange.exceptions import MissingRate
from rest_framework.exceptions import PermissionDenied
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from django.core.validators import MinValueValidator, URLValidator
from django.core.exceptions import ValidationError
@ -412,6 +412,13 @@ class BaseInvenTreeSetting(models.Model):
super().clean()
# Encode as native values
if self.is_int():
self.value = self.as_int()
elif self.is_bool():
self.value = self.as_bool()
validator = self.__class__.get_setting_validator(self.key, **kwargs)
if validator is not None:
@ -461,7 +468,14 @@ class BaseInvenTreeSetting(models.Model):
if callable(validator):
# We can accept function validators with a single argument
validator(self.value)
if self.is_bool():
value = self.as_bool()
if self.is_int():
value = self.as_int()
validator(value)
def validate_unique(self, exclude=None, **kwargs):
"""
@ -635,6 +649,10 @@ class BaseInvenTreeSetting(models.Model):
return setting.get('protected', False)
@property
def protected(self):
return self.__class__.is_protected(self.key)
class GenericReferencedSettingClass:
"""
@ -1098,48 +1116,56 @@ class InvenTreeSetting(BaseInvenTreeSetting):
'default': True,
'validator': bool,
},
'LOGIN_ENABLE_REG': {
'name': _('Enable registration'),
'description': _('Enable self-registration for users on the login pages'),
'default': False,
'validator': bool,
},
'LOGIN_ENABLE_SSO': {
'name': _('Enable SSO'),
'description': _('Enable SSO on the login pages'),
'default': False,
'validator': bool,
},
'LOGIN_MAIL_REQUIRED': {
'name': _('Email required'),
'description': _('Require user to supply mail on signup'),
'default': False,
'validator': bool,
},
'LOGIN_SIGNUP_SSO_AUTO': {
'name': _('Auto-fill SSO users'),
'description': _('Automatically fill out user-details from SSO account-data'),
'default': True,
'validator': bool,
},
'LOGIN_SIGNUP_MAIL_TWICE': {
'name': _('Mail twice'),
'description': _('On signup ask users twice for their mail'),
'default': False,
'validator': bool,
},
'LOGIN_SIGNUP_PWD_TWICE': {
'name': _('Password twice'),
'description': _('On signup ask users twice for their password'),
'default': True,
'validator': bool,
},
'SIGNUP_GROUP': {
'name': _('Group on signup'),
'description': _('Group to which new users are assigned on registration'),
'default': '',
'choices': settings_group_options
},
'LOGIN_ENFORCE_MFA': {
'name': _('Enforce MFA'),
'description': _('Users must use multifactor security.'),
@ -1154,6 +1180,7 @@ class InvenTreeSetting(BaseInvenTreeSetting):
'validator': bool,
'requires_restart': True,
},
# Settings for plugin mixin features
'ENABLE_PLUGINS_URL': {
'name': _('Enable URL integration'),
@ -1162,6 +1189,7 @@ class InvenTreeSetting(BaseInvenTreeSetting):
'validator': bool,
'requires_restart': True,
},
'ENABLE_PLUGINS_NAVIGATION': {
'name': _('Enable navigation integration'),
'description': _('Enable plugins to integrate into navigation'),
@ -1169,6 +1197,7 @@ class InvenTreeSetting(BaseInvenTreeSetting):
'validator': bool,
'requires_restart': True,
},
'ENABLE_PLUGINS_APP': {
'name': _('Enable app integration'),
'description': _('Enable plugins to add apps'),
@ -1176,6 +1205,7 @@ class InvenTreeSetting(BaseInvenTreeSetting):
'validator': bool,
'requires_restart': True,
},
'ENABLE_PLUGINS_SCHEDULE': {
'name': _('Enable schedule integration'),
'description': _('Enable plugins to run scheduled tasks'),
@ -1183,6 +1213,7 @@ class InvenTreeSetting(BaseInvenTreeSetting):
'validator': bool,
'requires_restart': True,
},
'ENABLE_PLUGINS_EVENTS': {
'name': _('Enable event integration'),
'description': _('Enable plugins to respond to internal events'),
@ -1236,18 +1267,21 @@ class InvenTreeUserSetting(BaseInvenTreeSetting):
'default': True,
'validator': bool,
},
'HOMEPAGE_CATEGORY_STARRED': {
'name': _('Show subscribed categories'),
'description': _('Show subscribed part categories on the homepage'),
'default': True,
'validator': bool,
},
'HOMEPAGE_PART_LATEST': {
'name': _('Show latest parts'),
'description': _('Show latest parts on the homepage'),
'default': True,
'validator': bool,
},
'PART_RECENT_COUNT': {
'name': _('Recent Part Count'),
'description': _('Number of recent parts to display on index page'),
@ -1261,78 +1295,91 @@ class InvenTreeUserSetting(BaseInvenTreeSetting):
'default': True,
'validator': bool,
},
'HOMEPAGE_STOCK_RECENT': {
'name': _('Show recent stock changes'),
'description': _('Show recently changed stock items on the homepage'),
'default': True,
'validator': bool,
},
'STOCK_RECENT_COUNT': {
'name': _('Recent Stock Count'),
'description': _('Number of recent stock items to display on index page'),
'default': 10,
'validator': [int, MinValueValidator(1)]
},
'HOMEPAGE_STOCK_LOW': {
'name': _('Show low stock'),
'description': _('Show low stock items on the homepage'),
'default': True,
'validator': bool,
},
'HOMEPAGE_STOCK_DEPLETED': {
'name': _('Show depleted stock'),
'description': _('Show depleted stock items on the homepage'),
'default': True,
'validator': bool,
},
'HOMEPAGE_STOCK_NEEDED': {
'name': _('Show needed stock'),
'description': _('Show stock items needed for builds on the homepage'),
'default': True,
'validator': bool,
},
'HOMEPAGE_STOCK_EXPIRED': {
'name': _('Show expired stock'),
'description': _('Show expired stock items on the homepage'),
'default': True,
'validator': bool,
},
'HOMEPAGE_STOCK_STALE': {
'name': _('Show stale stock'),
'description': _('Show stale stock items on the homepage'),
'default': True,
'validator': bool,
},
'HOMEPAGE_BUILD_PENDING': {
'name': _('Show pending builds'),
'description': _('Show pending builds on the homepage'),
'default': True,
'validator': bool,
},
'HOMEPAGE_BUILD_OVERDUE': {
'name': _('Show overdue builds'),
'description': _('Show overdue builds on the homepage'),
'default': True,
'validator': bool,
},
'HOMEPAGE_PO_OUTSTANDING': {
'name': _('Show outstanding POs'),
'description': _('Show outstanding POs on the homepage'),
'default': True,
'validator': bool,
},
'HOMEPAGE_PO_OVERDUE': {
'name': _('Show overdue POs'),
'description': _('Show overdue POs on the homepage'),
'default': True,
'validator': bool,
},
'HOMEPAGE_SO_OUTSTANDING': {
'name': _('Show outstanding SOs'),
'description': _('Show outstanding SOs on the homepage'),
'default': True,
'validator': bool,
},
'HOMEPAGE_SO_OVERDUE': {
'name': _('Show overdue SOs'),
'description': _('Show overdue SOs on the homepage'),

View File

@ -50,11 +50,12 @@ class SettingsSerializer(InvenTreeModelSerializer):
"""
Make sure protected values are not returned
"""
result = obj.value
# never return protected values
if obj.is_protected:
if obj.protected:
result = '***'
else:
result = obj.value
return result

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
from django.test import TestCase

View File

@ -9,7 +9,9 @@ from django.contrib.auth import get_user_model
from django.urls import reverse
from InvenTree.api_tester import InvenTreeAPITestCase
from .models import InvenTreeSetting, WebhookEndpoint, WebhookMessage, NotificationEntry
from InvenTree.helpers import str2bool
from .models import InvenTreeSetting, InvenTreeUserSetting, WebhookEndpoint, WebhookMessage, NotificationEntry
from .api import WebhookView
CONTENT_TYPE_JSON = 'application/json'
@ -156,13 +158,224 @@ class SettingsTest(TestCase):
raise ValueError(f'Non-boolean default value specified for {key}') # pragma: no cover
class SettingsApiTest(InvenTreeAPITestCase):
class GlobalSettingsApiTest(InvenTreeAPITestCase):
"""
Tests for the global settings API
"""
def test_settings_api(self):
# test setting with choice
def test_global_settings_api_list(self):
"""
Test list URL for global settings
"""
url = reverse('api-global-setting-list')
# Read out each of the global settings value, to ensure they are instantiated in the database
for key in InvenTreeSetting.SETTINGS:
InvenTreeSetting.get_setting_object(key)
response = self.get(url, expected_code=200)
# Number of results should match the number of settings
self.assertEqual(len(response.data), len(InvenTreeSetting.SETTINGS.keys()))
def test_company_name(self):
setting = InvenTreeSetting.get_setting_object('INVENTREE_COMPANY_NAME')
# Check default value
self.assertEqual(setting.value, 'My company name')
url = reverse('api-global-setting-detail', kwargs={'pk': setting.pk})
# Test getting via the API
for val in ['test', '123', 'My company nam3']:
setting.value = val
setting.save()
response = self.get(url, expected_code=200)
self.assertEqual(response.data['value'], val)
# Test setting via the API
for val in ['cat', 'hat', 'bat', 'mat']:
response = self.patch(
url,
{
'value': val,
},
expected_code=200
)
self.assertEqual(response.data['value'], val)
setting.refresh_from_db()
self.assertEqual(setting.value, val)
class UserSettingsApiTest(InvenTreeAPITestCase):
"""
Tests for the user settings API
"""
def test_user_settings_api_list(self):
"""
Test list URL for user settings
"""
url = reverse('api-user-setting-list')
self.get(url, expected_code=200)
def test_user_setting_boolean(self):
"""
Test a boolean user setting value
"""
# Ensure we have a boolean setting available
setting = InvenTreeUserSetting.get_setting_object(
'SEARCH_PREVIEW_SHOW_PARTS',
user=self.user
)
# Check default values
self.assertEqual(setting.to_native_value(), True)
# Fetch via API
url = reverse('api-user-setting-detail', kwargs={'pk': setting.pk})
response = self.get(url, expected_code=200)
self.assertEqual(response.data['pk'], setting.pk)
self.assertEqual(response.data['key'], 'SEARCH_PREVIEW_SHOW_PARTS')
self.assertEqual(response.data['description'], 'Display parts in search preview window')
self.assertEqual(response.data['type'], 'boolean')
self.assertEqual(len(response.data['choices']), 0)
self.assertTrue(str2bool(response.data['value']))
# Assign some truthy values
for v in ['true', True, 1, 'y', 'TRUE']:
self.patch(
url,
{
'value': str(v),
},
expected_code=200,
)
response = self.get(url, expected_code=200)
self.assertTrue(str2bool(response.data['value']))
# Assign some falsey values
for v in ['false', False, '0', 'n', 'FalSe']:
self.patch(
url,
{
'value': str(v),
},
expected_code=200,
)
response = self.get(url, expected_code=200)
self.assertFalse(str2bool(response.data['value']))
# Assign some invalid values
for v in ['x', '', 'invalid', None, '-1', 'abcde']:
response = self.patch(
url,
{
'value': str(v),
},
expected_code=200
)
# Invalid values evaluate to False
self.assertFalse(str2bool(response.data['value']))
def test_user_setting_choice(self):
setting = InvenTreeUserSetting.get_setting_object(
'DATE_DISPLAY_FORMAT',
user=self.user
)
url = reverse('api-user-setting-detail', kwargs={'pk': setting.pk})
# Check default value
self.assertEqual(setting.value, 'YYYY-MM-DD')
# Check that a valid option can be assigned via the API
for opt in ['YYYY-MM-DD', 'DD-MM-YYYY', 'MM/DD/YYYY']:
self.patch(
url,
{
'value': opt,
},
expected_code=200,
)
setting.refresh_from_db()
self.assertEqual(setting.value, opt)
# Send an invalid option
for opt in ['cat', 'dog', 12345]:
response = self.patch(
url,
{
'value': opt,
},
expected_code=400,
)
self.assertIn('Chosen value is not a valid option', str(response.data))
def test_user_setting_integer(self):
setting = InvenTreeUserSetting.get_setting_object(
'SEARCH_PREVIEW_RESULTS',
user=self.user
)
url = reverse('api-user-setting-detail', kwargs={'pk': setting.pk})
# Check default value for this setting
self.assertEqual(setting.value, 10)
for v in [1, 9, 99]:
setting.value = v
setting.save()
response = self.get(url)
self.assertEqual(response.data['value'], str(v))
# Set valid options via the api
for v in [5, 15, 25]:
self.patch(
url,
{
'value': v,
},
expected_code=200,
)
setting.refresh_from_db()
self.assertEqual(setting.to_native_value(), v)
# Set invalid options via the API
# Note that this particular setting has a MinValueValidator(1) associated with it
for v in [0, -1, -5]:
response = self.patch(
url,
{
'value': v,
},
expected_code=400,
)
class WebhookMessageTests(TestCase):
def setUp(self):

View File

@ -7,7 +7,7 @@ from __future__ import unicode_literals
import os
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from django.conf import settings
from django.core.files.storage import FileSystemStorage