2
0
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:
Oliver Walters
2022-09-29 21:17:22 +10:00
33 changed files with 17224 additions and 16495 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

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

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');
} }
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,

View File

@@ -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');
} }