2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-15 19:45:46 +00:00

Merge branch 'master' of github.com:inventree/InvenTree

This commit is contained in:
Oliver
2022-09-28 13:00:51 +10:00
6 changed files with 103 additions and 30 deletions

View File

@ -58,7 +58,7 @@ def load_config_data() -> map:
return data 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. """Helper function for retrieving a configuration setting value.
- First preference is to look for the environment variable - 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' env_var: Name of the environment variable e.g. 'INVENTREE_STATIC_ROOT'
config_key: Key to lookup in the configuration file config_key: Key to lookup in the configuration file
default_value: Value to return if first two options are not provided 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 # First, try to load from the environment variables
if env_var is not None: if env_var is not None:
val = os.getenv(env_var, None) val = os.getenv(env_var, None)
if val is not None: if val is not None:
return val return try_typecasting(val)
# Next, try to load from configuration file # Next, try to load from configuration file
if config_key is not None: 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] cfg_data = cfg_data[key]
if result is not None: if result is not None:
return result return try_typecasting(result)
# Finally, return the default value # 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): def get_boolean_setting(env_var=None, config_key=None, default_value=False):

View File

@ -346,6 +346,12 @@ for key in db_keys:
env_var = os.environ.get(env_key, None) env_var = os.environ.get(env_key, None)
if env_var: 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 # Override configuration value
db_config[key] = env_var db_config[key] = env_var
@ -503,7 +509,7 @@ DATABASES = {
# Cache configuration # Cache configuration
cache_host = get_setting('INVENTREE_CACHE_HOST', 'cache.host', None) 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 if cache_host: # pragma: no cover
# We are going to rely upon a possibly non-localhost for our cache, # 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 configuration options
EMAIL_BACKEND = get_setting('INVENTREE_EMAIL_BACKEND', 'email.backend', 'django.core.mail.backends.smtp.EmailBackend') 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_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_USER = get_setting('INVENTREE_EMAIL_USERNAME', 'email.username', '')
EMAIL_HOST_PASSWORD = get_setting('INVENTREE_EMAIL_PASSWORD', 'email.password', '') EMAIL_HOST_PASSWORD = get_setting('INVENTREE_EMAIL_PASSWORD', 'email.password', '')
EMAIL_SUBJECT_PREFIX = get_setting('INVENTREE_EMAIL_PREFIX', 'email.prefix', '[InvenTree] ') 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 SOCIALACCOUNT_STORE_TOKENS = True
# settings for allauth # settings for allauth
ACCOUNT_EMAIL_CONFIRMATION_EXPIRE_DAYS = get_setting('INVENTREE_LOGIN_CONFIRM_DAYS', 'login_confirm_days', 3) 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) ACCOUNT_LOGIN_ATTEMPTS_LIMIT = get_setting('INVENTREE_LOGIN_ATTEMPTS', 'login_attempts', 5, typecast=int)
ACCOUNT_LOGOUT_ON_PASSWORD_CHANGE = True ACCOUNT_LOGOUT_ON_PASSWORD_CHANGE = True
ACCOUNT_PREVENT_ENUMERATION = True ACCOUNT_PREVENT_ENUMERATION = True

View File

@ -1,6 +1,6 @@
"""Plugin mixin classes.""" """Plugin mixin classes."""
import json import json as json_pkg
import logging import logging
from django.db.utils import OperationalError, ProgrammingError from django.db.utils import OperationalError, ProgrammingError
@ -413,7 +413,7 @@ class APICallMixin:
groups.append(f'{key}={",".join([str(a) for a in val])}') groups.append(f'{key}={",".join([str(a) for a in val])}')
return f'?{"&".join(groups)}' 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. """Do an API call.
Simplest call example: 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 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'. 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. 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. 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. 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. 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, '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: if data:
kwargs['data'] = json.dumps(data) kwargs['data'] = data
# run command # run command
response = requests.request(method, **kwargs) response = requests.request(method, **kwargs)

View File

@ -258,7 +258,7 @@ class APICallMixinTest(BaseMixinDefinition, TestCase):
# api_call with post and data # api_call with post and data
result = self.mixin.api_call( result = self.mixin.api_call(
'https://reqres.in/api/users/', 'https://reqres.in/api/users/',
data={"name": "morpheus", "job": "leader"}, json={"name": "morpheus", "job": "leader"},
method='POST', method='POST',
endpoint_is_url=True, endpoint_is_url=True,
) )
@ -280,6 +280,25 @@ class APICallMixinTest(BaseMixinDefinition, TestCase):
with self.assertRaises(MixinNotImplementedError): with self.assertRaises(MixinNotImplementedError):
self.mixin_wrong2.has_api_call() 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): class PanelMixinTests(InvenTreeTestCase):
"""Test that the PanelMixin plugin operates correctly.""" """Test that the PanelMixin plugin operates correctly."""

View File

@ -34,19 +34,29 @@ class InvenTreeBarcodePlugin(BarcodeMixin, InvenTreePlugin):
def format_matched_response(self, label, model, instance): def format_matched_response(self, label, model, instance):
"""Format a response for the scanned data""" """Format a response for the scanned data"""
response = { data = {
'pk': instance.pk 'pk': instance.pk
} }
# Add in the API URL if available # Add in the API URL if available
if hasattr(model, 'get_api_url'): 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 # Add in the web URL if available
if hasattr(instance, 'get_absolute_url'): 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): class InvenTreeInternalBarcodePlugin(InvenTreeBarcodePlugin):

View File

@ -129,9 +129,25 @@ function postBarcodeData(barcode_data, options={}) {
data, data,
{ {
method: 'POST', method: 'POST',
error: function() { error: function(xhr) {
enableBarcodeInput(modal, true); 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) { success: function(response, status) {
modalEnable(modal, false); 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') { function showBarcodeMessage(modal, message, style='danger') {
var html = `<div class='alert alert-block alert-${style}'>`; var html = `<div class='alert alert-block alert-${style}'>`;
@ -179,7 +198,10 @@ function showBarcodeMessage(modal, message, style='danger') {
function showInvalidResponseError(modal, response, status) { function showInvalidResponseError(modal, response, status) {
showBarcodeMessage(modal, `{% trans "Invalid server response" %}<br>{% trans "Status" %}: '${status}'`); showBarcodeMessage(
modal,
`{% trans "Invalid server response" %}<br>{% trans "Status" %}: '${status}'`
);
} }
@ -320,12 +342,11 @@ function barcodeDialog(title, options={}) {
$(modal).modal('show'); $(modal).modal('show');
} }
/*
* Perform a barcode scan,
* and (potentially) redirect the browser
*/
function barcodeScanDialog() { function barcodeScanDialog() {
/*
* Perform a barcode scan,
* and (potentially) redirect the browser
*/
var modal = '#modal-form'; var modal = '#modal-form';
@ -333,11 +354,12 @@ function barcodeScanDialog() {
'{% trans "Scan Barcode" %}', '{% trans "Scan Barcode" %}',
{ {
onScan: function(response) { onScan: function(response) {
if ('url' in response) {
$(modal).modal('hide');
// Redirect to the URL! var url = response.url;
window.location.href = response.url;
if (url) {
$(modal).modal('hide');
window.location.href = url;
} else { } else {
showBarcodeMessage( showBarcodeMessage(
modal, modal,