mirror of
https://github.com/inventree/InvenTree.git
synced 2025-08-03 02:21:34 +00:00
Merge branch 'master' of github.com:SchrodingersGat/InvenTree
This commit is contained in:
InvenTree
InvenTree
locale
cs
LC_MESSAGES
da
LC_MESSAGES
de
LC_MESSAGES
el
LC_MESSAGES
en
LC_MESSAGES
es
LC_MESSAGES
es_MX
LC_MESSAGES
fa
LC_MESSAGES
fr
LC_MESSAGES
he
LC_MESSAGES
hu
LC_MESSAGES
id
LC_MESSAGES
it
LC_MESSAGES
ja
LC_MESSAGES
ko
LC_MESSAGES
nl
LC_MESSAGES
no
LC_MESSAGES
pl
LC_MESSAGES
pt
LC_MESSAGES
pt_br
LC_MESSAGES
ru
LC_MESSAGES
sv
LC_MESSAGES
th
LC_MESSAGES
tr
LC_MESSAGES
vi
LC_MESSAGES
zh
LC_MESSAGES
plugin
templates
js
translated
@@ -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):
|
||||
|
@@ -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
|
||||
|
||||
|
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."""
|
||||
|
||||
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)
|
||||
|
@@ -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."""
|
||||
|
@@ -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):
|
||||
|
@@ -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 = `<div class='alert alert-block alert-${style}'>`;
|
||||
@@ -179,7 +198,10 @@ function showBarcodeMessage(modal, message, style='danger') {
|
||||
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* 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,
|
||||
|
@@ -812,7 +812,7 @@ function loadBomTable(table, options={}) {
|
||||
// Part column
|
||||
cols.push(
|
||||
{
|
||||
field: 'sub_part_detail.full_name',
|
||||
field: 'sub_part',
|
||||
title: '{% trans "Part" %}',
|
||||
sortable: true,
|
||||
switchable: false,
|
||||
@@ -1194,12 +1194,15 @@ function loadBomTable(table, options={}) {
|
||||
response[idx].parentId = bom_pk;
|
||||
}
|
||||
|
||||
var row = $(table).bootstrapTable('getRowByUniqueId', bom_pk);
|
||||
var row = table.bootstrapTable('getRowByUniqueId', bom_pk);
|
||||
row.sub_assembly_received = true;
|
||||
|
||||
$(table).bootstrapTable('updateByUniqueId', bom_pk, row, true);
|
||||
table.bootstrapTable('updateByUniqueId', bom_pk, row, true);
|
||||
|
||||
table.bootstrapTable('append', response);
|
||||
|
||||
// Auto-expand the newly added row
|
||||
$(`.treegrid-${bom_pk}`).treegrid('expand');
|
||||
},
|
||||
error: function(xhr) {
|
||||
console.error('Error requesting BOM for part=' + part_pk);
|
||||
@@ -1252,28 +1255,39 @@ function loadBomTable(table, options={}) {
|
||||
|
||||
table.treegrid({
|
||||
treeColumn: 1,
|
||||
onExpand: function() {
|
||||
}
|
||||
});
|
||||
|
||||
table.treegrid('collapseAll');
|
||||
|
||||
// Callback for 'load sub assembly' button
|
||||
$(table).find('.load-sub-assembly').click(function(event) {
|
||||
table.find('.load-sub-assembly').click(function(event) {
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
var pk = $(this).attr('pk');
|
||||
var row = $(table).bootstrapTable('getRowByUniqueId', pk);
|
||||
var row = table.bootstrapTable('getRowByUniqueId', pk);
|
||||
|
||||
// Request BOM data for this subassembly
|
||||
requestSubItems(row.pk, row.sub_part);
|
||||
|
||||
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) {
|
||||
table.bootstrapTable('uncheckAll');
|
||||
}
|
||||
|
Reference in New Issue
Block a user