From 058e53459b87b808a65f4f268c896cfd3a40dc1d Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 23 Jun 2021 20:07:56 +1000 Subject: [PATCH 001/147] Add simple function for determining OPTIONS --- InvenTree/InvenTree/urls.py | 2 + InvenTree/templates/base.html | 3 +- .../script/inventree => templates/js}/api.js | 0 InvenTree/templates/js/forms.js | 38 +++++++++++++++++++ 4 files changed, 42 insertions(+), 1 deletion(-) rename InvenTree/{InvenTree/static/script/inventree => templates/js}/api.js (100%) create mode 100644 InvenTree/templates/js/forms.js diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index 0108418517..2ec7b02f23 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -103,6 +103,8 @@ settings_urls = [ # Some javascript files are served 'dynamically', allowing them to pass through the Django translation layer dynamic_javascript_urls = [ + url(r'^api.js', DynamicJsView.as_view(template_name='js/api.js'), name='api.js'), + url(r'^forms.js', DynamicJsView.as_view(template_name='js/forms.js'), name='forms.js'), url(r'^modals.js', DynamicJsView.as_view(template_name='js/modals.js'), name='modals.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'), diff --git a/InvenTree/templates/base.html b/InvenTree/templates/base.html index 65712b7394..128cbba2dd 100644 --- a/InvenTree/templates/base.html +++ b/InvenTree/templates/base.html @@ -144,11 +144,12 @@ - + + diff --git a/InvenTree/InvenTree/static/script/inventree/api.js b/InvenTree/templates/js/api.js similarity index 100% rename from InvenTree/InvenTree/static/script/inventree/api.js rename to InvenTree/templates/js/api.js diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js new file mode 100644 index 0000000000..fa2ca9eb2e --- /dev/null +++ b/InvenTree/templates/js/forms.js @@ -0,0 +1,38 @@ +/** + * This file contains code for rendering (and managing) HTML forms + * which are served via the django-drf API. + * + * The django DRF library provides an OPTIONS method for each API endpoint, + * which allows us to introspect the available fields at any given endpoint. + * + * The OPTIONS method provides the following information for each available field: + * + * - Field name + * - Field label (translated) + * - Field help text (translated) + * - Field type + * - Read / write status + * - Field required status + * - min_value / max_value + */ + + +/* + * Get the API endpoint options at the provided URL, + * using a HTTP options request. + */ +function getApiEndpointOptions(url, options={}) { + + $.ajax({ + url: url, + type: 'OPTIONS', + contentType: 'application/json', + dataType: 'json', + accepts: { + json: 'application/json', + }, + success: function(response) { + console.log(response); + } + }); +} From eaa5913c8cf05d05b9fc95ca4215952ddc473a8e Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 23 Jun 2021 20:30:26 +1000 Subject: [PATCH 002/147] Adds custom DRF metadata handler - Limit available "actions" data to only what the user is allowed to do --- InvenTree/InvenTree/metadata.py | 67 +++++++++++++++++++++++++++++++++ InvenTree/InvenTree/settings.py | 1 + InvenTree/templates/js/forms.js | 8 ++-- 3 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 InvenTree/InvenTree/metadata.py diff --git a/InvenTree/InvenTree/metadata.py b/InvenTree/InvenTree/metadata.py new file mode 100644 index 0000000000..b9d0732acf --- /dev/null +++ b/InvenTree/InvenTree/metadata.py @@ -0,0 +1,67 @@ + +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from rest_framework.metadata import SimpleMetadata + +import users.models + + +class InvenTreeMetadata(SimpleMetadata): + """ + Custom metadata class for the DRF API. + + This custom metadata class imits the available "actions", + based on the user's role permissions. + + Thus when a client send an OPTIONS request to an API endpoint, + it will only receive a list of actions which it is allowed to perform! + + """ + + def determine_metadata(self, request, view): + + metadata = super().determine_metadata(request, view) + + user = request.user + + if user is None: + # No actions for you! + metadata['actions'] = {} + return metadata + + try: + # Extract the model name associated with the view + model = view.serializer_class.Meta.model + + # Construct the 'table name' from the model + app_label = model._meta.app_label + tbl_label = model._meta.model_name + + table = f"{app_label}_{tbl_label}" + + actions = metadata['actions'] + + check = users.models.RuleSet.check_table_permission + + # Map the request method to a permission type + rolemap = { + 'GET': 'view', + 'OPTIONS': 'view', + 'POST': 'add', + 'PUT': 'change', + 'PATCH': 'change', + 'DELETE': 'delete', + } + + # Remove any HTTP methods that the user does not have permission for + for method, permission in rolemap.items(): + if method in actions and not check(user, table, permission): + del actions[method] + + except AttributeError: + # We will assume that if the serializer class does *not* have a Meta + # then we don't need a permission + pass + + return metadata diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index fceaf9a58f..be13411fd8 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -341,6 +341,7 @@ REST_FRAMEWORK = { 'InvenTree.permissions.RolePermission', ), 'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema', + 'DEFAULT_METADATA_CLASS': 'InvenTree.metadata.InvenTreeMetadata' } WSGI_APPLICATION = 'InvenTree.wsgi.application' diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index fa2ca9eb2e..822688d9ad 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -23,7 +23,8 @@ */ function getApiEndpointOptions(url, options={}) { - $.ajax({ + // Return the ajax request object + return $.ajax({ url: url, type: 'OPTIONS', contentType: 'application/json', @@ -31,8 +32,7 @@ function getApiEndpointOptions(url, options={}) { accepts: { json: 'application/json', }, - success: function(response) { - console.log(response); - } }); } + + From 82a6ff777261fe3c0a19eab807996a3edbbd37df Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 23 Jun 2021 20:58:05 +1000 Subject: [PATCH 003/147] Adds unit testing for fancy new metadata class --- InvenTree/InvenTree/api_tester.py | 16 +++++ InvenTree/InvenTree/metadata.py | 38 ++++++----- InvenTree/InvenTree/test_api.py | 65 +++++++++++++++++++ .../migrations/0029_auto_20210601_1525.py | 9 ++- .../migrations/0026_auto_20201110_1011.py | 10 ++- InvenTree/users/models.py | 2 +- 6 files changed, 119 insertions(+), 21 deletions(-) diff --git a/InvenTree/InvenTree/api_tester.py b/InvenTree/InvenTree/api_tester.py index a803e6797f..2ba4f0136c 100644 --- a/InvenTree/InvenTree/api_tester.py +++ b/InvenTree/InvenTree/api_tester.py @@ -73,6 +73,22 @@ class InvenTreeAPITestCase(APITestCase): ruleset.save() break + def getActions(self, url): + """ + Return a dict of the 'actions' available at a given endpoint. + Makes use of the HTTP 'OPTIONS' method to request this. + """ + + response = self.client.options(url) + self.assertEqual(response.status_code, 200) + + actions = response.data.get('actions', None) + + if not actions: + actions = {} + + return actions + def get(self, url, data={}, expected_code=200): """ Issue a GET request diff --git a/InvenTree/InvenTree/metadata.py b/InvenTree/InvenTree/metadata.py index b9d0732acf..b78eaa9d8c 100644 --- a/InvenTree/InvenTree/metadata.py +++ b/InvenTree/InvenTree/metadata.py @@ -40,24 +40,32 @@ class InvenTreeMetadata(SimpleMetadata): table = f"{app_label}_{tbl_label}" - actions = metadata['actions'] + actions = metadata.get('actions', None) - check = users.models.RuleSet.check_table_permission + if actions is not None: - # Map the request method to a permission type - rolemap = { - 'GET': 'view', - 'OPTIONS': 'view', - 'POST': 'add', - 'PUT': 'change', - 'PATCH': 'change', - 'DELETE': 'delete', - } + check = users.models.RuleSet.check_table_permission - # Remove any HTTP methods that the user does not have permission for - for method, permission in rolemap.items(): - if method in actions and not check(user, table, permission): - del actions[method] + # Map the request method to a permission type + rolemap = { + 'POST': 'add', + 'PUT': 'change', + 'PATCH': 'change', + 'DELETE': 'delete', + } + + # Remove any HTTP methods that the user does not have permission for + for method, permission in rolemap.items(): + if method in actions and not check(user, table, permission): + del actions[method] + + # Add a 'DELETE' action if we are allowed to delete + if check(user, table, 'delete'): + actions['DELETE'] = True + + # Add a 'VIEW' action if we are allowed to view + if check(user, table, 'view'): + actions['GET'] = True except AttributeError: # We will assume that if the serializer class does *not* have a Meta diff --git a/InvenTree/InvenTree/test_api.py b/InvenTree/InvenTree/test_api.py index 8435d756fb..a877300a27 100644 --- a/InvenTree/InvenTree/test_api.py +++ b/InvenTree/InvenTree/test_api.py @@ -157,3 +157,68 @@ class APITests(InvenTreeAPITestCase): # New role permissions should have been added now self.assertIn('delete', roles['part']) self.assertIn('change', roles['build']) + + def test_list_endpoint_actions(self): + """ + Tests for the OPTIONS method for API endpoints. + """ + + self.basicAuth() + + # Without any 'part' permissions, we should not see any available actions + url = reverse('api-part-list') + + actions = self.getActions(url) + + # No actions, as there are no permissions! + self.assertEqual(len(actions), 0) + + # Assign a new role + self.assignRole('part.view') + actions = self.getActions(url) + + # As we don't have "add" permission, there should be no available API actions + self.assertEqual(len(actions), 0) + + # But let's make things interesting... + # Why don't we treat ourselves to some "add" permissions + self.assignRole('part.add') + + actions = self.getActions(url) + + self.assertIn('POST', actions) + self.assertEqual(len(actions), 1) + + def test_detail_endpoint_actions(self): + """ + Tests for detail API endpoint actions + """ + + self.basicAuth() + + url = reverse('api-part-detail', kwargs={'pk': 1}) + + actions = self.getActions(url) + + # No actions, as we do not have any permissions! + self.assertEqual(len(actions), 0) + + # Add a 'add' permission + # Note: 'add' permission automatically implies 'change' also + self.assignRole('part.add') + + actions = self.getActions(url) + + # 'add' permission does not apply here! + self.assertEqual(len(actions), 1) + self.assertIn('PUT', actions.keys()) + + # Add some other permissions + self.assignRole('part.change') + self.assignRole('part.delete') + + actions = self.getActions(url) + + self.assertEqual(len(actions), 2) + self.assertIn('PUT', actions.keys()) + self.assertIn('DELETE', actions.keys()) diff --git a/InvenTree/build/migrations/0029_auto_20210601_1525.py b/InvenTree/build/migrations/0029_auto_20210601_1525.py index e8b2d58947..fa6bab6b26 100644 --- a/InvenTree/build/migrations/0029_auto_20210601_1525.py +++ b/InvenTree/build/migrations/0029_auto_20210601_1525.py @@ -1,8 +1,13 @@ # Generated by Django 3.2 on 2021-06-01 05:25 +import logging + from django.db import migrations +logger = logging.getLogger('inventree') + + def assign_bom_items(apps, schema_editor): """ Run through existing BuildItem objects, @@ -13,7 +18,7 @@ def assign_bom_items(apps, schema_editor): BomItem = apps.get_model('part', 'bomitem') Part = apps.get_model('part', 'part') - print("Assigning BomItems to existing BuildItem objects") + logger.info("Assigning BomItems to existing BuildItem objects") count_valid = 0 count_total = 0 @@ -41,7 +46,7 @@ def assign_bom_items(apps, schema_editor): pass if count_total > 0: - print(f"Assigned BomItem for {count_valid}/{count_total} entries") + logger.info(f"Assigned BomItem for {count_valid}/{count_total} entries") def unassign_bom_items(apps, schema_editor): diff --git a/InvenTree/company/migrations/0026_auto_20201110_1011.py b/InvenTree/company/migrations/0026_auto_20201110_1011.py index 20ec7d2f6f..29a5099c3a 100644 --- a/InvenTree/company/migrations/0026_auto_20201110_1011.py +++ b/InvenTree/company/migrations/0026_auto_20201110_1011.py @@ -1,5 +1,6 @@ # Generated by Django 3.0.7 on 2020-11-10 10:11 +import logging import sys from moneyed import CURRENCIES @@ -7,6 +8,9 @@ from django.db import migrations, connection from company.models import SupplierPriceBreak +logger = logging.getLogger('inventree') + + def migrate_currencies(apps, schema_editor): """ Migrate from the 'old' method of handling currencies, @@ -19,7 +23,7 @@ def migrate_currencies(apps, schema_editor): for the SupplierPriceBreak model, to a new django-money compatible currency. """ - print("Updating currency references for SupplierPriceBreak model...") + logger.info("Updating currency references for SupplierPriceBreak model...") # A list of available currency codes currency_codes = CURRENCIES.keys() @@ -39,7 +43,7 @@ def migrate_currencies(apps, schema_editor): suffix = suffix.strip().upper() if suffix not in currency_codes: - print("Missing suffix:", suffix) + logger.warning(f"Missing suffix: '{suffix}'") while suffix not in currency_codes: # Ask the user to input a valid currency @@ -72,7 +76,7 @@ def migrate_currencies(apps, schema_editor): count += 1 if count > 0: - print(f"Updated {count} SupplierPriceBreak rows") + logger.info(f"Updated {count} SupplierPriceBreak rows") def reverse_currencies(apps, schema_editor): """ diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index 2763ba0e10..23353948b1 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -208,7 +208,7 @@ class RuleSet(models.Model): return True # Print message instead of throwing an error - print("Failed permission check for", table, permission) + logger.info(f"User '{user.name}' failed permission check for {table}.{permission}") return False @staticmethod From b8a3117c83952d5fdea2f481f8366c9835b549ad Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 23 Jun 2021 21:21:39 +1000 Subject: [PATCH 004/147] Fix unit tests --- InvenTree/InvenTree/test_api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/test_api.py b/InvenTree/InvenTree/test_api.py index a877300a27..7a69e9da6f 100644 --- a/InvenTree/InvenTree/test_api.py +++ b/InvenTree/InvenTree/test_api.py @@ -219,6 +219,7 @@ class APITests(InvenTreeAPITestCase): actions = self.getActions(url) - self.assertEqual(len(actions), 2) + self.assertEqual(len(actions), 3) + self.assertIn('GET', actions.keys()) self.assertIn('PUT', actions.keys()) self.assertIn('DELETE', actions.keys()) From 2c1db2a902e00c4118c95a20bb77cebb70facb92 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 23 Jun 2021 21:40:09 +1000 Subject: [PATCH 005/147] Further tweaks --- InvenTree/InvenTree/metadata.py | 4 ++-- InvenTree/InvenTree/test_api.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/InvenTree/InvenTree/metadata.py b/InvenTree/InvenTree/metadata.py index b78eaa9d8c..ac06f79d3e 100644 --- a/InvenTree/InvenTree/metadata.py +++ b/InvenTree/InvenTree/metadata.py @@ -60,11 +60,11 @@ class InvenTreeMetadata(SimpleMetadata): del actions[method] # Add a 'DELETE' action if we are allowed to delete - if check(user, table, 'delete'): + if 'DELETE' in view.allowed_methods and check(user, table, 'delete'): actions['DELETE'] = True # Add a 'VIEW' action if we are allowed to view - if check(user, table, 'view'): + if 'GET' in view.allowed_methods and check(user, table, 'view'): actions['GET'] = True except AttributeError: diff --git a/InvenTree/InvenTree/test_api.py b/InvenTree/InvenTree/test_api.py index 7a69e9da6f..7581ec4fec 100644 --- a/InvenTree/InvenTree/test_api.py +++ b/InvenTree/InvenTree/test_api.py @@ -186,8 +186,9 @@ class APITests(InvenTreeAPITestCase): actions = self.getActions(url) + self.assertEqual(len(actions), 2) self.assertIn('POST', actions) - self.assertEqual(len(actions), 1) + self.assertIn('GET', actions) def test_detail_endpoint_actions(self): """ From 0d9808fbb8f2582484d0c2d0e84e69545a1b4e19 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 23 Jun 2021 21:41:19 +1000 Subject: [PATCH 006/147] Adds 'constructForm' javascript function - Skeleton only (for now!) --- InvenTree/templates/js/forms.js | 128 +++++++++++++++++++++++++++++++- 1 file changed, 126 insertions(+), 2 deletions(-) diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index 822688d9ad..bcdafd9401 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -16,15 +16,70 @@ * - min_value / max_value */ +/* + * Return true if the OPTIONS specify that the user + * can perform a GET method at the endpoint. + */ +function canView(OPTIONS) { + + if ('actions' in OPTIONS) { + return ('GET' in OPTIONS.actions); + } else { + return false; + } +} + + +/* + * Return true if the OPTIONS specify that the user + * can perform a POST method at the endpoint + */ +function canCreate(OPTIONS) { + + if ('actions' in OPTIONS) { + return ('POST' in OPTIONS.actions); + } else { + return false; + } +} + + +/* + * Return true if the OPTIONS specify that the user + * can perform a PUT or PATCH method at the endpoint + */ +function canChange(OPTIONS) { + + if ('actions' in OPTIONS) { + return ('PUT' in OPTIONS.actions || 'PATCH' in OPTIONS.actions); + } else { + return false; + } +} + + +/* + * Return true if the OPTIONS specify that the user + * can perform a DELETE method at the endpoint + */ +function canDelete(OPTIONS) { + + if ('actions' in OPTIONS) { + return ('DELETE' in OPTIONS.actions); + } else { + return false; + } +} + /* * Get the API endpoint options at the provided URL, * using a HTTP options request. */ -function getApiEndpointOptions(url, options={}) { +function getApiEndpointOptions(url, callback, options={}) { // Return the ajax request object - return $.ajax({ + $.ajax({ url: url, type: 'OPTIONS', contentType: 'application/json', @@ -32,7 +87,76 @@ function getApiEndpointOptions(url, options={}) { accepts: { json: 'application/json', }, + success: callback, }); } +/* + * Request API OPTIONS data from the server, + * and construct a modal form based on the response. + * + * arguments: + * - method: The HTTP method e.g. 'PUT', 'POST', 'DELETE', + * + * options: + * - method: + */ +function constructForm(url, method, options={}) { + + method = method.toUpperCase(); + + // Request OPTIONS endpoint from the API + getApiEndpointOptions(url, function(OPTIONS) { + + /* + * Determine what "type" of form we want to construct, + * based on the requested action. + * + * First we must determine if the user has the correct permissions! + */ + + switch (method) { + case 'POST': + if (canCreate(OPTIONS)) { + console.log('create'); + } else { + // User does not have permission to POST to the endpoint + console.log('cannot POST'); + // TODO + } + break; + case 'PUT': + case 'PATCH': + if (canChange(OPTIONS)) { + console.log("change"); + } else { + // User does not have permission to PUT/PATCH to the endpoint + // TODO + console.log('cannot edit'); + } + break; + case 'DELETE': + if (canDelete(OPTIONS)) { + console.log('delete'); + } else { + // User does not have permission to DELETE to the endpoint + // TODO + console.log('cannot delete'); + } + break; + case 'GET': + if (canView(OPTIONS)) { + console.log('view'); + } else { + // User does not have permission to GET to the endpoint + // TODO + console.log('cannot view'); + } + break; + default: + console.log(`constructForm() called with invalid method '${method}'`); + break; + } + }); +} \ No newline at end of file From c387e1a6fc7829521fa8f53f26c32416bb177119 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 23 Jun 2021 22:11:26 +1000 Subject: [PATCH 007/147] Working on functions to construct the various form components --- InvenTree/templates/js/forms.js | 136 +++++++++++++++++++++++++++++++- 1 file changed, 135 insertions(+), 1 deletion(-) diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index bcdafd9401..8d74299975 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -119,7 +119,7 @@ function constructForm(url, method, options={}) { switch (method) { case 'POST': if (canCreate(OPTIONS)) { - console.log('create'); + constructCreateForm(url, OPTIONS.actions.POST); } else { // User does not have permission to POST to the endpoint console.log('cannot POST'); @@ -159,4 +159,138 @@ function constructForm(url, method, options={}) { break; } }); +} + + +/* + * Construct a 'creation' (POST) form, to create a new model in the database. + * + * arguments: + * - fields: The 'actions' object provided by the OPTIONS endpoint + * + * options: + * - + */ +function constructCreateForm(url, fields, options={}) { + + var html = ''; + + for (const key in fields) { + //console.log('field:', key); + + html += constructField(key, fields[key], options); + } + + var modal = '#modal-form'; + + modalEnable(modal, true); + + $(modal).find('.modal-form-content').html(html); + + $(modal).modal('show'); +} + + +/* + * Construct a single form 'field' for rendering in a form. + * + * arguments: + * - name: The 'name' of the field + * - parameters: The field parameters supplied by the DRF OPTIONS method + * + * options: + * - + * + * The function constructs a fieldset which mostly replicates django "crispy" forms: + * + * - Field name + * - Field (depends on specified field type) + * - Field description (help text) + * - Field errors + */ +function constructField(name, parameters, options={}) { + + var field_name = `id_${name}`; + + var html = `
`; + + // Add a label + html += constructLabel(name, parameters); + + html += `
`; + + html += constructInput(name, parameters, options); + html += constructHelpText(name, parameters, options); + + // TODO: Add the "error message" + + html += `
`; // controls + + html += `
`; // form-group + + return html; +} + + +/* + * Construct a 'label' div + * + * arguments: + * - name: The name of the field + * - required: Is this a required field? + */ +function constructLabel(name, parameters) { + + var label_classes = 'control-label'; + + if (parameters.required) { + label_classes += ' requiredField'; + } + + var html =''; + + html += ``; + + return html; +} + + +/* + * Construct a form input based on the field parameters + * + * arguments: + * - name: The name of the field + * - parameters: Field parameters returned by the OPTIONS method + * + */ +function constructInput(name, parameters, options={}) { + + var html = ''; + + // TODO: Construct an input field based on the field type! + + return html; +} + + +/* + * Construct a 'help text' div based on the field parameters + * + * arguments: + * - name: The name of the field + * - parameters: Field parameters returned by the OPTIONS method + * + */ +function constructHelpText(name, parameters, options={}) { + + var html = `
${parameters.help_text}
`; + + return html; } \ No newline at end of file From aa0237766515358c5dec4fccc588b1d5707128d5 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 23 Jun 2021 22:25:53 +1000 Subject: [PATCH 008/147] Updates for field rendering --- InvenTree/templates/js/forms.js | 47 +++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index 8d74299975..c741530523 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -176,9 +176,14 @@ function constructCreateForm(url, fields, options={}) { var html = ''; for (const key in fields) { - //console.log('field:', key); - html += constructField(key, fields[key], options); + var field = fields[key]; + + console.log(key, field.label, field.help_text); + + var f = constructField(key, field, options); + + html += f; } var modal = '#modal-form'; @@ -218,16 +223,19 @@ function constructField(name, parameters, options={}) { html += constructLabel(name, parameters); html += `
`; - + html += constructInput(name, parameters, options); - html += constructHelpText(name, parameters, options); - + + if (parameters.help_text) { + html += constructHelpText(name, parameters, options); + } + // TODO: Add the "error message" - + html += `
`; // controls - + html += ``; // form-group - + return html; } @@ -247,15 +255,18 @@ function constructLabel(name, parameters) { label_classes += ' requiredField'; } - var html =''; + var html = `