From 934de1f77217ddc744f847008da9b348b8b361a6 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 13 Jan 2022 10:24:47 +1100 Subject: [PATCH 01/17] Adds the ability for 'scheduled tasks' to be member functions of plugins --- .../plugin/builtin/integration/mixins.py | 55 ++++++++++++++++--- InvenTree/plugin/registry.py | 16 ++++++ .../samples/integration/scheduled_task.py | 27 ++++++++- docker/dev-config.env | 2 +- 4 files changed, 89 insertions(+), 11 deletions(-) diff --git a/InvenTree/plugin/builtin/integration/mixins.py b/InvenTree/plugin/builtin/integration/mixins.py index a2087ee879..0af712641d 100644 --- a/InvenTree/plugin/builtin/integration/mixins.py +++ b/InvenTree/plugin/builtin/integration/mixins.py @@ -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.registry.call_plugin_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") diff --git a/InvenTree/plugin/registry.py b/InvenTree/plugin/registry.py index e31f3c6529..b08f643412 100644 --- a/InvenTree/plugin/registry.py +++ b/InvenTree/plugin/registry.py @@ -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) + + plugin_func(*args, **kwargs) + # region public functions # region loading / unloading def load_plugins(self): diff --git a/InvenTree/plugin/samples/integration/scheduled_task.py b/InvenTree/plugin/samples/integration/scheduled_task.py index 825dab134f..c8b1c4c5d0 100644 --- a/InvenTree/plugin/samples/integration/scheduled_task.py +++ b/InvenTree/plugin/samples/integration/scheduled_task.py @@ -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}") diff --git a/docker/dev-config.env b/docker/dev-config.env index b7ee4d8526..63a0afe4fb 100644 --- a/docker/dev-config.env +++ b/docker/dev-config.env @@ -14,4 +14,4 @@ INVENTREE_DB_USER=pguser INVENTREE_DB_PASSWORD=pgpassword # Enable plugins? -INVENTREE_PLUGINS_ENABLED=False +INVENTREE_PLUGINS_ENABLED=True From 519a1009aba870727c3b0ecbdc10b5481b49dcdd Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 13 Jan 2022 10:47:37 +1100 Subject: [PATCH 02/17] Fix: wrap args in quotes --- InvenTree/plugin/builtin/integration/mixins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/plugin/builtin/integration/mixins.py b/InvenTree/plugin/builtin/integration/mixins.py index 0af712641d..bb1d5f8ee6 100644 --- a/InvenTree/plugin/builtin/integration/mixins.py +++ b/InvenTree/plugin/builtin/integration/mixins.py @@ -203,7 +203,7 @@ class ScheduleMixin: Schedule.objects.create( name=task_name, func='plugin.registry.registry.call_plugin_function', - args=f'{slug}, {func_name}', + args=f"'{slug}', '{func_name}'", schedule_type=task['schedule'], minutes=task.get('minutes', None), repeats=task.get('repeats', -1), From ae016730f840e2867717c4db2666f11e00f9426b Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 13 Jan 2022 10:53:33 +1100 Subject: [PATCH 03/17] Fix - needs a global function to schedule --- InvenTree/plugin/builtin/integration/mixins.py | 2 +- InvenTree/plugin/registry.py | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/InvenTree/plugin/builtin/integration/mixins.py b/InvenTree/plugin/builtin/integration/mixins.py index bb1d5f8ee6..7306a30a3c 100644 --- a/InvenTree/plugin/builtin/integration/mixins.py +++ b/InvenTree/plugin/builtin/integration/mixins.py @@ -202,7 +202,7 @@ class ScheduleMixin: Schedule.objects.create( name=task_name, - func='plugin.registry.registry.call_plugin_function', + func='plugin.registry.call_function', args=f"'{slug}', '{func_name}'", schedule_type=task['schedule'], minutes=task.get('minutes', None), diff --git a/InvenTree/plugin/registry.py b/InvenTree/plugin/registry.py index b08f643412..09389aae73 100644 --- a/InvenTree/plugin/registry.py +++ b/InvenTree/plugin/registry.py @@ -73,7 +73,7 @@ class PluginsRegistry: plugin_func = getattr(plugin, func) - plugin_func(*args, **kwargs) + return plugin_func(*args, **kwargs) # region public functions # region loading / unloading @@ -573,3 +573,7 @@ class PluginsRegistry: registry = PluginsRegistry() + +def call_function(plugin_name, function_name): + """ Global helper function to call a specific member function of a plugin """ + return registry.call_plugin_function(plugin_name, function_name) From 3fb77f466c5b240c879785f4c234a0ade4f149a1 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 13 Jan 2022 11:00:33 +1100 Subject: [PATCH 04/17] PEP fixes --- InvenTree/plugin/registry.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/InvenTree/plugin/registry.py b/InvenTree/plugin/registry.py index 09389aae73..4b7f12bc6c 100644 --- a/InvenTree/plugin/registry.py +++ b/InvenTree/plugin/registry.py @@ -574,6 +574,7 @@ class PluginsRegistry: registry = PluginsRegistry() -def call_function(plugin_name, function_name): + +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) + return registry.call_plugin_function(plugin_name, function_name, *args, **kwargs) From f997e092b14ec094a44ef18a74e296ae5112502f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 13 Jan 2022 01:11:42 +0000 Subject: [PATCH 05/17] Bump pillow from 8.3.2 to 9.0.0 Bumps [pillow](https://github.com/python-pillow/Pillow) from 8.3.2 to 9.0.0. - [Release notes](https://github.com/python-pillow/Pillow/releases) - [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst) - [Commits](https://github.com/python-pillow/Pillow/compare/8.3.2...9.0.0) --- updated-dependencies: - dependency-name: pillow dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d6405bb7bc..ac2cd1dddc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 From 1927daa56de4db2a871caf49f6caf3decc4cbc84 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 13 Jan 2022 02:26:20 +0000 Subject: [PATCH 06/17] Bump django from 3.2.10 to 3.2.11 Bumps [django](https://github.com/django/django) from 3.2.10 to 3.2.11. - [Release notes](https://github.com/django/django/releases) - [Commits](https://github.com/django/django/compare/3.2.10...3.2.11) --- updated-dependencies: - dependency-name: django dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ac2cd1dddc..b707e9821f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 From eb21e1f84414cfc561f5e2b38bc810d5c5796a66 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 13 Jan 2022 16:22:38 +1100 Subject: [PATCH 07/17] Display "boolean" plugin setting values as checkboxes --- InvenTree/plugin/models.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/InvenTree/plugin/models.py b/InvenTree/plugin/models.py index 30735e7510..d6fd71d7c4 100644 --- a/InvenTree/plugin/models.py +++ b/InvenTree/plugin/models.py @@ -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) From 2f016910952d8cd97e75b301c35d5c3d710d9138 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 13 Jan 2022 16:37:42 +1100 Subject: [PATCH 08/17] "patch" for legacy checkboxes in HTML forms - Update to match the style of more "modern" API-driven forms - These legacy forms will be removed in future revision anyway --- InvenTree/templates/js/translated/modals.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/InvenTree/templates/js/translated/modals.js b/InvenTree/templates/js/translated/modals.js index e8697ac656..ad5e1c2742 100644 --- a/InvenTree/templates/js/translated/modals.js +++ b/InvenTree/templates/js/translated/modals.js @@ -372,6 +372,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(`
`); +} + + function loadingMessageContent() { /* Render a 'loading' message to display in a form * when waiting for a response from the server @@ -686,7 +694,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); } From 6f3918deeac122f10a586d4e0582c88e3b77824b Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 14 Jan 2022 00:02:00 +1100 Subject: [PATCH 09/17] Fix for i18n javascript --- InvenTree/part/templatetags/inventree_extras.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/InvenTree/part/templatetags/inventree_extras.py b/InvenTree/part/templatetags/inventree_extras.py index e2faedfd9e..64b8b8536f 100644 --- a/InvenTree/part/templatetags/inventree_extras.py +++ b/InvenTree/part/templatetags/inventree_extras.py @@ -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) From 391836689da7609e26008f3a8f4ff303f0638293 Mon Sep 17 00:00:00 2001 From: eeintech Date: Mon, 17 Jan 2022 09:04:14 -0500 Subject: [PATCH 10/17] Allow decimal for quantity received on PO --- InvenTree/order/models.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index a86c437e05..684d2b76b4 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -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") }) From fcb958c5c0f904100ef90a5c07cb830bb3b85530 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 19 Jan 2022 12:31:35 +1100 Subject: [PATCH 11/17] Adds an option to enable nginx proxy for "dev mode" in docker - Commented out by default - No functional change unless code is un-commented in the docker-compose script --- docker/docker-compose.dev.yml | 22 ++++++++++++++ docker/nginx.dev.conf | 57 +++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 docker/nginx.dev.conf diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index 14a9c4bbfc..c4691acdce 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -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,24 @@ 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 + #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) diff --git a/docker/nginx.dev.conf b/docker/nginx.dev.conf new file mode 100644 index 0000000000..8fc47e622c --- /dev/null +++ b/docker/nginx.dev.conf @@ -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; + } + +} From f9ffbe322a74a8a5b3ac78c3c1fc605e7e894b5b Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 19 Jan 2022 12:32:30 +1100 Subject: [PATCH 12/17] Adds note --- docker/docker-compose.dev.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index c4691acdce..ca0f837142 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -76,6 +76,7 @@ services: ### 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 From 40564f035713d0346189bd0b6e5193ccfdc13dcd Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 19 Jan 2022 14:38:05 +1100 Subject: [PATCH 13/17] Rearrange drop-down menu --- InvenTree/templates/navbar.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/templates/navbar.html b/InvenTree/templates/navbar.html index 551f1e99ed..7fd4a76eff 100644 --- a/InvenTree/templates/navbar.html +++ b/InvenTree/templates/navbar.html @@ -110,12 +110,12 @@ {% if user.is_staff and not demo %}
  • {% trans "Admin" %}
  • {% endif %} +
  • {% trans "Settings" %}
  • {% trans "Logout" %}
  • {% else %}
  • {% trans "Login" %}
  • {% endif %}
    -
  • {% trans "Settings" %}
  • {% if system_healthy or not user.is_staff %} From f16b18f7ad51eaa1ab5a6097215d5f1ec1a41209 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 19 Jan 2022 14:41:22 +1100 Subject: [PATCH 14/17] icon fixes --- InvenTree/templates/InvenTree/settings/sidebar.html | 4 ++-- InvenTree/templates/sidebar_header.html | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/InvenTree/templates/InvenTree/settings/sidebar.html b/InvenTree/templates/InvenTree/settings/sidebar.html index b17b226f28..85e0b4ce94 100644 --- a/InvenTree/templates/InvenTree/settings/sidebar.html +++ b/InvenTree/templates/InvenTree/settings/sidebar.html @@ -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 %} diff --git a/InvenTree/templates/sidebar_header.html b/InvenTree/templates/sidebar_header.html index bb909a03a8..1aac8c9f7b 100644 --- a/InvenTree/templates/sidebar_header.html +++ b/InvenTree/templates/sidebar_header.html @@ -3,6 +3,6 @@
    {% if icon %}{% endif %} - {% if text %}{% endif %} + {% if text %}{% endif %}
    \ No newline at end of file From 21f2dd5896e5685f8213cfcb1173ffc696e57425 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 19 Jan 2022 18:55:27 +1100 Subject: [PATCH 15/17] Use modal "depth" to construct form fields - Top level modals are not changed --- InvenTree/templates/js/translated/forms.js | 52 ++++++++++++++++------ 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/InvenTree/templates/js/translated/forms.js b/InvenTree/templates/js/translated/forms.js index 43e8d5ce62..e9e03f04fc 100644 --- a/InvenTree/templates/js/translated/forms.js +++ b/InvenTree/templates/js/translated/forms.js @@ -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}'`); @@ -1376,6 +1382,8 @@ function addSecondaryModal(field, fields, options) { var name = field.name; + var depth = options.depth || 0; + var secondary = field.secondary; var html = ` @@ -1419,6 +1427,8 @@ function addSecondaryModal(field, fields, options) { // Method should be "POST" for creation secondary.method = secondary.method || 'POST'; + secondary.depth = depth + 1; + constructForm( url, secondary @@ -1757,6 +1767,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 +1807,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 +1827,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 +1836,12 @@ function constructField(name, parameters, options) { if (parameters.group != options.current_group) { html += ` -
    -
    `; +
    +
    `; if (group_options.collapsible) { html += ` -
    - + -
    +
    `; } @@ -1848,7 +1874,7 @@ function constructField(name, parameters, options) { html += parameters.before; } - html += `
    `; + html += `
    `; // Add a label if (!options.hideLabels) { @@ -1886,13 +1912,13 @@ function constructField(name, parameters, options) { } } - html += constructInput(name, parameters, options); + html += constructInput(field_name, parameters, options); if (extra) { if (!parameters.required) { html += ` - + `; } @@ -1909,7 +1935,7 @@ function constructField(name, parameters, options) { } // Div for error messages - html += `
    `; + html += `
    `; html += `
    `; // controls From 3c328feb658b947b497fc6e07236635b7b8def6b Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 19 Jan 2022 19:03:47 +1100 Subject: [PATCH 16/17] Donate keyboard focus when creating a secondary modal --- InvenTree/templates/js/translated/forms.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/InvenTree/templates/js/translated/forms.js b/InvenTree/templates/js/translated/forms.js index e9e03f04fc..4e6e2fd162 100644 --- a/InvenTree/templates/js/translated/forms.js +++ b/InvenTree/templates/js/translated/forms.js @@ -1424,6 +1424,11 @@ function addSecondaryModal(field, fields, options) { }; } + // Relinquish keyboard focus for this modal + $(options.modal).modal({ + keyboard: false, + }); + // Method should be "POST" for creation secondary.method = secondary.method || 'POST'; From 9ffcdbc4172844cbb23bdd785ac74aa53f631166 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 19 Jan 2022 19:18:20 +1100 Subject: [PATCH 17/17] Fix issues when re-opening a secondary modal --- InvenTree/templates/js/translated/forms.js | 19 ++++++++++--------- InvenTree/templates/js/translated/modals.js | 3 +++ 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/InvenTree/templates/js/translated/forms.js b/InvenTree/templates/js/translated/forms.js index 4e6e2fd162..50ae39df2f 100644 --- a/InvenTree/templates/js/translated/forms.js +++ b/InvenTree/templates/js/translated/forms.js @@ -1380,23 +1380,23 @@ function initializeRelatedFields(fields, options) { */ function addSecondaryModal(field, fields, options) { - var name = field.name; + var field_name = getFieldName(field.name, options); var depth = options.depth || 0; - var secondary = field.secondary; - var html = ` -
    - ${secondary.label || secondary.title} +
    + ${field.secondary.label || field.secondary.title}
    `; - $(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; @@ -1417,8 +1417,7 @@ 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); } }); }; @@ -1432,6 +1431,8 @@ function addSecondaryModal(field, fields, options) { // Method should be "POST" for creation secondary.method = secondary.method || 'POST'; + secondary.modal = null; + secondary.depth = depth + 1; constructForm( diff --git a/InvenTree/templates/js/translated/modals.js b/InvenTree/templates/js/translated/modals.js index ad5e1c2742..539ce61912 100644 --- a/InvenTree/templates/js/translated/modals.js +++ b/InvenTree/templates/js/translated/modals.js @@ -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; }