Merge remote-tracking branch 'inventree/master'

This commit is contained in:
Oliver Walters 2022-05-02 00:08:17 +10:00
commit de4afb77fd
9 changed files with 305 additions and 50 deletions

View File

@ -19,42 +19,30 @@ jobs:
INVENTREE_STATIC_ROOT: ./static INVENTREE_STATIC_ROOT: ./static
steps: steps:
- uses: actions/checkout@v2 - name: Checkout Code
- name: Get Current Translations uses: actions/checkout@v2
run: | - name: Set up Python 3.7
git fetch uses: actions/setup-python@v1
git checkout origin/l10 -- `git ls-tree origin/l10 -r --name-only | grep ".po"` with:
git reset python-version: 3.7
- name: Set up Python 3.7 - name: Install Dependencies
uses: actions/setup-python@v1 run: |
with: sudo apt-get update
python-version: 3.7 sudo apt-get install -y gettext
- name: Install Dependencies pip3 install invoke
run: | invoke install
sudo apt-get update - name: Make Translations
sudo apt-get install -y gettext run: |
pip3 install invoke invoke translate
invoke install - name: Commit files
- name: Make Translations run: |
run: | git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
invoke translate git config --local user.name "github-actions[bot]"
- name: stash changes git checkout -b l10_local
run: | git add "*.po"
git stash git commit -m "updated translation base"
- name: Checkout Translation Branch - name: Push changes
uses: actions/checkout@v2.3.4 uses: ad-m/github-push-action@master
with: with:
ref: l10 github_token: ${{ secrets.GITHUB_TOKEN }}
- name: Commit files branch: l10
run: |
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git checkout stash -- .
git reset
git add "*.po"
git commit -m "updated translation base"
- name: Push changes
uses: ad-m/github-push-action@master
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
branch: l10

3
.gitignore vendored
View File

@ -85,3 +85,6 @@ maintenance_mode_state.txt
# plugin dev directory # plugin dev directory
plugins/ plugins/
# Compiled translation files
*.mo

View File

@ -18,8 +18,6 @@ def assign_bom_items(apps, schema_editor):
BomItem = apps.get_model('part', 'bomitem') BomItem = apps.get_model('part', 'bomitem')
Part = apps.get_model('part', 'part') Part = apps.get_model('part', 'part')
logger.info("Assigning BomItems to existing BuildItem objects")
count_valid = 0 count_valid = 0
count_total = 0 count_total = 0
@ -29,6 +27,10 @@ def assign_bom_items(apps, schema_editor):
# Note: Before this migration, variant stock assignment was not allowed, # Note: Before this migration, variant stock assignment was not allowed,
# so BomItem lookup should be pretty easy # so BomItem lookup should be pretty easy
if count_total == 0:
# First time around
logger.info("Assigning BomItems to existing BuildItem objects")
count_total += 1 count_total += 1
try: try:

View File

@ -406,6 +406,13 @@ class BaseInvenTreeSetting(models.Model):
super().clean() 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) validator = self.__class__.get_setting_validator(self.key, **kwargs)
if validator is not None: if validator is not None:
@ -455,7 +462,14 @@ class BaseInvenTreeSetting(models.Model):
if callable(validator): if callable(validator):
# We can accept function validators with a single argument # 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): def validate_unique(self, exclude=None, **kwargs):
""" """
@ -629,6 +643,10 @@ class BaseInvenTreeSetting(models.Model):
return setting.get('protected', False) return setting.get('protected', False)
@property
def protected(self):
return self.__class__.is_protected(self.key)
def settings_group_options(): def settings_group_options():
""" """
@ -1011,48 +1029,56 @@ class InvenTreeSetting(BaseInvenTreeSetting):
'default': True, 'default': True,
'validator': bool, 'validator': bool,
}, },
'LOGIN_ENABLE_REG': { 'LOGIN_ENABLE_REG': {
'name': _('Enable registration'), 'name': _('Enable registration'),
'description': _('Enable self-registration for users on the login pages'), 'description': _('Enable self-registration for users on the login pages'),
'default': False, 'default': False,
'validator': bool, 'validator': bool,
}, },
'LOGIN_ENABLE_SSO': { 'LOGIN_ENABLE_SSO': {
'name': _('Enable SSO'), 'name': _('Enable SSO'),
'description': _('Enable SSO on the login pages'), 'description': _('Enable SSO on the login pages'),
'default': False, 'default': False,
'validator': bool, 'validator': bool,
}, },
'LOGIN_MAIL_REQUIRED': { 'LOGIN_MAIL_REQUIRED': {
'name': _('Email required'), 'name': _('Email required'),
'description': _('Require user to supply mail on signup'), 'description': _('Require user to supply mail on signup'),
'default': False, 'default': False,
'validator': bool, 'validator': bool,
}, },
'LOGIN_SIGNUP_SSO_AUTO': { 'LOGIN_SIGNUP_SSO_AUTO': {
'name': _('Auto-fill SSO users'), 'name': _('Auto-fill SSO users'),
'description': _('Automatically fill out user-details from SSO account-data'), 'description': _('Automatically fill out user-details from SSO account-data'),
'default': True, 'default': True,
'validator': bool, 'validator': bool,
}, },
'LOGIN_SIGNUP_MAIL_TWICE': { 'LOGIN_SIGNUP_MAIL_TWICE': {
'name': _('Mail twice'), 'name': _('Mail twice'),
'description': _('On signup ask users twice for their mail'), 'description': _('On signup ask users twice for their mail'),
'default': False, 'default': False,
'validator': bool, 'validator': bool,
}, },
'LOGIN_SIGNUP_PWD_TWICE': { 'LOGIN_SIGNUP_PWD_TWICE': {
'name': _('Password twice'), 'name': _('Password twice'),
'description': _('On signup ask users twice for their password'), 'description': _('On signup ask users twice for their password'),
'default': True, 'default': True,
'validator': bool, 'validator': bool,
}, },
'SIGNUP_GROUP': { 'SIGNUP_GROUP': {
'name': _('Group on signup'), 'name': _('Group on signup'),
'description': _('Group to which new users are assigned on registration'), 'description': _('Group to which new users are assigned on registration'),
'default': '', 'default': '',
'choices': settings_group_options 'choices': settings_group_options
}, },
'LOGIN_ENFORCE_MFA': { 'LOGIN_ENFORCE_MFA': {
'name': _('Enforce MFA'), 'name': _('Enforce MFA'),
'description': _('Users must use multifactor security.'), 'description': _('Users must use multifactor security.'),
@ -1067,6 +1093,7 @@ class InvenTreeSetting(BaseInvenTreeSetting):
'validator': bool, 'validator': bool,
'requires_restart': True, 'requires_restart': True,
}, },
# Settings for plugin mixin features # Settings for plugin mixin features
'ENABLE_PLUGINS_URL': { 'ENABLE_PLUGINS_URL': {
'name': _('Enable URL integration'), 'name': _('Enable URL integration'),
@ -1075,6 +1102,7 @@ class InvenTreeSetting(BaseInvenTreeSetting):
'validator': bool, 'validator': bool,
'requires_restart': True, 'requires_restart': True,
}, },
'ENABLE_PLUGINS_NAVIGATION': { 'ENABLE_PLUGINS_NAVIGATION': {
'name': _('Enable navigation integration'), 'name': _('Enable navigation integration'),
'description': _('Enable plugins to integrate into navigation'), 'description': _('Enable plugins to integrate into navigation'),
@ -1082,6 +1110,7 @@ class InvenTreeSetting(BaseInvenTreeSetting):
'validator': bool, 'validator': bool,
'requires_restart': True, 'requires_restart': True,
}, },
'ENABLE_PLUGINS_APP': { 'ENABLE_PLUGINS_APP': {
'name': _('Enable app integration'), 'name': _('Enable app integration'),
'description': _('Enable plugins to add apps'), 'description': _('Enable plugins to add apps'),
@ -1089,6 +1118,7 @@ class InvenTreeSetting(BaseInvenTreeSetting):
'validator': bool, 'validator': bool,
'requires_restart': True, 'requires_restart': True,
}, },
'ENABLE_PLUGINS_SCHEDULE': { 'ENABLE_PLUGINS_SCHEDULE': {
'name': _('Enable schedule integration'), 'name': _('Enable schedule integration'),
'description': _('Enable plugins to run scheduled tasks'), 'description': _('Enable plugins to run scheduled tasks'),
@ -1096,6 +1126,7 @@ class InvenTreeSetting(BaseInvenTreeSetting):
'validator': bool, 'validator': bool,
'requires_restart': True, 'requires_restart': True,
}, },
'ENABLE_PLUGINS_EVENTS': { 'ENABLE_PLUGINS_EVENTS': {
'name': _('Enable event integration'), 'name': _('Enable event integration'),
'description': _('Enable plugins to respond to internal events'), 'description': _('Enable plugins to respond to internal events'),
@ -1149,18 +1180,21 @@ class InvenTreeUserSetting(BaseInvenTreeSetting):
'default': True, 'default': True,
'validator': bool, 'validator': bool,
}, },
'HOMEPAGE_CATEGORY_STARRED': { 'HOMEPAGE_CATEGORY_STARRED': {
'name': _('Show subscribed categories'), 'name': _('Show subscribed categories'),
'description': _('Show subscribed part categories on the homepage'), 'description': _('Show subscribed part categories on the homepage'),
'default': True, 'default': True,
'validator': bool, 'validator': bool,
}, },
'HOMEPAGE_PART_LATEST': { 'HOMEPAGE_PART_LATEST': {
'name': _('Show latest parts'), 'name': _('Show latest parts'),
'description': _('Show latest parts on the homepage'), 'description': _('Show latest parts on the homepage'),
'default': True, 'default': True,
'validator': bool, 'validator': bool,
}, },
'PART_RECENT_COUNT': { 'PART_RECENT_COUNT': {
'name': _('Recent Part Count'), 'name': _('Recent Part Count'),
'description': _('Number of recent parts to display on index page'), 'description': _('Number of recent parts to display on index page'),
@ -1174,78 +1208,91 @@ class InvenTreeUserSetting(BaseInvenTreeSetting):
'default': True, 'default': True,
'validator': bool, 'validator': bool,
}, },
'HOMEPAGE_STOCK_RECENT': { 'HOMEPAGE_STOCK_RECENT': {
'name': _('Show recent stock changes'), 'name': _('Show recent stock changes'),
'description': _('Show recently changed stock items on the homepage'), 'description': _('Show recently changed stock items on the homepage'),
'default': True, 'default': True,
'validator': bool, 'validator': bool,
}, },
'STOCK_RECENT_COUNT': { 'STOCK_RECENT_COUNT': {
'name': _('Recent Stock Count'), 'name': _('Recent Stock Count'),
'description': _('Number of recent stock items to display on index page'), 'description': _('Number of recent stock items to display on index page'),
'default': 10, 'default': 10,
'validator': [int, MinValueValidator(1)] 'validator': [int, MinValueValidator(1)]
}, },
'HOMEPAGE_STOCK_LOW': { 'HOMEPAGE_STOCK_LOW': {
'name': _('Show low stock'), 'name': _('Show low stock'),
'description': _('Show low stock items on the homepage'), 'description': _('Show low stock items on the homepage'),
'default': True, 'default': True,
'validator': bool, 'validator': bool,
}, },
'HOMEPAGE_STOCK_DEPLETED': { 'HOMEPAGE_STOCK_DEPLETED': {
'name': _('Show depleted stock'), 'name': _('Show depleted stock'),
'description': _('Show depleted stock items on the homepage'), 'description': _('Show depleted stock items on the homepage'),
'default': True, 'default': True,
'validator': bool, 'validator': bool,
}, },
'HOMEPAGE_STOCK_NEEDED': { 'HOMEPAGE_STOCK_NEEDED': {
'name': _('Show needed stock'), 'name': _('Show needed stock'),
'description': _('Show stock items needed for builds on the homepage'), 'description': _('Show stock items needed for builds on the homepage'),
'default': True, 'default': True,
'validator': bool, 'validator': bool,
}, },
'HOMEPAGE_STOCK_EXPIRED': { 'HOMEPAGE_STOCK_EXPIRED': {
'name': _('Show expired stock'), 'name': _('Show expired stock'),
'description': _('Show expired stock items on the homepage'), 'description': _('Show expired stock items on the homepage'),
'default': True, 'default': True,
'validator': bool, 'validator': bool,
}, },
'HOMEPAGE_STOCK_STALE': { 'HOMEPAGE_STOCK_STALE': {
'name': _('Show stale stock'), 'name': _('Show stale stock'),
'description': _('Show stale stock items on the homepage'), 'description': _('Show stale stock items on the homepage'),
'default': True, 'default': True,
'validator': bool, 'validator': bool,
}, },
'HOMEPAGE_BUILD_PENDING': { 'HOMEPAGE_BUILD_PENDING': {
'name': _('Show pending builds'), 'name': _('Show pending builds'),
'description': _('Show pending builds on the homepage'), 'description': _('Show pending builds on the homepage'),
'default': True, 'default': True,
'validator': bool, 'validator': bool,
}, },
'HOMEPAGE_BUILD_OVERDUE': { 'HOMEPAGE_BUILD_OVERDUE': {
'name': _('Show overdue builds'), 'name': _('Show overdue builds'),
'description': _('Show overdue builds on the homepage'), 'description': _('Show overdue builds on the homepage'),
'default': True, 'default': True,
'validator': bool, 'validator': bool,
}, },
'HOMEPAGE_PO_OUTSTANDING': { 'HOMEPAGE_PO_OUTSTANDING': {
'name': _('Show outstanding POs'), 'name': _('Show outstanding POs'),
'description': _('Show outstanding POs on the homepage'), 'description': _('Show outstanding POs on the homepage'),
'default': True, 'default': True,
'validator': bool, 'validator': bool,
}, },
'HOMEPAGE_PO_OVERDUE': { 'HOMEPAGE_PO_OVERDUE': {
'name': _('Show overdue POs'), 'name': _('Show overdue POs'),
'description': _('Show overdue POs on the homepage'), 'description': _('Show overdue POs on the homepage'),
'default': True, 'default': True,
'validator': bool, 'validator': bool,
}, },
'HOMEPAGE_SO_OUTSTANDING': { 'HOMEPAGE_SO_OUTSTANDING': {
'name': _('Show outstanding SOs'), 'name': _('Show outstanding SOs'),
'description': _('Show outstanding SOs on the homepage'), 'description': _('Show outstanding SOs on the homepage'),
'default': True, 'default': True,
'validator': bool, 'validator': bool,
}, },
'HOMEPAGE_SO_OVERDUE': { 'HOMEPAGE_SO_OVERDUE': {
'name': _('Show overdue SOs'), 'name': _('Show overdue SOs'),
'description': _('Show overdue SOs on the homepage'), 'description': _('Show overdue SOs on the homepage'),

View File

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

View File

@ -9,7 +9,9 @@ from django.contrib.auth import get_user_model
from django.urls import reverse from django.urls import reverse
from InvenTree.api_tester import InvenTreeAPITestCase 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 from .api import WebhookView
CONTENT_TYPE_JSON = 'application/json' 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 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): def test_global_settings_api_list(self):
# test setting with choice """
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') url = reverse('api-user-setting-list')
self.get(url, expected_code=200) 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): class WebhookMessageTests(TestCase):
def setUp(self): def setUp(self):

View File

@ -1,5 +1,6 @@
{% extends "skeleton.html" %} {% extends "skeleton.html" %}
{% load static %} {% load static %}
{% load inventree_extras %}
{% load i18n %} {% load i18n %}
{% block head %} {% block head %}

View File

@ -2413,7 +2413,7 @@ function showAllocationSubTable(index, row, element, options) {
}, },
{ {
field: 'buttons', field: 'buttons',
title: '{% trans "" %}', title: '',
formatter: function(value, row, index, field) { formatter: function(value, row, index, field) {
var html = `<div class='btn-group float-right' role='group'>`; var html = `<div class='btn-group float-right' role='group'>`;

View File

@ -236,7 +236,7 @@ def translate(c):
manage(c, "compilemessages") manage(c, "compilemessages")
@task(pre=[install, migrate, translate_stats, static, clean_settings]) @task(pre=[install, migrate, translate, static, clean_settings])
def update(c): def update(c):
""" """
Update InvenTree installation. Update InvenTree installation.