From dce10072efe14c6941377fc76ffad5ac434541d7 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Wed, 28 Sep 2022 00:53:22 +0200 Subject: [PATCH 1/3] Add typecasting to certain settings (#3726) * [FR] Add typecasting to certain settings Fixes #3725 Add typecasting * Add types to: - INVENTREE_CACHE_PORT - INVENTREE_EMAIL_PORT - INVENTREE_LOGIN_CONFIRM_DAYS - INVENTREE_LOGIN_ATTEMPTS * cast DB_PORT to int * Add logging statements --- InvenTree/InvenTree/config.py | 19 ++++++++++++++----- InvenTree/InvenTree/settings.py | 14 ++++++++++---- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/InvenTree/InvenTree/config.py b/InvenTree/InvenTree/config.py index 7ce162bdff..4c95e84970 100644 --- a/InvenTree/InvenTree/config.py +++ b/InvenTree/InvenTree/config.py @@ -58,7 +58,7 @@ def load_config_data() -> map: return data -def get_setting(env_var=None, config_key=None, default_value=None): +def get_setting(env_var=None, config_key=None, default_value=None, typecast=None): """Helper function for retrieving a configuration setting value. - First preference is to look for the environment variable @@ -69,15 +69,24 @@ def get_setting(env_var=None, config_key=None, default_value=None): env_var: Name of the environment variable e.g. 'INVENTREE_STATIC_ROOT' config_key: Key to lookup in the configuration file default_value: Value to return if first two options are not provided - + typecast: Function to use for typecasting the value """ + def try_typecasting(value): + """Attempt to typecast the value""" + if typecast is not None: + # Try to typecast the value + try: + return typecast(value) + except Exception as error: + logger.error(f"Failed to typecast '{env_var}' with value '{value}' to type '{typecast}' with error {error}") + return value # First, try to load from the environment variables if env_var is not None: val = os.getenv(env_var, None) if val is not None: - return val + return try_typecasting(val) # Next, try to load from configuration file if config_key is not None: @@ -96,10 +105,10 @@ def get_setting(env_var=None, config_key=None, default_value=None): cfg_data = cfg_data[key] if result is not None: - return result + return try_typecasting(result) # Finally, return the default value - return default_value + return try_typecasting(default_value) def get_boolean_setting(env_var=None, config_key=None, default_value=False): diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 977a481269..c14d957147 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -346,6 +346,12 @@ for key in db_keys: env_var = os.environ.get(env_key, None) if env_var: + # Make use PORT is int + if key == 'PORT': + try: + env_var = int(env_var) + except ValueError: + logger.error(f"Invalid number for {env_key}: {env_var}") # Override configuration value db_config[key] = env_var @@ -503,7 +509,7 @@ DATABASES = { # Cache configuration cache_host = get_setting('INVENTREE_CACHE_HOST', 'cache.host', None) -cache_port = get_setting('INVENTREE_CACHE_PORT', 'cache.port', '6379') +cache_port = get_setting('INVENTREE_CACHE_PORT', 'cache.port', '6379', typecast=int) if cache_host: # pragma: no cover # We are going to rely upon a possibly non-localhost for our cache, @@ -670,7 +676,7 @@ EXCHANGE_BACKEND = 'InvenTree.exchange.InvenTreeExchange' # Email configuration options EMAIL_BACKEND = get_setting('INVENTREE_EMAIL_BACKEND', 'email.backend', 'django.core.mail.backends.smtp.EmailBackend') EMAIL_HOST = get_setting('INVENTREE_EMAIL_HOST', 'email.host', '') -EMAIL_PORT = int(get_setting('INVENTREE_EMAIL_PORT', 'email.port', 25)) +EMAIL_PORT = get_setting('INVENTREE_EMAIL_PORT', 'email.port', 25, typecast=int) EMAIL_HOST_USER = get_setting('INVENTREE_EMAIL_USERNAME', 'email.username', '') EMAIL_HOST_PASSWORD = get_setting('INVENTREE_EMAIL_PASSWORD', 'email.password', '') EMAIL_SUBJECT_PREFIX = get_setting('INVENTREE_EMAIL_PREFIX', 'email.prefix', '[InvenTree] ') @@ -719,8 +725,8 @@ SOCIALACCOUNT_PROVIDERS = CONFIG.get('social_providers', []) SOCIALACCOUNT_STORE_TOKENS = True # settings for allauth -ACCOUNT_EMAIL_CONFIRMATION_EXPIRE_DAYS = get_setting('INVENTREE_LOGIN_CONFIRM_DAYS', 'login_confirm_days', 3) -ACCOUNT_LOGIN_ATTEMPTS_LIMIT = get_setting('INVENTREE_LOGIN_ATTEMPTS', 'login_attempts', 5) +ACCOUNT_EMAIL_CONFIRMATION_EXPIRE_DAYS = get_setting('INVENTREE_LOGIN_CONFIRM_DAYS', 'login_confirm_days', 3, typecast=int) +ACCOUNT_LOGIN_ATTEMPTS_LIMIT = get_setting('INVENTREE_LOGIN_ATTEMPTS', 'login_attempts', 5, typecast=int) ACCOUNT_LOGOUT_ON_PASSWORD_CHANGE = True ACCOUNT_PREVENT_ENUMERATION = True From 7568d2367030996987be028daf0071a2ce853817 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Wed, 28 Sep 2022 00:56:03 +0200 Subject: [PATCH 2/3] APIMixin: Add flag for passing data as json (#3635) * out-of-scope: Add flag for passing data as json * switch to json/data for APImixin * fix testing sytax * add missing coverage * fix test --- InvenTree/plugin/base/integration/mixins.py | 15 +++++++++---- .../plugin/base/integration/test_mixins.py | 21 ++++++++++++++++++- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/InvenTree/plugin/base/integration/mixins.py b/InvenTree/plugin/base/integration/mixins.py index 7a14a9a890..6a18216209 100644 --- a/InvenTree/plugin/base/integration/mixins.py +++ b/InvenTree/plugin/base/integration/mixins.py @@ -1,6 +1,6 @@ """Plugin mixin classes.""" -import json +import json as json_pkg import logging from django.db.utils import OperationalError, ProgrammingError @@ -413,7 +413,7 @@ class APICallMixin: groups.append(f'{key}={",".join([str(a) for a in val])}') return f'?{"&".join(groups)}' - def api_call(self, endpoint: str, method: str = 'GET', url_args: dict = None, data=None, headers: dict = None, simple_response: bool = True, endpoint_is_url: bool = False): + def api_call(self, endpoint: str, method: str = 'GET', url_args: dict = None, data=None, json=None, headers: dict = None, simple_response: bool = True, endpoint_is_url: bool = False): """Do an API call. Simplest call example: @@ -426,7 +426,8 @@ class APICallMixin: endpoint (str): Path to current endpoint. Either the endpoint or the full or if the flag is set method (str, optional): HTTP method that should be uses - capitalized. Defaults to 'GET'. url_args (dict, optional): arguments that should be appended to the url. Defaults to None. - data (Any, optional): Data that should be transmitted in the body - must be JSON serializable. Defaults to None. + data (Any, optional): Data that should be transmitted in the body - url-encoded. Defaults to None. + json (Any, optional): Data that should be transmitted in the body - must be JSON serializable. Defaults to None. headers (dict, optional): Headers that should be used for the request. Defaults to self.api_headers. simple_response (bool, optional): Return the response as JSON. Defaults to True. endpoint_is_url (bool, optional): The provided endpoint is the full url - do not use self.api_url as base. Defaults to False. @@ -455,8 +456,14 @@ class APICallMixin: 'headers': headers, } + if data and json: + raise ValueError('You can either pass `data` or `json` to this function.') + + if json: + kwargs['data'] = json_pkg.dumps(json) + if data: - kwargs['data'] = json.dumps(data) + kwargs['data'] = data # run command response = requests.request(method, **kwargs) diff --git a/InvenTree/plugin/base/integration/test_mixins.py b/InvenTree/plugin/base/integration/test_mixins.py index 3f95d56694..d48b0ddc30 100644 --- a/InvenTree/plugin/base/integration/test_mixins.py +++ b/InvenTree/plugin/base/integration/test_mixins.py @@ -258,7 +258,7 @@ class APICallMixinTest(BaseMixinDefinition, TestCase): # api_call with post and data result = self.mixin.api_call( 'https://reqres.in/api/users/', - data={"name": "morpheus", "job": "leader"}, + json={"name": "morpheus", "job": "leader"}, method='POST', endpoint_is_url=True, ) @@ -280,6 +280,25 @@ class APICallMixinTest(BaseMixinDefinition, TestCase): with self.assertRaises(MixinNotImplementedError): self.mixin_wrong2.has_api_call() + # Too many data arguments + with self.assertRaises(ValueError): + self.mixin.api_call( + 'https://reqres.in/api/users/', + json={"a": 1, }, data={"a": 1}, + ) + + # Sending a request with a wrong data format should result in 40 + result = self.mixin.api_call( + 'https://reqres.in/api/users/', + data={"name": "morpheus", "job": "leader"}, + method='POST', + endpoint_is_url=True, + simple_response=False + ) + + self.assertEqual(result.status_code, 400) + self.assertIn('Bad Request', str(result.content)) + class PanelMixinTests(InvenTreeTestCase): """Test that the PanelMixin plugin operates correctly.""" From 8bcf72fbb2d0f7285cd21a96a03d32703097429f Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 28 Sep 2022 10:28:39 +1000 Subject: [PATCH 3/3] Barcode scan fix (#3727) * Fix barcode scanning in web interface * Improve error handling for barcode scan dialog * JS linting --- .../builtin/barcodes/inventree_barcode.py | 18 ++++++-- InvenTree/templates/js/translated/barcode.js | 46 ++++++++++++++----- 2 files changed, 48 insertions(+), 16 deletions(-) diff --git a/InvenTree/plugin/builtin/barcodes/inventree_barcode.py b/InvenTree/plugin/builtin/barcodes/inventree_barcode.py index 1b7594870e..4fbe20ca27 100644 --- a/InvenTree/plugin/builtin/barcodes/inventree_barcode.py +++ b/InvenTree/plugin/builtin/barcodes/inventree_barcode.py @@ -34,19 +34,29 @@ class InvenTreeBarcodePlugin(BarcodeMixin, InvenTreePlugin): def format_matched_response(self, label, model, instance): """Format a response for the scanned data""" - response = { + data = { 'pk': instance.pk } # Add in the API URL if available if hasattr(model, 'get_api_url'): - response['api_url'] = f"{model.get_api_url()}{instance.pk}/" + data['api_url'] = f"{model.get_api_url()}{instance.pk}/" # Add in the web URL if available if hasattr(instance, 'get_absolute_url'): - response['web_url'] = instance.get_absolute_url() + url = instance.get_absolute_url() + data['web_url'] = url + else: + url = None - return {label: response} + response = { + label: data + } + + if url is not None: + response['url'] = url + + return response class InvenTreeInternalBarcodePlugin(InvenTreeBarcodePlugin): diff --git a/InvenTree/templates/js/translated/barcode.js b/InvenTree/templates/js/translated/barcode.js index 818be0106b..de0105798d 100644 --- a/InvenTree/templates/js/translated/barcode.js +++ b/InvenTree/templates/js/translated/barcode.js @@ -129,9 +129,25 @@ function postBarcodeData(barcode_data, options={}) { data, { method: 'POST', - error: function() { + error: function(xhr) { + enableBarcodeInput(modal, true); - showBarcodeMessage(modal, '{% trans "Server error" %}'); + + switch (xhr.status || 0) { + case 400: + // No match for barcode, most likely + console.log(xhr); + + data = xhr.responseJSON || {}; + showBarcodeMessage(modal, data.error || '{% trans "Server error" %}'); + + break; + default: + // Any other error code means something went wrong + $(modal).modal('hide'); + + showApiError(xhr, url); + } }, success: function(response, status) { modalEnable(modal, false); @@ -166,6 +182,9 @@ function postBarcodeData(barcode_data, options={}) { } +/* + * Display a message within the barcode scanning dialog + */ function showBarcodeMessage(modal, message, style='danger') { var html = `
`; @@ -179,7 +198,10 @@ function showBarcodeMessage(modal, message, style='danger') { function showInvalidResponseError(modal, response, status) { - showBarcodeMessage(modal, `{% trans "Invalid server response" %}
{% trans "Status" %}: '${status}'`); + showBarcodeMessage( + modal, + `{% trans "Invalid server response" %}
{% trans "Status" %}: '${status}'` + ); } @@ -320,12 +342,11 @@ function barcodeDialog(title, options={}) { $(modal).modal('show'); } - +/* +* Perform a barcode scan, +* and (potentially) redirect the browser +*/ function barcodeScanDialog() { - /* - * Perform a barcode scan, - * and (potentially) redirect the browser - */ var modal = '#modal-form'; @@ -333,11 +354,12 @@ function barcodeScanDialog() { '{% trans "Scan Barcode" %}', { onScan: function(response) { - if ('url' in response) { - $(modal).modal('hide'); - // Redirect to the URL! - window.location.href = response.url; + var url = response.url; + + if (url) { + $(modal).modal('hide'); + window.location.href = url; } else { showBarcodeMessage( modal,