This commit is contained in:
Oliver 2021-07-30 11:26:53 +10:00
commit 0e59c15773
42 changed files with 436 additions and 152 deletions

2
.gitattributes vendored
View File

@ -7,5 +7,5 @@
*.yml text
*.yaml text
*.conf text
*.sh text
*.sh text eol=lf
*.js text

28
.github/workflows/javascript.yaml vendored Normal file
View File

@ -0,0 +1,28 @@
# Check javascript template files
name: Javascript Templates
on:
push:
branches:
- master
pull_request:
branches-ignore:
- l10*
jobs:
javascript:
runs-on: ubuntu-latest
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: Checkout Code
uses: actions/checkout@v2
- name: Check Files
run: |
cd ci
python check_js_templates.py

View File

@ -12,6 +12,7 @@ database setup in this file.
"""
import logging
import os
import random
import string
@ -202,7 +203,7 @@ STATICFILES_DIRS = [
# Translated Template settings
STATICFILES_I18_PREFIX = 'i18n'
STATICFILES_I18_SRC = os.path.join(BASE_DIR, 'templates', 'js')
STATICFILES_I18_SRC = os.path.join(BASE_DIR, 'templates', 'js', 'translated')
STATICFILES_I18_TRG = STATICFILES_DIRS[0] + '_' + STATICFILES_I18_PREFIX
STATICFILES_DIRS.append(STATICFILES_I18_TRG)
STATICFILES_I18_TRG = os.path.join(STATICFILES_I18_TRG, STATICFILES_I18_PREFIX)
@ -347,10 +348,22 @@ REST_FRAMEWORK = {
WSGI_APPLICATION = 'InvenTree.wsgi.application'
background_workers = os.environ.get('INVENTREE_BACKGROUND_WORKERS', None)
if background_workers is not None:
try:
background_workers = int(background_workers)
except ValueError:
background_workers = None
if background_workers is None:
# Sensible default?
background_workers = 4
# django-q configuration
Q_CLUSTER = {
'name': 'InvenTree',
'workers': 4,
'workers': background_workers,
'timeout': 90,
'retry': 120,
'queue_limit': 50,

View File

@ -93,28 +93,33 @@ settings_urls = [
url(r'^.*$', SettingsView.as_view(template_name='InvenTree/settings/settings.html'), name='settings'),
]
# Some javascript files are served 'dynamically', allowing them to pass through the Django translation layer
# These javascript files are served "dynamically" - i.e. rendered on demand
dynamic_javascript_urls = [
url(r'^api.js', DynamicJsView.as_view(template_name='js/api.js'), name='api.js'),
url(r'^attachment.js', DynamicJsView.as_view(template_name='js/attachment.js'), name='attachment.js'),
url(r'^barcode.js', DynamicJsView.as_view(template_name='js/barcode.js'), name='barcode.js'),
url(r'^bom.js', DynamicJsView.as_view(template_name='js/bom.js'), name='bom.js'),
url(r'^build.js', DynamicJsView.as_view(template_name='js/build.js'), name='build.js'),
url(r'^calendar.js', DynamicJsView.as_view(template_name='js/calendar.js'), name='calendar.js'),
url(r'^company.js', DynamicJsView.as_view(template_name='js/company.js'), name='company.js'),
url(r'^filters.js', DynamicJsView.as_view(template_name='js/filters.js'), name='filters.js'),
url(r'^forms.js', DynamicJsView.as_view(template_name='js/forms.js'), name='forms.js'),
url(r'^inventree.js', DynamicJsView.as_view(template_name='js/inventree.js'), name='inventree.js'),
url(r'^label.js', DynamicJsView.as_view(template_name='js/label.js'), name='label.js'),
url(r'^model_renderers.js', DynamicJsView.as_view(template_name='js/model_renderers.js'), name='model_renderers.js'),
url(r'^modals.js', DynamicJsView.as_view(template_name='js/modals.js'), name='modals.js'),
url(r'^nav.js', DynamicJsView.as_view(template_name='js/nav.js'), name='nav.js'),
url(r'^order.js', DynamicJsView.as_view(template_name='js/order.js'), name='order.js'),
url(r'^part.js', DynamicJsView.as_view(template_name='js/part.js'), name='part.js'),
url(r'^report.js', DynamicJsView.as_view(template_name='js/report.js'), name='report.js'),
url(r'^stock.js', DynamicJsView.as_view(template_name='js/stock.js'), name='stock.js'),
url(r'^tables.js', DynamicJsView.as_view(template_name='js/tables.js'), name='tables.js'),
url(r'^table_filters.js', DynamicJsView.as_view(template_name='js/table_filters.js'), name='table_filters.js'),
url(r'^inventree.js', DynamicJsView.as_view(template_name='js/dynamic/inventree.js'), name='inventree.js'),
url(r'^calendar.js', DynamicJsView.as_view(template_name='js/dynamic/calendar.js'), name='calendar.js'),
url(r'^nav.js', DynamicJsView.as_view(template_name='js/dynamic/nav.js'), name='nav.js'),
url(r'^settings.js', DynamicJsView.as_view(template_name='js/dynamic/settings.js'), name='settings.js'),
]
# These javascript files are pased through the Django translation layer
translated_javascript_urls = [
url(r'^api.js', DynamicJsView.as_view(template_name='js/translated/api.js'), name='api.js'),
url(r'^attachment.js', DynamicJsView.as_view(template_name='js/translated/attachment.js'), name='attachment.js'),
url(r'^barcode.js', DynamicJsView.as_view(template_name='js/translated/barcode.js'), name='barcode.js'),
url(r'^bom.js', DynamicJsView.as_view(template_name='js/translated/bom.js'), name='bom.js'),
url(r'^build.js', DynamicJsView.as_view(template_name='js/translated/build.js'), name='build.js'),
url(r'^company.js', DynamicJsView.as_view(template_name='js/translated/company.js'), name='company.js'),
url(r'^filters.js', DynamicJsView.as_view(template_name='js/translated/filters.js'), name='filters.js'),
url(r'^forms.js', DynamicJsView.as_view(template_name='js/translated/forms.js'), name='forms.js'),
url(r'^label.js', DynamicJsView.as_view(template_name='js/translated/label.js'), name='label.js'),
url(r'^model_renderers.js', DynamicJsView.as_view(template_name='js/translated/model_renderers.js'), name='model_renderers.js'),
url(r'^modals.js', DynamicJsView.as_view(template_name='js/translated/modals.js'), name='modals.js'),
url(r'^order.js', DynamicJsView.as_view(template_name='js/translated/order.js'), name='order.js'),
url(r'^part.js', DynamicJsView.as_view(template_name='js/translated/part.js'), name='part.js'),
url(r'^report.js', DynamicJsView.as_view(template_name='js/translated/report.js'), name='report.js'),
url(r'^stock.js', DynamicJsView.as_view(template_name='js/translated/stock.js'), name='stock.js'),
url(r'^tables.js', DynamicJsView.as_view(template_name='js/translated/tables.js'), name='tables.js'),
url(r'^table_filters.js', DynamicJsView.as_view(template_name='js/translated/table_filters.js'), name='table_filters.js'),
]
urlpatterns = [
@ -123,7 +128,8 @@ urlpatterns = [
url(r'^supplier-part/', include(supplier_part_urls)),
# "Dynamic" javascript files which are rendered using InvenTree templating.
url(r'^dynamic/', include(dynamic_javascript_urls)),
url(r'^js/dynamic/', include(dynamic_javascript_urls)),
url(r'^js/i18n/', include(translated_javascript_urls)),
url(r'^common/', include(common_urls)),

View File

@ -8,7 +8,7 @@ import re
import common.models
INVENTREE_SW_VERSION = "0.4.0"
INVENTREE_SW_VERSION = "0.4.1"
INVENTREE_API_VERSION = 8

View File

@ -82,7 +82,7 @@
},
{
success: function(response) {
var prefix = '{% settings_value "BUILDORDER_REFERENCE_PREFIX" %}';
var prefix = global_settings.BUILDORDER_REFERENCE_PREFIX;
for (var idx = 0; idx < response.length; idx++) {

View File

@ -38,6 +38,67 @@ class BaseInvenTreeSetting(models.Model):
class Meta:
abstract = True
@classmethod
def allValues(cls, user=None):
"""
Return a dict of "all" defined global settings.
This performs a single database lookup,
and then any settings which are not *in* the database
are assigned their default values
"""
keys = set()
settings = []
results = cls.objects.all()
if user is not None:
results = results.filter(user=user)
# Query the database
for setting in results:
settings.append({
"key": setting.key.upper(),
"value": setting.value
})
keys.add(setting.key.upper())
# Specify any "default" values which are not in the database
for key in cls.GLOBAL_SETTINGS.keys():
if key.upper() not in keys:
settings.append({
"key": key.upper(),
"value": cls.get_setting_default(key)
})
# Enforce javascript formatting
for idx, setting in enumerate(settings):
key = setting['key']
value = setting['value']
validator = cls.get_setting_validator(key)
# Convert to javascript compatible booleans
if cls.validator_is_bool(validator):
value = str(value).lower()
# Numerical values remain the same
elif cls.validator_is_int(validator):
pass
# Wrap strings with quotes
else:
value = f"'{value}'"
setting["value"] = value
return settings
@classmethod
def get_setting_name(cls, key):
"""
@ -368,13 +429,7 @@ class BaseInvenTreeSetting(models.Model):
validator = self.__class__.get_setting_validator(self.key)
if validator == bool:
return True
if type(validator) in [list, tuple]:
for v in validator:
if v == bool:
return True
return self.__class__.validator_is_bool(validator)
def as_bool(self):
"""
@ -385,6 +440,19 @@ class BaseInvenTreeSetting(models.Model):
return InvenTree.helpers.str2bool(self.value)
@classmethod
def validator_is_bool(cls, validator):
if validator == bool:
return True
if type(validator) in [list, tuple]:
for v in validator:
if v == bool:
return True
return False
def is_int(self):
"""
Check if the setting is required to be an integer value:
@ -392,6 +460,11 @@ class BaseInvenTreeSetting(models.Model):
validator = self.__class__.get_setting_validator(self.key)
return self.__class__.validator_is_int(validator)
@classmethod
def validator_is_int(cls, validator):
if validator == int:
return True

View File

@ -6,6 +6,8 @@ Provides a JSON API for the Company app
from __future__ import unicode_literals
from django_filters.rest_framework import DjangoFilterBackend
from django_filters import rest_framework as rest_filters
from rest_framework import filters
from rest_framework import generics
@ -84,6 +86,23 @@ class CompanyDetail(generics.RetrieveUpdateDestroyAPIView):
return queryset
class ManufacturerPartFilter(rest_filters.FilterSet):
"""
Custom API filters for the ManufacturerPart list endpoint.
"""
class Meta:
model = ManufacturerPart
fields = [
'manufacturer',
'MPN',
'part',
]
# Filter by 'active' status of linked part
active = rest_filters.BooleanFilter(field_name='part__active')
class ManufacturerPartList(generics.ListCreateAPIView):
""" API endpoint for list view of ManufacturerPart object
@ -98,6 +117,7 @@ class ManufacturerPartList(generics.ListCreateAPIView):
)
serializer_class = ManufacturerPartSerializer
filterset_class = ManufacturerPartFilter
def get_serializer(self, *args, **kwargs):
@ -115,45 +135,12 @@ class ManufacturerPartList(generics.ListCreateAPIView):
return self.serializer_class(*args, **kwargs)
def filter_queryset(self, queryset):
"""
Custom filtering for the queryset.
"""
queryset = super().filter_queryset(queryset)
params = self.request.query_params
# Filter by manufacturer
manufacturer = params.get('manufacturer', None)
if manufacturer is not None:
queryset = queryset.filter(manufacturer=manufacturer)
# Filter by parent part?
part = params.get('part', None)
if part is not None:
queryset = queryset.filter(part=part)
# Filter by 'active' status of the part?
active = params.get('active', None)
if active is not None:
active = str2bool(active)
queryset = queryset.filter(part__active=active)
return queryset
filter_backends = [
DjangoFilterBackend,
filters.SearchFilter,
filters.OrderingFilter,
]
filter_fields = [
]
search_fields = [
'manufacturer__name',
'description',

View File

@ -198,17 +198,16 @@
);
});
{% settings_value "INVENTREE_DOWNLOAD_FROM_URL" as allow_download %}
if (global_settings.INVENTREE_DOWNLOAD_FROM_URL) {
{% if allow_download %}
$('#company-image-url').click(function() {
launchModalForm(
'{% url "company-image-download" company.id %}',
{
reload: true,
}
)
});
{% endif %}
$('#company-image-url').click(function() {
launchModalForm(
'{% url "company-image-download" company.id %}',
{
reload: true,
}
)
});
}
{% endblock %}

View File

@ -164,7 +164,7 @@ $("#edit-order").click(function() {
constructForm('{% url "api-po-detail" order.pk %}', {
fields: {
reference: {
prefix: "{% settings_value 'PURCHASEORDER_REFERENCE_PREFIX' %}",
prefix: global_settings.PURCHASEORDER_REFERENCE_PREFIX,
},
{% if order.lines.count == 0 and order.status == PurchaseOrderStatus.PENDING %}
supplier: {

View File

@ -66,7 +66,7 @@
},
{
success: function(response) {
var prefix = '{% settings_value "PURCHASEORDER_REFERENCE_PREFIX" %}';
var prefix = global_settings.PURCHASEORDER_REFERENCE_PREFIX;
for (var idx = 0; idx < response.length; idx++) {

View File

@ -157,7 +157,7 @@ $("#edit-order").click(function() {
constructForm('{% url "api-so-detail" order.pk %}', {
fields: {
reference: {
prefix: "{% settings_value 'SALESORDER_REFERENCE_PREFIX' %}",
prefix: global_settings.SALESORDER_REFERENCE_PREFIX,
},
{% if order.lines.count == 0 and order.status == SalesOrderStatus.PENDING %}
customer: {

View File

@ -67,7 +67,7 @@
{
success: function(response) {
var prefix = '{% settings_value "SALESORDER_REFERENCE_PREFIX" %}';
var prefix = global_settings.SALESORDER_REFERENCE_PREFIX;
for (var idx = 0; idx < response.length; idx++) {
var order = response[idx];

View File

@ -394,17 +394,16 @@
{% if roles.part.change %}
{% settings_value "INVENTREE_DOWNLOAD_FROM_URL" as allow_download %}
{% if allow_download %}
$("#part-image-url").click(function() {
launchModalForm(
'{% url "part-image-download" part.id %}',
{
reload: true,
}
);
});
{% endif %}
if (global_settings.INVENTREE_DOWNLOAD_FROM_URL) {
$("#part-image-url").click(function() {
launchModalForm(
'{% url "part-image-download" part.id %}',
{
reload: true,
}
);
});
}
$("#part-image-select").click(function() {
launchModalForm("{% url 'part-image-select' part.id %}",

View File

@ -207,6 +207,24 @@ def settings_value(key, *args, **kwargs):
return InvenTreeSetting.get_setting(key)
@register.simple_tag()
def user_settings(user, *args, **kwargs):
"""
Return all USER settings as a key:value dict
"""
return InvenTreeUserSetting.allValues(user=user)
@register.simple_tag()
def global_settings(*args, **kwargs):
"""
Return all GLOBAL InvenTree settings as a key:value dict
"""
return InvenTreeSetting.allValues()
@register.simple_tag()
def get_color_theme_css(username):
try:

View File

@ -145,19 +145,22 @@
<script type='text/javascript' src="{% static 'script/inventree/notification.js' %}"></script>
<script type='text/javascript' src="{% static 'script/inventree/sidenav.js' %}"></script>
<!-- translated -->
<script type='text/javascript' src="{% i18n_static 'inventree.js' %}"></script>
<!-- dynamic javascript templates -->
<script type='text/javascript' src="{% url 'inventree.js' %}"></script>
<script type='text/javascript' src="{% url 'calendar.js' %}"></script>
<script type='text/javascript' src="{% url 'nav.js' %}"></script>
<script type='text/javascript' src="{% url 'settings.js' %}"></script>
<!-- translated javascript templates-->
<script type='text/javascript' src="{% i18n_static 'api.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'attachment.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'barcode.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'bom.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'build.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'calendar.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'company.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'filters.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'forms.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'label.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'nav.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'modals.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'model_renderers.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'order.js' %}"></script>

View File

@ -91,11 +91,7 @@ function inventreeDocReady() {
url: '/api/part/',
data: {
search: request.term,
{% if request.user %}
limit: {% settings_value 'SEARCH_PREVIEW_RESULTS' user=request.user %},
{% else %}
limit: 25,
{% endif %}
limit: user_settings.SEARCH_PREVIEW_RESULTS,
offset: 0
},
success: function (data) {

View File

@ -0,0 +1,17 @@
{% load inventree_extras %}
// InvenTree settings
{% user_settings request.user as USER_SETTINGS %}
{% global_settings as GLOBAL_SETTINGS %}
var user_settings = {
{% for setting in USER_SETTINGS %}
{{ setting.key }}: {{ setting.value|safe }},
{% endfor %}
};
var global_settings = {
{% for setting in GLOBAL_SETTINGS %}
{{ setting.key }}: {{ setting.value|safe }},
{% endfor %}
};

View File

@ -5,7 +5,7 @@
function buildFormFields() {
return {
reference: {
prefix: "{% settings_value 'BUILDORDER_REFERENCE_PREFIX' %}",
prefix: global_settings.BUILDORDER_REFERENCE_PREFIX,
},
title: {},
part: {},
@ -232,7 +232,7 @@ function loadBuildOrderAllocationTable(table, options={}) {
switchable: false,
title: '{% trans "Build Order" %}',
formatter: function(value, row) {
var prefix = "{% settings_value 'BUILDORDER_REFERENCE_PREFIX' %}";
var prefix = global_settings.BUILDORDER_REFERENCE_PREFIX;
var ref = `${prefix}${row.build_detail.reference}`;
@ -848,7 +848,7 @@ function loadBuildTable(table, options) {
switchable: true,
formatter: function(value, row, index, field) {
var prefix = "{% settings_value 'BUILDORDER_REFERENCE_PREFIX' %}";
var prefix = global_settings.BUILDORDER_REFERENCE_PREFIX;
if (prefix) {
value = `${prefix}${value}`;

View File

@ -9,7 +9,7 @@ function createSalesOrder(options={}) {
method: 'POST',
fields: {
reference: {
prefix: '{% settings_value "SALESORDER_REFERENCE_PREFIX" %}',
prefix: global_settings.SALESORDER_REFERENCE_PREFIX,
},
customer: {
value: options.customer,
@ -40,7 +40,7 @@ function createPurchaseOrder(options={}) {
method: 'POST',
fields: {
reference: {
prefix: "{% settings_value 'PURCHASEORDER_REFERENCE_PREFIX' %}",
prefix: global_settings.PURCHASEORDER_REFERENCE_PREFIX,
},
supplier: {
value: options.supplier,
@ -214,7 +214,7 @@ function loadPurchaseOrderTable(table, options) {
switchable: false,
formatter: function(value, row, index, field) {
var prefix = "{% settings_value 'PURCHASEORDER_REFERENCE_PREFIX' %}";
var prefix = global_settings.PURCHASEORDER_REFERENCE_PREFIX;
if (prefix) {
value = `${prefix}${value}`;
@ -309,7 +309,7 @@ function loadSalesOrderTable(table, options) {
title: '{% trans "Sales Order" %}',
formatter: function(value, row, index, field) {
var prefix = "{% settings_value 'SALESORDER_REFERENCE_PREFIX' %}";
var prefix = global_settings.SALESORDER_REFERENCE_PREFIX;
if (prefix) {
value = `${prefix}${value}`;
@ -423,7 +423,7 @@ function loadSalesOrderAllocationTable(table, options={}) {
switchable: false,
formatter: function(value, row) {
var prefix = "{% settings_value 'SALESORDER_REFERENCE_PREFIX' %}";
var prefix = global_settings.SALESORDER_REFERENCE_PREFIX;
var ref = `${prefix}${row.order_detail.reference}`;

View File

@ -6,8 +6,6 @@
* Requires api.js to be loaded first
*/
{% settings_value 'BARCODE_ENABLE' as barcodes %}
function stockStatusCodes() {
return [
{% for code in StockStatus.list %}
@ -704,8 +702,7 @@ function loadStockTable(table, options) {
name: 'stock',
original: original,
showColumns: true,
{% settings_value 'STOCK_GROUP_BY_PART' as group_by_part %}
{% if group_by_part %}
{% if False %}
groupByField: options.groupByField || 'part',
groupBy: grouping,
groupByFormatter: function(field, id, data) {
@ -1011,14 +1008,13 @@ function loadStockTable(table, options) {
title: '{% trans "Stocktake" %}',
sortable: true,
},
{% settings_value "STOCK_ENABLE_EXPIRY" as expiry %}
{% if expiry %}
{
field: 'expiry_date',
title: '{% trans "Expiry Date" %}',
sortable: true,
visible: global_settings.STOCK_ENABLE_EXPIRY,
switchable: global_settings.STOCK_ENABLE_EXPIRY,
},
{% endif %}
{
field: 'updated',
title: '{% trans "Last Updated" %}',
@ -1037,7 +1033,7 @@ function loadStockTable(table, options) {
if (row.purchase_order_reference) {
var prefix = '{% settings_value "PURCHASEORDER_REFERENCE_PREFIX" %}';
var prefix = global_settings.PURCHASEORDER_REFERENCE_PREFIX;
text = prefix + row.purchase_order_reference;
}
@ -1090,15 +1086,18 @@ function loadStockTable(table, options) {
}
*/
var buttons = [
'#stock-print-options',
'#stock-options',
];
if (global_settings.BARCODE_ENABLE) {
buttons.push('#stock-barcode-options');
}
linkButtonsToSelection(
table,
[
'#stock-print-options',
{% if barcodes %}
'#stock-barcode-options',
{% endif %}
'#stock-options',
]
buttons,
);
@ -1138,19 +1137,19 @@ function loadStockTable(table, options) {
printTestReports(items);
})
{% if barcodes %}
$('#multi-item-barcode-scan-into-location').click(function() {
var selections = $('#stock-table').bootstrapTable('getSelections');
if (global_settings.BARCODE_ENABLE) {
$('#multi-item-barcode-scan-into-location').click(function() {
var selections = $('#stock-table').bootstrapTable('getSelections');
var items = [];
var items = [];
selections.forEach(function(item) {
items.push(item.pk);
})
selections.forEach(function(item) {
items.push(item.pk);
})
scanItemsIntoLocation(items);
});
{% endif %}
scanItemsIntoLocation(items);
});
}
$('#multi-item-stocktake').click(function() {
stockAdjustment('count');

View File

@ -121,7 +121,8 @@ function getAvailableTableFilters(tableKey) {
// Filters for the "Stock" table
if (tableKey == 'stock') {
return {
var filters = {
active: {
type: 'bool',
title: '{% trans "Active parts" %}',
@ -147,19 +148,6 @@ function getAvailableTableFilters(tableKey) {
title: '{% trans "Depleted" %}',
description: '{% trans "Show stock items which are depleted" %}',
},
{% settings_value "STOCK_ENABLE_EXPIRY" as expiry %}
{% if expiry %}
expired: {
type: 'bool',
title: '{% trans "Expired" %}',
description: '{% trans "Show stock items which have expired" %}',
},
stale: {
type: 'bool',
title: '{% trans "Stale" %}',
description: '{% trans "Show stock which is close to expiring" %}',
},
{% endif %}
in_stock: {
type: 'bool',
title: '{% trans "In Stock" %}',
@ -216,6 +204,23 @@ function getAvailableTableFilters(tableKey) {
description: '{% trans "Show stock items which have a purchase price set" %}',
},
};
// Optional filters if stock expiry functionality is enabled
if (global_settings.STOCK_ENABLE_EXPIRY) {
filters.expired = {
type: 'bool',
title: '{% trans "Expired" %}',
description: '{% trans "Show stock items which have expired" %}',
};
filters.stale = {
type: 'bool',
title: '{% trans "Stale" %}',
description: '{% trans "Show stock which is close to expiring" %}',
};
}
return filters;
}
// Filters for the 'stock test' table

121
ci/check_js_templates.py Normal file
View File

@ -0,0 +1,121 @@
"""
Test that the "translated" javascript files to not contain template tags
which need to be determined at "run time".
This is because the "translated" javascript files are compiled into the "static" directory.
They should only contain template tags that render static information.
"""
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import sys
import re
import os
import pathlib
here = os.path.abspath(os.path.dirname(__file__))
template_dir = os.path.abspath(os.path.join(here, '..', 'InvenTree', 'templates'))
# We only care about the 'translated' files
js_i18n_dir = os.path.join(template_dir, 'js', 'translated')
js_dynamic_dir = os.path.join(template_dir, 'js', 'dynamic')
errors = 0
print("=================================")
print("Checking static javascript files:")
print("=================================")
def check_invalid_tag(data):
pattern = r"{%(\w+)"
err_count = 0
for idx, line in enumerate(data):
results = re.findall(pattern, line)
for result in results:
err_count += 1
print(f" - Error on line {idx+1}: %{{{result[0]}")
return err_count
def check_prohibited_tags(data):
allowed_tags = [
'if',
'elif',
'else',
'endif',
'for',
'endfor',
'trans',
'load',
'include',
'url',
]
pattern = r"{% (\w+)\s"
err_count = 0
has_trans = False
for idx, line in enumerate(data):
for tag in re.findall(pattern, line):
if tag not in allowed_tags:
print(f" > Line {idx+1} contains prohibited template tag '{tag}'")
err_count += 1
if tag == 'trans':
has_trans = True
if not has_trans:
print(f" > file is missing 'trans' tags")
err_count += 1
return err_count
for filename in pathlib.Path(js_i18n_dir).rglob('*.js'):
print(f"Checking file 'translated/{os.path.basename(filename)}':")
with open(filename, 'r') as js_file:
data = js_file.readlines()
errors += check_invalid_tag(data)
errors += check_prohibited_tags(data)
for filename in pathlib.Path(js_dynamic_dir).rglob('*.js'):
print(f"Checking file 'dynamic/{os.path.basename(filename)}':")
# Check that the 'dynamic' files do not contains any translated strings
with open(filename, 'r') as js_file:
data = js_file.readlines()
pattern = r'{% trans '
err_count = 0
for idx, line in enumerate(data):
results = re.findall(pattern, line)
if len(results) > 0:
errors += 1
print(f" > prohibited {{% trans %}} tag found at line {idx + 1}")
if errors > 0:
print(f"Found {errors} incorrect template tags")
sys.exit(errors)

View File

@ -27,6 +27,10 @@ ENV INVENTREE_MEDIA_ROOT="${INVENTREE_DATA_DIR}/media"
ENV INVENTREE_CONFIG_FILE="${INVENTREE_DATA_DIR}/config.yaml"
ENV INVENTREE_SECRET_KEY_FILE="${INVENTREE_DATA_DIR}/secret_key.txt"
# Worker configuration (can be altered by user)
ENV INVENTREE_GUNICORN_WORKERS="4"
ENV INVENTREE_BACKGROUND_WORKERS="4"
# Default web server port is 8000
ENV INVENTREE_WEB_PORT="8000"

View File

@ -1,6 +1,22 @@
import multiprocessing
import os
import logging
workers = multiprocessing.cpu_count() * 2 + 1
logger = logging.getLogger('inventree')
workers = os.environ.get('INVENTREE_GUNICORN_WORKERS', None)
if workers is not None:
try:
workers = int(workers)
except ValueError:
workers = None
if workers is None:
workers = multiprocessing.cpu_count() * 2 + 1
logger.info(f"Starting gunicorn server with {workers} workers")
max_requests = 1000
max_requests_jitter = 50