diff --git a/.github/workflows/translations.yml b/.github/workflows/translations.yml index 24106c028e..108bee2132 100644 --- a/.github/workflows/translations.yml +++ b/.github/workflows/translations.yml @@ -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 diff --git a/.gitignore b/.gitignore index 6532442dc7..56d4180482 100644 --- a/.gitignore +++ b/.gitignore @@ -85,3 +85,6 @@ maintenance_mode_state.txt # plugin dev directory plugins/ + +# Compiled translation files +*.mo diff --git a/InvenTree/build/migrations/0029_auto_20210601_1525.py b/InvenTree/build/migrations/0029_auto_20210601_1525.py index 12ec66960c..5470416dcd 100644 --- a/InvenTree/build/migrations/0029_auto_20210601_1525.py +++ b/InvenTree/build/migrations/0029_auto_20210601_1525.py @@ -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: diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index f1cd8bc09a..89febf3713 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -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'), diff --git a/InvenTree/common/serializers.py b/InvenTree/common/serializers.py index 71ccac8a4d..86d45cd881 100644 --- a/InvenTree/common/serializers.py +++ b/InvenTree/common/serializers.py @@ -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 diff --git a/InvenTree/common/tests.py b/InvenTree/common/tests.py index c3ce4f9e51..e2966fb8d5 100644 --- a/InvenTree/common/tests.py +++ b/InvenTree/common/tests.py @@ -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): diff --git a/InvenTree/templates/503.html b/InvenTree/templates/503.html index 7b5b25d611..606fc03ff0 100644 --- a/InvenTree/templates/503.html +++ b/InvenTree/templates/503.html @@ -1,5 +1,6 @@ {% extends "skeleton.html" %} {% load static %} +{% load inventree_extras %} {% load i18n %} {% block head %} diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js index c971a4d694..4aad54a65a 100644 --- a/InvenTree/templates/js/translated/order.js +++ b/InvenTree/templates/js/translated/order.js @@ -2413,7 +2413,7 @@ function showAllocationSubTable(index, row, element, options) { }, { field: 'buttons', - title: '{% trans "" %}', + title: '', formatter: function(value, row, index, field) { var html = `
`; diff --git a/tasks.py b/tasks.py index 34528e2609..0578f69acd 100644 --- a/tasks.py +++ b/tasks.py @@ -236,7 +236,7 @@ def translate(c): manage(c, "compilemessages") -@task(pre=[install, migrate, translate_stats, static, clean_settings]) +@task(pre=[install, migrate, translate, static, clean_settings]) def update(c): """ Update InvenTree installation.