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:
@ -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):
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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."""
|
||||||
|
@ -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):
|
||||||
|
@ -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,
|
||||||
|
Reference in New Issue
Block a user