mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-31 13:15:43 +00:00 
			
		
		
		
	Merge branch 'inventree:master' into matmair/issue2279
This commit is contained in:
		| @@ -27,6 +27,7 @@ from stock import models as stock_models | ||||
| from company.models import Company, SupplierPart | ||||
| from plugin.events import trigger_event | ||||
|  | ||||
| import InvenTree.helpers | ||||
| from InvenTree.fields import InvenTreeModelMoneyField, RoundingDecimalField | ||||
| from InvenTree.helpers import decimal2string, increment, getSetting | ||||
| from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus, StockStatus, StockHistoryCode | ||||
| @@ -414,16 +415,12 @@ class PurchaseOrder(Order): | ||||
|             ) | ||||
|  | ||||
|         try: | ||||
|             if not (quantity % 1 == 0): | ||||
|                 raise ValidationError({ | ||||
|                     "quantity": _("Quantity must be an integer") | ||||
|                 }) | ||||
|             if quantity < 0: | ||||
|                 raise ValidationError({ | ||||
|                     "quantity": _("Quantity must be a positive number") | ||||
|                 }) | ||||
|             quantity = int(quantity) | ||||
|         except (ValueError, TypeError): | ||||
|             quantity = InvenTree.helpers.clean_decimal(quantity) | ||||
|         except TypeError: | ||||
|             raise ValidationError({ | ||||
|                 "quantity": _("Invalid quantity provided") | ||||
|             }) | ||||
|   | ||||
| @@ -451,8 +451,17 @@ class I18nStaticNode(StaticNode): | ||||
|     replaces a variable named *lng* in the path with the current language | ||||
|     """ | ||||
|     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) | ||||
|  | ||||
|         return ret | ||||
|  | ||||
|  | ||||
| @@ -480,4 +489,5 @@ else: | ||||
|         # change path to called ressource | ||||
|         bits[1] = f"'{loc_name}/{{lng}}.{bits[1][1:-1]}'" | ||||
|         token.contents = ' '.join(bits) | ||||
|  | ||||
|         return I18nStaticNode.handle_token(parser, token) | ||||
|   | ||||
| @@ -75,10 +75,18 @@ class ScheduleMixin: | ||||
|             'schedule': "I",                        # Schedule type (see django_q.Schedule) | ||||
|             'minutes': 30,                          # Number of minutes (only if schedule type = Minutes) | ||||
|             '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: 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'] | ||||
| @@ -94,11 +102,14 @@ class ScheduleMixin: | ||||
|  | ||||
|     def __init__(self): | ||||
|         super().__init__() | ||||
|         self.add_mixin('schedule', 'has_scheduled_tasks', __class__) | ||||
|         self.scheduled_tasks = getattr(self, 'SCHEDULED_TASKS', {}) | ||||
|  | ||||
|         self.scheduled_tasks = self.get_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 | ||||
|     def has_scheduled_tasks(self): | ||||
|         """ | ||||
| @@ -158,18 +169,46 @@ class ScheduleMixin: | ||||
|  | ||||
|                 task_name = self.get_task_name(key) | ||||
|  | ||||
|                 # If a matching scheduled task does not exist, create it! | ||||
|                 if not Schedule.objects.filter(name=task_name).exists(): | ||||
|                 if 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( | ||||
|                         name=task_name, | ||||
|                         func=task['func'], | ||||
|                         func=func_name, | ||||
|                         schedule_type=task['schedule'], | ||||
|                         minutes=task.get('minutes', None), | ||||
|                         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): | ||||
|             # Database might not yet be 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 | ||||
|     """ | ||||
|  | ||||
|     def is_bool(self, **kwargs): | ||||
|  | ||||
|         kwargs['plugin'] = self.plugin | ||||
|  | ||||
|         return super().is_bool(**kwargs) | ||||
|  | ||||
|     @property | ||||
|     def name(self): | ||||
|         return self.__class__.get_setting_name(self.key, plugin=self.plugin) | ||||
|   | ||||
| @@ -59,6 +59,22 @@ class PluginsRegistry: | ||||
|         # mixins | ||||
|         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 loading / unloading | ||||
|     def load_plugins(self): | ||||
| @@ -557,3 +573,8 @@ class 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.mixins import ScheduleMixin | ||||
| from plugin.mixins import ScheduleMixin, SettingsMixin | ||||
|  | ||||
|  | ||||
| # Define some simple tasks to perform | ||||
| @@ -15,7 +15,7 @@ def print_world(): | ||||
|     print("World") | ||||
|  | ||||
|  | ||||
| class ScheduledTaskPlugin(ScheduleMixin, IntegrationPluginBase): | ||||
| class ScheduledTaskPlugin(ScheduleMixin, SettingsMixin, IntegrationPluginBase): | ||||
|     """ | ||||
|     A sample plugin which provides support for scheduled tasks | ||||
|     """ | ||||
| @@ -25,6 +25,11 @@ class ScheduledTaskPlugin(ScheduleMixin, IntegrationPluginBase): | ||||
|     PLUGIN_TITLE = "Scheduled Tasks" | ||||
|  | ||||
|     SCHEDULED_TASKS = { | ||||
|         'member': { | ||||
|             'func': 'member_func', | ||||
|             'schedule': 'I', | ||||
|             'minutes': 30, | ||||
|         }, | ||||
|         'hello': { | ||||
|             'func': 'plugin.samples.integration.scheduled_task.print_hello', | ||||
|             'schedule': 'I', | ||||
| @@ -35,3 +40,21 @@ class ScheduledTaskPlugin(ScheduleMixin, IntegrationPluginBase): | ||||
|             '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 %} | ||||
|  | ||||
| {% 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 %} | ||||
| {% 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 %} | ||||
| {% include "sidebar_item.html" with label='user-display' text=text icon="fa-desktop" %} | ||||
| {% trans "Home Page" as text %} | ||||
|   | ||||
| @@ -835,7 +835,9 @@ function updateFieldValue(name, value, field, options) { | ||||
| // Find the named field element in the modal DOM | ||||
| 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) { | ||||
|         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 | ||||
|  */ | ||||
| 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 | ||||
|     $(options.modal).find(`#div_id_${field_name}`).addClass('form-field-error'); | ||||
| @@ -1226,7 +1230,9 @@ function addClearCallbacks(fields, 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) { | ||||
|         console.log(`WARNING: addClearCallback could not find field '${name}'`); | ||||
| @@ -1374,21 +1380,23 @@ function initializeRelatedFields(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 = ` | ||||
|     <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}'> | ||||
|             ${secondary.label || secondary.title} | ||||
|         <div type='button' class='btn btn-primary btn-secondary btn-form-secondary' title='${field.secondary.title || field.secondary.label}' id='btn-new-${field_name}'> | ||||
|             ${field.secondary.label || field.secondary.title} | ||||
|         </div> | ||||
|     </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 | ||||
|     $(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 | ||||
|         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 | ||||
|                 inventreeGet(`${url}${data.pk}/`, {}, { | ||||
|                     success: function(responseData) { | ||||
|  | ||||
|                         setRelatedFieldData(name, responseData, options); | ||||
|                         setRelatedFieldData(field.name, responseData, options); | ||||
|                     } | ||||
|                 }); | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         // Relinquish keyboard focus for this modal | ||||
|         $(options.modal).modal({ | ||||
|             keyboard: false, | ||||
|         }); | ||||
|  | ||||
|         // Method should be "POST" for creation | ||||
|         secondary.method = secondary.method || 'POST'; | ||||
|  | ||||
|         secondary.modal = null; | ||||
|  | ||||
|         secondary.depth = depth + 1; | ||||
|  | ||||
|         constructForm( | ||||
|             url, | ||||
|             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. | ||||
|  *  | ||||
| @@ -1783,7 +1813,7 @@ function constructField(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 | ||||
|     if (parameters.hidden) { | ||||
| @@ -1803,6 +1833,8 @@ function constructField(name, parameters, options) { | ||||
|  | ||||
|         var group = parameters.group; | ||||
|  | ||||
|         var group_id = getFieldName(group, options); | ||||
|  | ||||
|         var group_options = options.groups[group] || {}; | ||||
|  | ||||
|         // Are we starting a new group? | ||||
| @@ -1810,12 +1842,12 @@ function constructField(name, parameters, options) { | ||||
|         if (parameters.group != options.current_group) { | ||||
|  | ||||
|             html += ` | ||||
|             <div class='panel form-panel' id='form-panel-${group}' group='${group}'> | ||||
|                 <div class='panel-heading form-panel-heading' id='form-panel-heading-${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_id}'>`; | ||||
|             if (group_options.collapsible) { | ||||
|                 html += ` | ||||
|                 <div data-bs-toggle='collapse' data-bs-target='#form-panel-content-${group}'> | ||||
|                     <a href='#'><span id='group-icon-${group}' class='fas fa-angle-up'></span>  | ||||
|                 <div data-bs-toggle='collapse' data-bs-target='#form-panel-content-${group_id}'> | ||||
|                     <a href='#'><span id='group-icon-${group_id}' class='fas fa-angle-up'></span>  | ||||
|                 `; | ||||
|             } else { | ||||
|                 html += `<div>`; | ||||
| @@ -1829,7 +1861,7 @@ function constructField(name, parameters, options) { | ||||
|  | ||||
|             html += ` | ||||
|                 </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 += `<div id='div_${field_name}' class='${form_classes}'>`; | ||||
|     html += `<div id='div_id_${field_name}' class='${form_classes}'>`; | ||||
|  | ||||
|     // Add a label | ||||
|     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 (!parameters.required) { | ||||
|             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>`; | ||||
|         } | ||||
| @@ -1909,7 +1941,7 @@ function constructField(name, parameters, options) { | ||||
|     } | ||||
|  | ||||
|     // Div for error messages | ||||
|     html += `<div id='errors-${name}'></div>`; | ||||
|     html += `<div id='errors-${field_name}'></div>`; | ||||
|  | ||||
|  | ||||
|     html += `</div>`; // controls | ||||
|   | ||||
| @@ -127,6 +127,9 @@ function createNewModal(options={}) { | ||||
|         $(modal_name).find('#modal-form-cancel').hide(); | ||||
|     } | ||||
|  | ||||
|     // Steal keyboard focus | ||||
|     $(modal_name).focus(); | ||||
|  | ||||
|     // Return the "name" of the modal | ||||
|     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() { | ||||
|     /* Render a 'loading' message to display in a form  | ||||
|      * 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 | ||||
|      */ | ||||
|     $(modal).find('.modal-form-content').html(form_html); | ||||
|  | ||||
|     attachSelect(modal); | ||||
|     attachBootstrapCheckbox(modal); | ||||
| } | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -121,12 +121,12 @@ | ||||
|           {% 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> | ||||
|           {% 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> | ||||
|           {% else %} | ||||
|           <li><a class='dropdown-item' href="{% url 'account_login' %}"><span class="fas fa-sign-in-alt"></span> {% trans "Login" %}</a></li> | ||||
|           {% endif %} | ||||
|           <hr> | ||||
|           <li><a class='dropdown-item' href="{% url 'settings' %}"><span class="fas fa-cog"></span> {% trans "Settings" %}</a></li> | ||||
|           <li id='launch-stats'> | ||||
|             <a class='dropdown-item' href='#'> | ||||
|               {% if system_healthy or not user.is_staff %} | ||||
|   | ||||
| @@ -3,6 +3,6 @@ | ||||
|     <h6> | ||||
|         <i class="bi bi-bootstrap"></i> | ||||
|         {% 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> | ||||
| </span> | ||||
| @@ -14,4 +14,4 @@ INVENTREE_DB_USER=pguser | ||||
| INVENTREE_DB_PASSWORD=pgpassword | ||||
|  | ||||
| # Enable plugins? | ||||
| INVENTREE_PLUGINS_ENABLED=False | ||||
| INVENTREE_PLUGINS_ENABLED=True | ||||
|   | ||||
| @@ -45,6 +45,10 @@ services: | ||||
|         ports: | ||||
|             # Expose web server on port 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: | ||||
|             # Ensure you specify the location of the 'src' directory at the end of this file | ||||
|             - src:/home/inventree | ||||
| @@ -70,6 +74,25 @@ services: | ||||
|             - dev-config.env | ||||
|         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: | ||||
|     # NOTE: Change "../" to a directory on your local machine, where the InvenTree source code is located | ||||
|     # 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 | ||||
| Django==3.2.10                   # Django package | ||||
| Django==3.2.11                   # Django package | ||||
| certifi                         # Certifi is (most likely) installed through one of the requirements above | ||||
| coreapi==2.3.0                  # API documentation | ||||
| 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 | ||||
| markdown==3.3.4                 # Force particular version of markdown | ||||
| 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 | ||||
| pygments==2.7.4                 # Syntax highlighting | ||||
| python-barcode[images]==0.13.1  # Barcode generator | ||||
|   | ||||
		Reference in New Issue
	
	Block a user