mirror of
https://github.com/inventree/InvenTree.git
synced 2025-12-28 23:08:03 +00:00
Merge branch 'master' of github.com:SchrodingersGat/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
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function barcodeScanDialog() {
|
|
||||||
/*
|
/*
|
||||||
* Perform a barcode scan,
|
* Perform a barcode scan,
|
||||||
* and (potentially) redirect the browser
|
* and (potentially) redirect the browser
|
||||||
*/
|
*/
|
||||||
|
function barcodeScanDialog() {
|
||||||
|
|
||||||
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,
|
||||||
|
|||||||
@@ -812,7 +812,7 @@ function loadBomTable(table, options={}) {
|
|||||||
// Part column
|
// Part column
|
||||||
cols.push(
|
cols.push(
|
||||||
{
|
{
|
||||||
field: 'sub_part_detail.full_name',
|
field: 'sub_part',
|
||||||
title: '{% trans "Part" %}',
|
title: '{% trans "Part" %}',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
switchable: false,
|
switchable: false,
|
||||||
@@ -1194,12 +1194,15 @@ function loadBomTable(table, options={}) {
|
|||||||
response[idx].parentId = bom_pk;
|
response[idx].parentId = bom_pk;
|
||||||
}
|
}
|
||||||
|
|
||||||
var row = $(table).bootstrapTable('getRowByUniqueId', bom_pk);
|
var row = table.bootstrapTable('getRowByUniqueId', bom_pk);
|
||||||
row.sub_assembly_received = true;
|
row.sub_assembly_received = true;
|
||||||
|
|
||||||
$(table).bootstrapTable('updateByUniqueId', bom_pk, row, true);
|
table.bootstrapTable('updateByUniqueId', bom_pk, row, true);
|
||||||
|
|
||||||
table.bootstrapTable('append', response);
|
table.bootstrapTable('append', response);
|
||||||
|
|
||||||
|
// Auto-expand the newly added row
|
||||||
|
$(`.treegrid-${bom_pk}`).treegrid('expand');
|
||||||
},
|
},
|
||||||
error: function(xhr) {
|
error: function(xhr) {
|
||||||
console.error('Error requesting BOM for part=' + part_pk);
|
console.error('Error requesting BOM for part=' + part_pk);
|
||||||
@@ -1252,28 +1255,39 @@ function loadBomTable(table, options={}) {
|
|||||||
|
|
||||||
table.treegrid({
|
table.treegrid({
|
||||||
treeColumn: 1,
|
treeColumn: 1,
|
||||||
onExpand: function() {
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
table.treegrid('collapseAll');
|
table.treegrid('collapseAll');
|
||||||
|
|
||||||
// Callback for 'load sub assembly' button
|
// Callback for 'load sub assembly' button
|
||||||
$(table).find('.load-sub-assembly').click(function(event) {
|
table.find('.load-sub-assembly').click(function(event) {
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
var pk = $(this).attr('pk');
|
var pk = $(this).attr('pk');
|
||||||
var row = $(table).bootstrapTable('getRowByUniqueId', pk);
|
var row = table.bootstrapTable('getRowByUniqueId', pk);
|
||||||
|
|
||||||
// Request BOM data for this subassembly
|
// Request BOM data for this subassembly
|
||||||
requestSubItems(row.pk, row.sub_part);
|
requestSubItems(row.pk, row.sub_part);
|
||||||
|
|
||||||
row.sub_assembly_requested = true;
|
row.sub_assembly_requested = true;
|
||||||
$(table).bootstrapTable('updateByUniqueId', pk, row, true);
|
table.bootstrapTable('updateByUniqueId', pk, row, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var data = table.bootstrapTable('getData');
|
||||||
|
|
||||||
|
for (var idx = 0; idx < data.length; idx++) {
|
||||||
|
var row = data[idx];
|
||||||
|
|
||||||
|
if (!row.parentId) {
|
||||||
|
row.parentId = parent_id;
|
||||||
|
|
||||||
|
table.bootstrapTable('updateByUniqueId', row.pk, row, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onLoadSuccess: function() {
|
onLoadSuccess: function(data) {
|
||||||
|
|
||||||
if (options.editable) {
|
if (options.editable) {
|
||||||
table.bootstrapTable('uncheckAll');
|
table.bootstrapTable('uncheckAll');
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user