mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge remote-tracking branch 'inventree/master'
This commit is contained in:
commit
de4afb77fd
66
.github/workflows/translations.yml
vendored
66
.github/workflows/translations.yml
vendored
@ -19,42 +19,30 @@ jobs:
|
||||
INVENTREE_STATIC_ROOT: ./static
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Get Current Translations
|
||||
run: |
|
||||
git fetch
|
||||
git checkout origin/l10 -- `git ls-tree origin/l10 -r --name-only | grep ".po"`
|
||||
git reset
|
||||
- name: Set up Python 3.7
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.7
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y gettext
|
||||
pip3 install invoke
|
||||
invoke install
|
||||
- name: Make Translations
|
||||
run: |
|
||||
invoke translate
|
||||
- name: stash changes
|
||||
run: |
|
||||
git stash
|
||||
- name: Checkout Translation Branch
|
||||
uses: actions/checkout@v2.3.4
|
||||
with:
|
||||
ref: l10
|
||||
- name: Commit files
|
||||
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
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up Python 3.7
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.7
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y gettext
|
||||
pip3 install invoke
|
||||
invoke install
|
||||
- name: Make Translations
|
||||
run: |
|
||||
invoke translate
|
||||
- name: Commit files
|
||||
run: |
|
||||
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git config --local user.name "github-actions[bot]"
|
||||
git checkout -b l10_local
|
||||
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
3
.gitignore
vendored
@ -85,3 +85,6 @@ maintenance_mode_state.txt
|
||||
|
||||
# plugin dev directory
|
||||
plugins/
|
||||
|
||||
# Compiled translation files
|
||||
*.mo
|
||||
|
@ -17,8 +17,6 @@ def assign_bom_items(apps, schema_editor):
|
||||
BuildItem = apps.get_model('build', 'builditem')
|
||||
BomItem = apps.get_model('part', 'bomitem')
|
||||
Part = apps.get_model('part', 'part')
|
||||
|
||||
logger.info("Assigning BomItems to existing BuildItem objects")
|
||||
|
||||
count_valid = 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,
|
||||
# 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
|
||||
|
||||
try:
|
||||
|
@ -406,6 +406,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:
|
||||
@ -455,7 +462,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):
|
||||
"""
|
||||
@ -629,6 +643,10 @@ class BaseInvenTreeSetting(models.Model):
|
||||
|
||||
return setting.get('protected', False)
|
||||
|
||||
@property
|
||||
def protected(self):
|
||||
return self.__class__.is_protected(self.key)
|
||||
|
||||
|
||||
def settings_group_options():
|
||||
"""
|
||||
@ -1011,48 +1029,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.'),
|
||||
@ -1067,6 +1093,7 @@ class InvenTreeSetting(BaseInvenTreeSetting):
|
||||
'validator': bool,
|
||||
'requires_restart': True,
|
||||
},
|
||||
|
||||
# Settings for plugin mixin features
|
||||
'ENABLE_PLUGINS_URL': {
|
||||
'name': _('Enable URL integration'),
|
||||
@ -1075,6 +1102,7 @@ class InvenTreeSetting(BaseInvenTreeSetting):
|
||||
'validator': bool,
|
||||
'requires_restart': True,
|
||||
},
|
||||
|
||||
'ENABLE_PLUGINS_NAVIGATION': {
|
||||
'name': _('Enable navigation integration'),
|
||||
'description': _('Enable plugins to integrate into navigation'),
|
||||
@ -1082,6 +1110,7 @@ class InvenTreeSetting(BaseInvenTreeSetting):
|
||||
'validator': bool,
|
||||
'requires_restart': True,
|
||||
},
|
||||
|
||||
'ENABLE_PLUGINS_APP': {
|
||||
'name': _('Enable app integration'),
|
||||
'description': _('Enable plugins to add apps'),
|
||||
@ -1089,6 +1118,7 @@ class InvenTreeSetting(BaseInvenTreeSetting):
|
||||
'validator': bool,
|
||||
'requires_restart': True,
|
||||
},
|
||||
|
||||
'ENABLE_PLUGINS_SCHEDULE': {
|
||||
'name': _('Enable schedule integration'),
|
||||
'description': _('Enable plugins to run scheduled tasks'),
|
||||
@ -1096,6 +1126,7 @@ class InvenTreeSetting(BaseInvenTreeSetting):
|
||||
'validator': bool,
|
||||
'requires_restart': True,
|
||||
},
|
||||
|
||||
'ENABLE_PLUGINS_EVENTS': {
|
||||
'name': _('Enable event integration'),
|
||||
'description': _('Enable plugins to respond to internal events'),
|
||||
@ -1149,18 +1180,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'),
|
||||
@ -1174,78 +1208,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'),
|
||||
|
@ -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
|
||||
|
||||
|
@ -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):
|
||||
|
@ -1,5 +1,6 @@
|
||||
{% extends "skeleton.html" %}
|
||||
{% load static %}
|
||||
{% load inventree_extras %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block head %}
|
||||
|
@ -2413,7 +2413,7 @@ function showAllocationSubTable(index, row, element, options) {
|
||||
},
|
||||
{
|
||||
field: 'buttons',
|
||||
title: '{% trans "" %}',
|
||||
title: '',
|
||||
formatter: function(value, row, index, field) {
|
||||
|
||||
var html = `<div class='btn-group float-right' role='group'>`;
|
||||
|
Loading…
Reference in New Issue
Block a user