mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge branch 'inventree:master' into matmair/issue2279
This commit is contained in:
commit
a540a5582e
@ -27,6 +27,7 @@ from stock import models as stock_models
|
|||||||
from company.models import Company, SupplierPart
|
from company.models import Company, SupplierPart
|
||||||
from plugin.events import trigger_event
|
from plugin.events import trigger_event
|
||||||
|
|
||||||
|
import InvenTree.helpers
|
||||||
from InvenTree.fields import InvenTreeModelMoneyField, RoundingDecimalField
|
from InvenTree.fields import InvenTreeModelMoneyField, RoundingDecimalField
|
||||||
from InvenTree.helpers import decimal2string, increment, getSetting
|
from InvenTree.helpers import decimal2string, increment, getSetting
|
||||||
from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus, StockStatus, StockHistoryCode
|
from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus, StockStatus, StockHistoryCode
|
||||||
@ -414,16 +415,12 @@ class PurchaseOrder(Order):
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if not (quantity % 1 == 0):
|
|
||||||
raise ValidationError({
|
|
||||||
"quantity": _("Quantity must be an integer")
|
|
||||||
})
|
|
||||||
if quantity < 0:
|
if quantity < 0:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
"quantity": _("Quantity must be a positive number")
|
"quantity": _("Quantity must be a positive number")
|
||||||
})
|
})
|
||||||
quantity = int(quantity)
|
quantity = InvenTree.helpers.clean_decimal(quantity)
|
||||||
except (ValueError, TypeError):
|
except TypeError:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
"quantity": _("Invalid quantity provided")
|
"quantity": _("Invalid quantity provided")
|
||||||
})
|
})
|
||||||
|
@ -451,8 +451,17 @@ class I18nStaticNode(StaticNode):
|
|||||||
replaces a variable named *lng* in the path with the current language
|
replaces a variable named *lng* in the path with the current language
|
||||||
"""
|
"""
|
||||||
def render(self, context):
|
def render(self, context):
|
||||||
self.path.var = self.path.var.format(lng=context.request.LANGUAGE_CODE)
|
|
||||||
|
self.original = getattr(self, 'original', None)
|
||||||
|
|
||||||
|
if not self.original:
|
||||||
|
# Store the original (un-rendered) path template, as it gets overwritten below
|
||||||
|
self.original = self.path.var
|
||||||
|
|
||||||
|
self.path.var = self.original.format(lng=context.request.LANGUAGE_CODE)
|
||||||
|
|
||||||
ret = super().render(context)
|
ret = super().render(context)
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
@ -480,4 +489,5 @@ else:
|
|||||||
# change path to called ressource
|
# change path to called ressource
|
||||||
bits[1] = f"'{loc_name}/{{lng}}.{bits[1][1:-1]}'"
|
bits[1] = f"'{loc_name}/{{lng}}.{bits[1][1:-1]}'"
|
||||||
token.contents = ' '.join(bits)
|
token.contents = ' '.join(bits)
|
||||||
|
|
||||||
return I18nStaticNode.handle_token(parser, token)
|
return I18nStaticNode.handle_token(parser, token)
|
||||||
|
@ -75,10 +75,18 @@ class ScheduleMixin:
|
|||||||
'schedule': "I", # Schedule type (see django_q.Schedule)
|
'schedule': "I", # Schedule type (see django_q.Schedule)
|
||||||
'minutes': 30, # Number of minutes (only if schedule type = Minutes)
|
'minutes': 30, # Number of minutes (only if schedule type = Minutes)
|
||||||
'repeats': 5, # Number of repeats (leave blank for 'forever')
|
'repeats': 5, # Number of repeats (leave blank for 'forever')
|
||||||
}
|
},
|
||||||
|
'member_func': {
|
||||||
|
'func': 'my_class_func', # Note, without the 'dot' notation, it will call a class member function
|
||||||
|
'schedule': "H", # Once per hour
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
Note: 'schedule' parameter must be one of ['I', 'H', 'D', 'W', 'M', 'Q', 'Y']
|
Note: 'schedule' parameter must be one of ['I', 'H', 'D', 'W', 'M', 'Q', 'Y']
|
||||||
|
|
||||||
|
Note: The 'func' argument can take two different forms:
|
||||||
|
- Dotted notation e.g. 'module.submodule.func' - calls a global function with the defined path
|
||||||
|
- Member notation e.g. 'my_func' (no dots!) - calls a member function of the calling class
|
||||||
"""
|
"""
|
||||||
|
|
||||||
ALLOWABLE_SCHEDULE_TYPES = ['I', 'H', 'D', 'W', 'M', 'Q', 'Y']
|
ALLOWABLE_SCHEDULE_TYPES = ['I', 'H', 'D', 'W', 'M', 'Q', 'Y']
|
||||||
@ -94,11 +102,14 @@ class ScheduleMixin:
|
|||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.add_mixin('schedule', 'has_scheduled_tasks', __class__)
|
self.scheduled_tasks = self.get_scheduled_tasks()
|
||||||
self.scheduled_tasks = getattr(self, 'SCHEDULED_TASKS', {})
|
|
||||||
|
|
||||||
self.validate_scheduled_tasks()
|
self.validate_scheduled_tasks()
|
||||||
|
|
||||||
|
self.add_mixin('schedule', 'has_scheduled_tasks', __class__)
|
||||||
|
|
||||||
|
def get_scheduled_tasks(self):
|
||||||
|
return getattr(self, 'SCHEDULED_TASKS', {})
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def has_scheduled_tasks(self):
|
def has_scheduled_tasks(self):
|
||||||
"""
|
"""
|
||||||
@ -158,18 +169,46 @@ class ScheduleMixin:
|
|||||||
|
|
||||||
task_name = self.get_task_name(key)
|
task_name = self.get_task_name(key)
|
||||||
|
|
||||||
# If a matching scheduled task does not exist, create it!
|
if Schedule.objects.filter(name=task_name).exists():
|
||||||
if not Schedule.objects.filter(name=task_name).exists():
|
# Scheduled task already exists - continue!
|
||||||
|
continue
|
||||||
|
|
||||||
logger.info(f"Adding scheduled task '{task_name}'")
|
logger.info(f"Adding scheduled task '{task_name}'")
|
||||||
|
|
||||||
|
func_name = task['func'].strip()
|
||||||
|
|
||||||
|
if '.' in func_name:
|
||||||
|
"""
|
||||||
|
Dotted notation indicates that we wish to run a globally defined function,
|
||||||
|
from a specified Python module.
|
||||||
|
"""
|
||||||
|
|
||||||
Schedule.objects.create(
|
Schedule.objects.create(
|
||||||
name=task_name,
|
name=task_name,
|
||||||
func=task['func'],
|
func=func_name,
|
||||||
schedule_type=task['schedule'],
|
schedule_type=task['schedule'],
|
||||||
minutes=task.get('minutes', None),
|
minutes=task.get('minutes', None),
|
||||||
repeats=task.get('repeats', -1),
|
repeats=task.get('repeats', -1),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
"""
|
||||||
|
Non-dotted notation indicates that we wish to call a 'member function' of the calling plugin.
|
||||||
|
|
||||||
|
This is managed by the plugin registry itself.
|
||||||
|
"""
|
||||||
|
|
||||||
|
slug = self.plugin_slug()
|
||||||
|
|
||||||
|
Schedule.objects.create(
|
||||||
|
name=task_name,
|
||||||
|
func='plugin.registry.call_function',
|
||||||
|
args=f"'{slug}', '{func_name}'",
|
||||||
|
schedule_type=task['schedule'],
|
||||||
|
minutes=task.get('minutes', None),
|
||||||
|
repeats=task.get('repeats', -1),
|
||||||
|
)
|
||||||
|
|
||||||
except (ProgrammingError, OperationalError):
|
except (ProgrammingError, OperationalError):
|
||||||
# Database might not yet be ready
|
# Database might not yet be ready
|
||||||
logger.warning("register_tasks failed, database not ready")
|
logger.warning("register_tasks failed, database not ready")
|
||||||
|
@ -124,6 +124,12 @@ class PluginSetting(common.models.BaseInvenTreeSetting):
|
|||||||
so that we can pass the plugin instance
|
so that we can pass the plugin instance
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def is_bool(self, **kwargs):
|
||||||
|
|
||||||
|
kwargs['plugin'] = self.plugin
|
||||||
|
|
||||||
|
return super().is_bool(**kwargs)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
return self.__class__.get_setting_name(self.key, plugin=self.plugin)
|
return self.__class__.get_setting_name(self.key, plugin=self.plugin)
|
||||||
|
@ -59,6 +59,22 @@ class PluginsRegistry:
|
|||||||
# mixins
|
# mixins
|
||||||
self.mixins_settings = {}
|
self.mixins_settings = {}
|
||||||
|
|
||||||
|
def call_plugin_function(self, slug, func, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Call a member function (named by 'func') of the plugin named by 'slug'.
|
||||||
|
|
||||||
|
As this is intended to be run by the background worker,
|
||||||
|
we do not perform any try/except here.
|
||||||
|
|
||||||
|
Instead, any error messages are returned to the worker.
|
||||||
|
"""
|
||||||
|
|
||||||
|
plugin = self.plugins[slug]
|
||||||
|
|
||||||
|
plugin_func = getattr(plugin, func)
|
||||||
|
|
||||||
|
return plugin_func(*args, **kwargs)
|
||||||
|
|
||||||
# region public functions
|
# region public functions
|
||||||
# region loading / unloading
|
# region loading / unloading
|
||||||
def load_plugins(self):
|
def load_plugins(self):
|
||||||
@ -557,3 +573,8 @@ class PluginsRegistry:
|
|||||||
|
|
||||||
|
|
||||||
registry = PluginsRegistry()
|
registry = PluginsRegistry()
|
||||||
|
|
||||||
|
|
||||||
|
def call_function(plugin_name, function_name, *args, **kwargs):
|
||||||
|
""" Global helper function to call a specific member function of a plugin """
|
||||||
|
return registry.call_plugin_function(plugin_name, function_name, *args, **kwargs)
|
||||||
|
@ -3,7 +3,7 @@ Sample plugin which supports task scheduling
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from plugin import IntegrationPluginBase
|
from plugin import IntegrationPluginBase
|
||||||
from plugin.mixins import ScheduleMixin
|
from plugin.mixins import ScheduleMixin, SettingsMixin
|
||||||
|
|
||||||
|
|
||||||
# Define some simple tasks to perform
|
# Define some simple tasks to perform
|
||||||
@ -15,7 +15,7 @@ def print_world():
|
|||||||
print("World")
|
print("World")
|
||||||
|
|
||||||
|
|
||||||
class ScheduledTaskPlugin(ScheduleMixin, IntegrationPluginBase):
|
class ScheduledTaskPlugin(ScheduleMixin, SettingsMixin, IntegrationPluginBase):
|
||||||
"""
|
"""
|
||||||
A sample plugin which provides support for scheduled tasks
|
A sample plugin which provides support for scheduled tasks
|
||||||
"""
|
"""
|
||||||
@ -25,6 +25,11 @@ class ScheduledTaskPlugin(ScheduleMixin, IntegrationPluginBase):
|
|||||||
PLUGIN_TITLE = "Scheduled Tasks"
|
PLUGIN_TITLE = "Scheduled Tasks"
|
||||||
|
|
||||||
SCHEDULED_TASKS = {
|
SCHEDULED_TASKS = {
|
||||||
|
'member': {
|
||||||
|
'func': 'member_func',
|
||||||
|
'schedule': 'I',
|
||||||
|
'minutes': 30,
|
||||||
|
},
|
||||||
'hello': {
|
'hello': {
|
||||||
'func': 'plugin.samples.integration.scheduled_task.print_hello',
|
'func': 'plugin.samples.integration.scheduled_task.print_hello',
|
||||||
'schedule': 'I',
|
'schedule': 'I',
|
||||||
@ -35,3 +40,21 @@ class ScheduledTaskPlugin(ScheduleMixin, IntegrationPluginBase):
|
|||||||
'schedule': 'H',
|
'schedule': 'H',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SETTINGS = {
|
||||||
|
'T_OR_F': {
|
||||||
|
'name': 'True or False',
|
||||||
|
'description': 'Print true or false when running the periodic task',
|
||||||
|
'validator': bool,
|
||||||
|
'default': False,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def member_func(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
A simple member function to demonstrate functionality
|
||||||
|
"""
|
||||||
|
|
||||||
|
t_or_f = self.get_setting('T_OR_F')
|
||||||
|
|
||||||
|
print(f"Called member_func - value is {t_or_f}")
|
||||||
|
@ -4,10 +4,10 @@
|
|||||||
{% load plugin_extras %}
|
{% load plugin_extras %}
|
||||||
|
|
||||||
{% trans "User Settings" as text %}
|
{% trans "User Settings" as text %}
|
||||||
{% include "sidebar_header.html" with text=text icon='fa-user' %}
|
{% include "sidebar_header.html" with text=text icon='fa-user-cog' %}
|
||||||
|
|
||||||
{% trans "Account Settings" as text %}
|
{% trans "Account Settings" as text %}
|
||||||
{% include "sidebar_item.html" with label='account' text=text icon="fa-cog" %}
|
{% include "sidebar_item.html" with label='account' text=text icon="fa-sign-in-alt" %}
|
||||||
{% trans "Display Settings" as text %}
|
{% trans "Display Settings" as text %}
|
||||||
{% include "sidebar_item.html" with label='user-display' text=text icon="fa-desktop" %}
|
{% include "sidebar_item.html" with label='user-display' text=text icon="fa-desktop" %}
|
||||||
{% trans "Home Page" as text %}
|
{% trans "Home Page" as text %}
|
||||||
|
@ -835,7 +835,9 @@ function updateFieldValue(name, value, field, options) {
|
|||||||
// Find the named field element in the modal DOM
|
// Find the named field element in the modal DOM
|
||||||
function getFormFieldElement(name, options) {
|
function getFormFieldElement(name, options) {
|
||||||
|
|
||||||
var el = $(options.modal).find(`#id_${name}`);
|
var field_name = getFieldName(name, options);
|
||||||
|
|
||||||
|
var el = $(options.modal).find(`#id_${field_name}`);
|
||||||
|
|
||||||
if (!el.exists) {
|
if (!el.exists) {
|
||||||
console.log(`ERROR: Could not find form element for field '${name}'`);
|
console.log(`ERROR: Could not find form element for field '${name}'`);
|
||||||
@ -1148,7 +1150,9 @@ function handleFormErrors(errors, fields, options) {
|
|||||||
/*
|
/*
|
||||||
* Add a rendered error message to the provided field
|
* Add a rendered error message to the provided field
|
||||||
*/
|
*/
|
||||||
function addFieldErrorMessage(field_name, error_text, error_idx, options) {
|
function addFieldErrorMessage(name, error_text, error_idx, options) {
|
||||||
|
|
||||||
|
field_name = getFieldName(name, options);
|
||||||
|
|
||||||
// Add the 'form-field-error' class
|
// Add the 'form-field-error' class
|
||||||
$(options.modal).find(`#div_id_${field_name}`).addClass('form-field-error');
|
$(options.modal).find(`#div_id_${field_name}`).addClass('form-field-error');
|
||||||
@ -1226,7 +1230,9 @@ function addClearCallbacks(fields, options) {
|
|||||||
|
|
||||||
function addClearCallback(name, field, options) {
|
function addClearCallback(name, field, options) {
|
||||||
|
|
||||||
var el = $(options.modal).find(`#clear_${name}`);
|
var field_name = getFieldName(name, options);
|
||||||
|
|
||||||
|
var el = $(options.modal).find(`#clear_${field_name}`);
|
||||||
|
|
||||||
if (!el) {
|
if (!el) {
|
||||||
console.log(`WARNING: addClearCallback could not find field '${name}'`);
|
console.log(`WARNING: addClearCallback could not find field '${name}'`);
|
||||||
@ -1374,21 +1380,23 @@ function initializeRelatedFields(fields, options) {
|
|||||||
*/
|
*/
|
||||||
function addSecondaryModal(field, fields, options) {
|
function addSecondaryModal(field, fields, options) {
|
||||||
|
|
||||||
var name = field.name;
|
var field_name = getFieldName(field.name, options);
|
||||||
|
|
||||||
var secondary = field.secondary;
|
var depth = options.depth || 0;
|
||||||
|
|
||||||
var html = `
|
var html = `
|
||||||
<span style='float: right;'>
|
<span style='float: right;'>
|
||||||
<div type='button' class='btn btn-primary btn-secondary btn-form-secondary' title='${secondary.title || secondary.label}' id='btn-new-${name}'>
|
<div type='button' class='btn btn-primary btn-secondary btn-form-secondary' title='${field.secondary.title || field.secondary.label}' id='btn-new-${field_name}'>
|
||||||
${secondary.label || secondary.title}
|
${field.secondary.label || field.secondary.title}
|
||||||
</div>
|
</div>
|
||||||
</span>`;
|
</span>`;
|
||||||
|
|
||||||
$(options.modal).find(`label[for="id_${name}"]`).append(html);
|
$(options.modal).find(`label[for="id_${field_name}"]`).append(html);
|
||||||
|
|
||||||
// Callback function when the secondary button is pressed
|
// Callback function when the secondary button is pressed
|
||||||
$(options.modal).find(`#btn-new-${name}`).click(function() {
|
$(options.modal).find(`#btn-new-${field_name}`).click(function() {
|
||||||
|
|
||||||
|
var secondary = field.secondary;
|
||||||
|
|
||||||
// Determine the API query URL
|
// Determine the API query URL
|
||||||
var url = secondary.api_url || field.api_url;
|
var url = secondary.api_url || field.api_url;
|
||||||
@ -1409,16 +1417,24 @@ function addSecondaryModal(field, fields, options) {
|
|||||||
// Force refresh from the API, to get full detail
|
// Force refresh from the API, to get full detail
|
||||||
inventreeGet(`${url}${data.pk}/`, {}, {
|
inventreeGet(`${url}${data.pk}/`, {}, {
|
||||||
success: function(responseData) {
|
success: function(responseData) {
|
||||||
|
setRelatedFieldData(field.name, responseData, options);
|
||||||
setRelatedFieldData(name, responseData, options);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Relinquish keyboard focus for this modal
|
||||||
|
$(options.modal).modal({
|
||||||
|
keyboard: false,
|
||||||
|
});
|
||||||
|
|
||||||
// Method should be "POST" for creation
|
// Method should be "POST" for creation
|
||||||
secondary.method = secondary.method || 'POST';
|
secondary.method = secondary.method || 'POST';
|
||||||
|
|
||||||
|
secondary.modal = null;
|
||||||
|
|
||||||
|
secondary.depth = depth + 1;
|
||||||
|
|
||||||
constructForm(
|
constructForm(
|
||||||
url,
|
url,
|
||||||
secondary
|
secondary
|
||||||
@ -1757,6 +1773,20 @@ function renderModelData(name, model, data, parameters, options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Construct a field name for the given field
|
||||||
|
*/
|
||||||
|
function getFieldName(name, options) {
|
||||||
|
var field_name = name;
|
||||||
|
|
||||||
|
if (options.depth) {
|
||||||
|
field_name += `_${options.depth}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return field_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Construct a single form 'field' for rendering in a form.
|
* Construct a single form 'field' for rendering in a form.
|
||||||
*
|
*
|
||||||
@ -1783,7 +1813,7 @@ function constructField(name, parameters, options) {
|
|||||||
return constructCandyInput(name, parameters, options);
|
return constructCandyInput(name, parameters, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
var field_name = `id_${name}`;
|
var field_name = getFieldName(name, options);
|
||||||
|
|
||||||
// Hidden inputs are rendered without label / help text / etc
|
// Hidden inputs are rendered without label / help text / etc
|
||||||
if (parameters.hidden) {
|
if (parameters.hidden) {
|
||||||
@ -1803,6 +1833,8 @@ function constructField(name, parameters, options) {
|
|||||||
|
|
||||||
var group = parameters.group;
|
var group = parameters.group;
|
||||||
|
|
||||||
|
var group_id = getFieldName(group, options);
|
||||||
|
|
||||||
var group_options = options.groups[group] || {};
|
var group_options = options.groups[group] || {};
|
||||||
|
|
||||||
// Are we starting a new group?
|
// Are we starting a new group?
|
||||||
@ -1810,12 +1842,12 @@ function constructField(name, parameters, options) {
|
|||||||
if (parameters.group != options.current_group) {
|
if (parameters.group != options.current_group) {
|
||||||
|
|
||||||
html += `
|
html += `
|
||||||
<div class='panel form-panel' id='form-panel-${group}' group='${group}'>
|
<div class='panel form-panel' id='form-panel-${group_id}' group='${group}'>
|
||||||
<div class='panel-heading form-panel-heading' id='form-panel-heading-${group}'>`;
|
<div class='panel-heading form-panel-heading' id='form-panel-heading-${group_id}'>`;
|
||||||
if (group_options.collapsible) {
|
if (group_options.collapsible) {
|
||||||
html += `
|
html += `
|
||||||
<div data-bs-toggle='collapse' data-bs-target='#form-panel-content-${group}'>
|
<div data-bs-toggle='collapse' data-bs-target='#form-panel-content-${group_id}'>
|
||||||
<a href='#'><span id='group-icon-${group}' class='fas fa-angle-up'></span>
|
<a href='#'><span id='group-icon-${group_id}' class='fas fa-angle-up'></span>
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
html += `<div>`;
|
html += `<div>`;
|
||||||
@ -1829,7 +1861,7 @@ function constructField(name, parameters, options) {
|
|||||||
|
|
||||||
html += `
|
html += `
|
||||||
</div></div>
|
</div></div>
|
||||||
<div class='panel-content form-panel-content' id='form-panel-content-${group}'>
|
<div class='panel-content form-panel-content' id='form-panel-content-${group_id}'>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1848,7 +1880,7 @@ function constructField(name, parameters, options) {
|
|||||||
html += parameters.before;
|
html += parameters.before;
|
||||||
}
|
}
|
||||||
|
|
||||||
html += `<div id='div_${field_name}' class='${form_classes}'>`;
|
html += `<div id='div_id_${field_name}' class='${form_classes}'>`;
|
||||||
|
|
||||||
// Add a label
|
// Add a label
|
||||||
if (!options.hideLabels) {
|
if (!options.hideLabels) {
|
||||||
@ -1886,13 +1918,13 @@ function constructField(name, parameters, options) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
html += constructInput(name, parameters, options);
|
html += constructInput(field_name, parameters, options);
|
||||||
|
|
||||||
if (extra) {
|
if (extra) {
|
||||||
|
|
||||||
if (!parameters.required) {
|
if (!parameters.required) {
|
||||||
html += `
|
html += `
|
||||||
<span class='input-group-text form-clear' id='clear_${name}' title='{% trans "Clear input" %}'>
|
<span class='input-group-text form-clear' id='clear_${field_name}' title='{% trans "Clear input" %}'>
|
||||||
<span class='icon-red fas fa-backspace'></span>
|
<span class='icon-red fas fa-backspace'></span>
|
||||||
</span>`;
|
</span>`;
|
||||||
}
|
}
|
||||||
@ -1909,7 +1941,7 @@ function constructField(name, parameters, options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Div for error messages
|
// Div for error messages
|
||||||
html += `<div id='errors-${name}'></div>`;
|
html += `<div id='errors-${field_name}'></div>`;
|
||||||
|
|
||||||
|
|
||||||
html += `</div>`; // controls
|
html += `</div>`; // controls
|
||||||
|
@ -127,6 +127,9 @@ function createNewModal(options={}) {
|
|||||||
$(modal_name).find('#modal-form-cancel').hide();
|
$(modal_name).find('#modal-form-cancel').hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Steal keyboard focus
|
||||||
|
$(modal_name).focus();
|
||||||
|
|
||||||
// Return the "name" of the modal
|
// Return the "name" of the modal
|
||||||
return modal_name;
|
return modal_name;
|
||||||
}
|
}
|
||||||
@ -372,6 +375,14 @@ function attachSelect(modal) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function attachBootstrapCheckbox(modal) {
|
||||||
|
/* Attach 'switch' functionality to any checkboxes on the form */
|
||||||
|
|
||||||
|
$(modal + ' .checkboxinput').addClass('form-check-input');
|
||||||
|
$(modal + ' .checkboxinput').wrap(`<div class='form-check form-switch'></div>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function loadingMessageContent() {
|
function loadingMessageContent() {
|
||||||
/* Render a 'loading' message to display in a form
|
/* Render a 'loading' message to display in a form
|
||||||
* when waiting for a response from the server
|
* when waiting for a response from the server
|
||||||
@ -686,7 +697,9 @@ function injectModalForm(modal, form_html) {
|
|||||||
* Updates the HTML of the form content, and then applies some other updates
|
* Updates the HTML of the form content, and then applies some other updates
|
||||||
*/
|
*/
|
||||||
$(modal).find('.modal-form-content').html(form_html);
|
$(modal).find('.modal-form-content').html(form_html);
|
||||||
|
|
||||||
attachSelect(modal);
|
attachSelect(modal);
|
||||||
|
attachBootstrapCheckbox(modal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -121,12 +121,12 @@
|
|||||||
{% if user.is_staff and not demo %}
|
{% if user.is_staff and not demo %}
|
||||||
<li><a class='dropdown-item' href="/admin/"><span class="fas fa-user-shield"></span> {% trans "Admin" %}</a></li>
|
<li><a class='dropdown-item' href="/admin/"><span class="fas fa-user-shield"></span> {% trans "Admin" %}</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<li><a class='dropdown-item' href="{% url 'settings' %}"><span class="fas fa-cog"></span> {% trans "Settings" %}</a></li>
|
||||||
<li><a class='dropdown-item' href="{% url 'account_logout' %}"><span class="fas fa-sign-out-alt"></span> {% trans "Logout" %}</a></li>
|
<li><a class='dropdown-item' href="{% url 'account_logout' %}"><span class="fas fa-sign-out-alt"></span> {% trans "Logout" %}</a></li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li><a class='dropdown-item' href="{% url 'account_login' %}"><span class="fas fa-sign-in-alt"></span> {% trans "Login" %}</a></li>
|
<li><a class='dropdown-item' href="{% url 'account_login' %}"><span class="fas fa-sign-in-alt"></span> {% trans "Login" %}</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<hr>
|
<hr>
|
||||||
<li><a class='dropdown-item' href="{% url 'settings' %}"><span class="fas fa-cog"></span> {% trans "Settings" %}</a></li>
|
|
||||||
<li id='launch-stats'>
|
<li id='launch-stats'>
|
||||||
<a class='dropdown-item' href='#'>
|
<a class='dropdown-item' href='#'>
|
||||||
{% if system_healthy or not user.is_staff %}
|
{% if system_healthy or not user.is_staff %}
|
||||||
|
@ -3,6 +3,6 @@
|
|||||||
<h6>
|
<h6>
|
||||||
<i class="bi bi-bootstrap"></i>
|
<i class="bi bi-bootstrap"></i>
|
||||||
{% if icon %}<span class='sidebar-item-icon fas {{ icon }}'></span>{% endif %}
|
{% if icon %}<span class='sidebar-item-icon fas {{ icon }}'></span>{% endif %}
|
||||||
{% if text %}<span class='sidebar-item-text' style='display: none;'>{{ text }}</span>{% endif %}
|
{% if text %}<span class='sidebar-item-text' style='display: none;'><strong>{{ text }}</strong></span>{% endif %}
|
||||||
</h6>
|
</h6>
|
||||||
</span>
|
</span>
|
@ -14,4 +14,4 @@ INVENTREE_DB_USER=pguser
|
|||||||
INVENTREE_DB_PASSWORD=pgpassword
|
INVENTREE_DB_PASSWORD=pgpassword
|
||||||
|
|
||||||
# Enable plugins?
|
# Enable plugins?
|
||||||
INVENTREE_PLUGINS_ENABLED=False
|
INVENTREE_PLUGINS_ENABLED=True
|
||||||
|
@ -45,6 +45,10 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
# Expose web server on port 8000
|
# Expose web server on port 8000
|
||||||
- 8000:8000
|
- 8000:8000
|
||||||
|
# Note: If using the inventree-dev-proxy container (see below),
|
||||||
|
# comment out the "ports" directive (above) and uncomment the "expose" directive
|
||||||
|
#expose:
|
||||||
|
# - 8000
|
||||||
volumes:
|
volumes:
|
||||||
# Ensure you specify the location of the 'src' directory at the end of this file
|
# Ensure you specify the location of the 'src' directory at the end of this file
|
||||||
- src:/home/inventree
|
- src:/home/inventree
|
||||||
@ -70,6 +74,25 @@ services:
|
|||||||
- dev-config.env
|
- dev-config.env
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
|
### Optional: Serve static and media files using nginx
|
||||||
|
### Uncomment the following lines to enable nginx proxy for testing
|
||||||
|
### Note: If enabling the proxy, change "ports" to "expose" for the inventree-dev-server container (above)
|
||||||
|
#inventree-dev-proxy:
|
||||||
|
# container_name: inventree-dev-proxy
|
||||||
|
# image: nginx:stable
|
||||||
|
# depends_on:
|
||||||
|
# - inventree-dev-server
|
||||||
|
# ports:
|
||||||
|
# # Change "8000" to the port that you want InvenTree web server to be available on
|
||||||
|
# - 8000:80
|
||||||
|
# volumes:
|
||||||
|
# # Provide ./nginx.conf file to the container
|
||||||
|
# # Refer to the provided example file as a starting point
|
||||||
|
# - ./nginx.dev.conf:/etc/nginx/conf.d/default.conf:ro
|
||||||
|
# # nginx proxy needs access to static and media files
|
||||||
|
# - src:/var/www
|
||||||
|
# restart: unless-stopped
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
# NOTE: Change "../" to a directory on your local machine, where the InvenTree source code is located
|
# NOTE: Change "../" to a directory on your local machine, where the InvenTree source code is located
|
||||||
# Persistent data, stored external to the container(s)
|
# Persistent data, stored external to the container(s)
|
||||||
|
57
docker/nginx.dev.conf
Normal file
57
docker/nginx.dev.conf
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
|
||||||
|
server {
|
||||||
|
|
||||||
|
# Listen for connection on (internal) port 80
|
||||||
|
listen 80;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
# Change 'inventree-dev-server' to the name of the inventree server container,
|
||||||
|
# and '8000' to the INVENTREE_WEB_PORT (if not default)
|
||||||
|
proxy_pass http://inventree-dev-server:8000;
|
||||||
|
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
|
||||||
|
proxy_redirect off;
|
||||||
|
|
||||||
|
client_max_body_size 100M;
|
||||||
|
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_request_buffering off;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
# Redirect any requests for static files
|
||||||
|
location /static/ {
|
||||||
|
alias /var/www/dev/static/;
|
||||||
|
autoindex on;
|
||||||
|
|
||||||
|
# Caching settings
|
||||||
|
expires 30d;
|
||||||
|
add_header Pragma public;
|
||||||
|
add_header Cache-Control "public";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Redirect any requests for media files
|
||||||
|
location /media/ {
|
||||||
|
alias /var/www/dev/media/;
|
||||||
|
|
||||||
|
# Media files require user authentication
|
||||||
|
auth_request /auth;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Use the 'user' API endpoint for auth
|
||||||
|
location /auth {
|
||||||
|
internal;
|
||||||
|
|
||||||
|
proxy_pass http://inventree-dev-server:8000/auth/;
|
||||||
|
|
||||||
|
proxy_pass_request_body off;
|
||||||
|
proxy_set_header Content-Length "";
|
||||||
|
proxy_set_header X-Original-URI $request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
# Please keep this list sorted
|
# Please keep this list sorted
|
||||||
Django==3.2.10 # Django package
|
Django==3.2.11 # Django package
|
||||||
certifi # Certifi is (most likely) installed through one of the requirements above
|
certifi # Certifi is (most likely) installed through one of the requirements above
|
||||||
coreapi==2.3.0 # API documentation
|
coreapi==2.3.0 # API documentation
|
||||||
coverage==5.3 # Unit test coverage
|
coverage==5.3 # Unit test coverage
|
||||||
@ -35,7 +35,7 @@ importlib_metadata # Backport for importlib.metadata
|
|||||||
inventree # Install the latest version of the InvenTree API python library
|
inventree # Install the latest version of the InvenTree API python library
|
||||||
markdown==3.3.4 # Force particular version of markdown
|
markdown==3.3.4 # Force particular version of markdown
|
||||||
pep8-naming==0.11.1 # PEP naming convention extension
|
pep8-naming==0.11.1 # PEP naming convention extension
|
||||||
pillow==8.3.2 # Image manipulation
|
pillow==9.0.0 # Image manipulation
|
||||||
py-moneyed==0.8.0 # Specific version requirement for py-moneyed
|
py-moneyed==0.8.0 # Specific version requirement for py-moneyed
|
||||||
pygments==2.7.4 # Syntax highlighting
|
pygments==2.7.4 # Syntax highlighting
|
||||||
python-barcode[images]==0.13.1 # Barcode generator
|
python-barcode[images]==0.13.1 # Barcode generator
|
||||||
|
Loading…
Reference in New Issue
Block a user