From 2d77b21a4e277d3a8a7fa2d3e99c9c5a25cd4078 Mon Sep 17 00:00:00 2001 From: rocheparadox Date: Mon, 11 Oct 2021 22:21:12 +0530 Subject: [PATCH 1/9] PART_NAME_FORMAT is introduced to display the names of parts in custom format. - For Feature Request InvenTree#2085 full_name construction in part.js is obsolete/redundant since the same is constructed in backend and sent through api response --- InvenTree/part/models.py | 47 +++++++++++++++++++---- InvenTree/part/settings.py | 10 +++++ InvenTree/templates/js/translated/part.js | 18 +-------- 3 files changed, 50 insertions(+), 25 deletions(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 8c43a623a0..692c3b53b8 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -8,6 +8,7 @@ import decimal import os import logging +import re from django.utils.translation import gettext_lazy as _ from django.core.exceptions import ValidationError @@ -555,7 +556,9 @@ class Part(MPTTModel): @property def full_name(self): - """ Format a 'full name' for this Part. + """ Format a 'full name' for this Part based on the format PART_NAME_FORMAT defined in part.settings file + + As a failsafe option, the following is done - IPN (if not null) - Part name @@ -564,17 +567,45 @@ class Part(MPTTModel): Elements are joined by the | character """ - elements = [] + full_name = part_settings.PART_NAME_FORMAT + field_parser_regex_pattern = re.compile('{.*?}') + field_regex_pattern = re.compile('(?<=part\\.)[A-z]*') - if self.IPN: - elements.append(self.IPN) + try: - elements.append(self.name) + for field_parser in field_parser_regex_pattern.findall(part_settings.PART_NAME_FORMAT): - if self.revision: - elements.append(self.revision) + # Each parser should contain a single field + field_name = field_regex_pattern.findall(field_parser)[0] + field_value = getattr(self, field_name) - return ' | '.join(elements) + if field_value: + # replace the part.$field with field's value and remove the braces + parsed_value = field_parser.replace(f'part.{field_name}', field_value)[1:-1] + full_name = full_name.replace(field_parser, parsed_value) + + else: + # remove the field parser in full name + full_name = full_name.replace(field_parser, '') + + return full_name + + except AttributeError as attr_err: + + logger.warning(f"exception while trying to create full name for part {self.name}", attr_err) + + # Fallback to default format + elements = [] + + if self.IPN: + elements.append(self.IPN) + + elements.append(self.name) + + if self.revision: + elements.append(self.revision) + + return ' | '.join(elements) def set_category(self, category): diff --git a/InvenTree/part/settings.py b/InvenTree/part/settings.py index e345a9d88d..c1d2f4c7e6 100644 --- a/InvenTree/part/settings.py +++ b/InvenTree/part/settings.py @@ -62,3 +62,13 @@ def part_trackable_default(): """ return InvenTreeSetting.get_setting('PART_TRACKABLE') + + +# CONSTANTS + +# Every brace pair is a field parser within which a field name of part has to be defined in the format part.$field_name +# When full name is constructed, It would be replaced by its value from the database and if the value is None, +# the entire field_parser i.e {.*} would be replaced with ''. +# Other characters inside and between the brace pairs would be copied as is. +PART_NAME_FORMAT = '{part.IPN | }{part.name}{ | part.revision}' + diff --git a/InvenTree/templates/js/translated/part.js b/InvenTree/templates/js/translated/part.js index aba3c46330..db8d435b96 100644 --- a/InvenTree/templates/js/translated/part.js +++ b/InvenTree/templates/js/translated/part.js @@ -876,23 +876,7 @@ function loadPartTable(table, url, options={}) { switchable: false, formatter: function(value, row) { - var name = ''; - - if (row.IPN) { - name += row.IPN; - name += ' | '; - } - - name += value; - - if (row.revision) { - name += ' | '; - name += row.revision; - } - - if (row.is_template) { - name = '' + name + ''; - } + var name = row.full_name; var display = imageHoverIcon(row.thumbnail) + renderLink(name, '/part/' + row.pk + '/'); From 2bf51b0ac363e6528d2ec91e45ad23ed217bde07 Mon Sep 17 00:00:00 2001 From: rocheparadox Date: Tue, 12 Oct 2021 19:06:23 +0530 Subject: [PATCH 2/9] Added PART_NAME_FORMAT to Inventree settings and exposed the same in settings window with a validator --- InvenTree/common/models.py | 29 +++++++++++++++++++ InvenTree/part/models.py | 28 ++++++------------ InvenTree/part/settings.py | 10 ------- .../templates/InvenTree/settings/part.html | 1 + 4 files changed, 39 insertions(+), 29 deletions(-) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index b9b7d9e20d..8c670a279d 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -9,6 +9,7 @@ from __future__ import unicode_literals import os import decimal import math +import re from django.db import models, transaction from django.contrib.auth.models import User @@ -487,6 +488,26 @@ class InvenTreeSetting(BaseInvenTreeSetting): even if that key does not exist. """ + def validate_part_name_format(self): + """ + Validate part name format. + Make sure that each template container has a field of Part Model + """ + + jinja_template_regex = re.compile('{{.*?}}') + field_name_regex = re.compile('(?<=part\\.)[A-z]*') + for jinja_template in jinja_template_regex.findall(str(self)): + # make sure at least one and only one field is present inside the parser + field_name = field_name_regex.findall(jinja_template) + if len(field_name) < 1: + raise ValidationError({ + 'value': 'At least one field must be present inside a jinja template container i.e {{}}' + }) + + # TODO: Make sure that the field_name exists in Part model + + return True + """ Dict of all global settings values: @@ -702,6 +723,14 @@ class InvenTreeSetting(BaseInvenTreeSetting): 'validator': bool }, + 'PART_NAME_FORMAT': { + 'name': _('Part Name Display Format'), + 'description': _('Format to display the part name'), + 'default': "{{ part.IPN if part.IPN }} {{ '|' if part.IPN }} {{ part.name }} {{ '|' if part.revision }}" + " {{ part.revision }}", + 'validator': validate_part_name_format + }, + 'REPORT_DEBUG_MODE': { 'name': _('Debug Mode'), 'description': _('Generate reports in debug mode (HTML output)'), diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 692c3b53b8..7a4834cd7a 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -24,6 +24,8 @@ from django.contrib.auth.models import User from django.db.models.signals import pre_delete from django.dispatch import receiver +from jinja2 import Template + from markdownx.models import MarkdownxField from django_cleanup import cleanup @@ -39,6 +41,7 @@ from datetime import datetime import hashlib from djmoney.contrib.exchange.models import convert_money from common.settings import currency_code_default +from common.models import InvenTreeSetting from InvenTree import helpers from InvenTree import validators @@ -556,7 +559,7 @@ class Part(MPTTModel): @property def full_name(self): - """ Format a 'full name' for this Part based on the format PART_NAME_FORMAT defined in part.settings file + """ Format a 'full name' for this Part based on the format PART_NAME_FORMAT defined in Inventree settings As a failsafe option, the following is done @@ -567,26 +570,13 @@ class Part(MPTTModel): Elements are joined by the | character """ - full_name = part_settings.PART_NAME_FORMAT - field_parser_regex_pattern = re.compile('{.*?}') - field_regex_pattern = re.compile('(?<=part\\.)[A-z]*') + # Add default_if_none to every jinja template variable + full_name_pattern = InvenTreeSetting.get_setting('PART_NAME_FORMAT') try: - - for field_parser in field_parser_regex_pattern.findall(part_settings.PART_NAME_FORMAT): - - # Each parser should contain a single field - field_name = field_regex_pattern.findall(field_parser)[0] - field_value = getattr(self, field_name) - - if field_value: - # replace the part.$field with field's value and remove the braces - parsed_value = field_parser.replace(f'part.{field_name}', field_value)[1:-1] - full_name = full_name.replace(field_parser, parsed_value) - - else: - # remove the field parser in full name - full_name = full_name.replace(field_parser, '') + context = {'part': self} + template_string = Template(full_name_pattern) + full_name = template_string.render(context) return full_name diff --git a/InvenTree/part/settings.py b/InvenTree/part/settings.py index c1d2f4c7e6..e345a9d88d 100644 --- a/InvenTree/part/settings.py +++ b/InvenTree/part/settings.py @@ -62,13 +62,3 @@ def part_trackable_default(): """ return InvenTreeSetting.get_setting('PART_TRACKABLE') - - -# CONSTANTS - -# Every brace pair is a field parser within which a field name of part has to be defined in the format part.$field_name -# When full name is constructed, It would be replaced by its value from the database and if the value is None, -# the entire field_parser i.e {.*} would be replaced with ''. -# Other characters inside and between the brace pairs would be copied as is. -PART_NAME_FORMAT = '{part.IPN | }{part.name}{ | part.revision}' - diff --git a/InvenTree/templates/InvenTree/settings/part.html b/InvenTree/templates/InvenTree/settings/part.html index 0ec5f56db6..ddd6fae1a9 100644 --- a/InvenTree/templates/InvenTree/settings/part.html +++ b/InvenTree/templates/InvenTree/settings/part.html @@ -17,6 +17,7 @@ {% include "InvenTree/settings/setting.html" with key="PART_IPN_REGEX" %} {% include "InvenTree/settings/setting.html" with key="PART_ALLOW_DUPLICATE_IPN" %} {% include "InvenTree/settings/setting.html" with key="PART_ALLOW_EDIT_IPN" %} + {% include "InvenTree/settings/setting.html" with key="PART_NAME_FORMAT" %} {% include "InvenTree/settings/setting.html" with key="PART_SHOW_PRICE_IN_FORMS" icon="fa-dollar-sign" %} {% include "InvenTree/settings/setting.html" with key="PART_SHOW_PRICE_IN_BOM" icon="fa-dollar-sign" %} {% include "InvenTree/settings/setting.html" with key="PART_SHOW_RELATED" icon="fa-random" %} From 4fddc656c495488405e5e19cbab1503ce6e4f2fb Mon Sep 17 00:00:00 2001 From: rocheparadox Date: Tue, 12 Oct 2021 19:51:21 +0530 Subject: [PATCH 3/9] removed unused import added unit tests for PART_NAME_FORMAT --- InvenTree/common/test_views.py | 22 ++++++++++++++++++++++ InvenTree/part/models.py | 1 - 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/InvenTree/common/test_views.py b/InvenTree/common/test_views.py index 56a244ba0c..c711742272 100644 --- a/InvenTree/common/test_views.py +++ b/InvenTree/common/test_views.py @@ -136,3 +136,25 @@ class SettingsViewTest(TestCase): for value in [False, 'False']: self.post(url, {'value': value}, valid=True) self.assertFalse(InvenTreeSetting.get_setting('PART_COMPONENT')) + + def test_part_name_format(self): + """ + Try posting some valid and invalid name formats for PART_NAME_FORMAT + """ + setting = InvenTreeSetting.get_setting_object('PART_NAME_FORMAT') + + # test default value + self.assertEqual(setting.value, "{{ part.IPN if part.IPN }} {{ '|' if part.IPN }} {{ part.name }} " + "{{ '|' if part.revision }} {{ part.revision }}") + + url = self.get_url(setting.pk) + + # Try posting an invalid part name format + invalid_values = ['{{asset.IPN}}', '{{part}}', '{{"|"}}'] + for invalid_value in invalid_values: + self.post(url, {'value': invalid_value}, valid=False) + + # try posting valid value + new_format = "{{ part.name if part.name }} {{ ' with revision ' if part.revision }} {{ part.revision }}" + self.post(url, {'value': new_format}, valid=True) + diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 7a4834cd7a..d5563382eb 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -8,7 +8,6 @@ import decimal import os import logging -import re from django.utils.translation import gettext_lazy as _ from django.core.exceptions import ValidationError From a01918d4b92cd831b4fde17b17091a1f5e646ee5 Mon Sep 17 00:00:00 2001 From: rocheparadox Date: Tue, 12 Oct 2021 19:54:09 +0530 Subject: [PATCH 4/9] removed blank line at the end of file --- InvenTree/common/test_views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/InvenTree/common/test_views.py b/InvenTree/common/test_views.py index c711742272..2c15f2e357 100644 --- a/InvenTree/common/test_views.py +++ b/InvenTree/common/test_views.py @@ -157,4 +157,3 @@ class SettingsViewTest(TestCase): # try posting valid value new_format = "{{ part.name if part.name }} {{ ' with revision ' if part.revision }} {{ part.revision }}" self.post(url, {'value': new_format}, valid=True) - From 0742fb063c9b09ffe85e7afa0e9e8e90d27104c1 Mon Sep 17 00:00:00 2001 From: rocheparadox Date: Wed, 13 Oct 2021 10:58:36 +0530 Subject: [PATCH 5/9] comment cleanup --- InvenTree/part/models.py | 1 - 1 file changed, 1 deletion(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index d5563382eb..cc75ca5821 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -569,7 +569,6 @@ class Part(MPTTModel): Elements are joined by the | character """ - # Add default_if_none to every jinja template variable full_name_pattern = InvenTreeSetting.get_setting('PART_NAME_FORMAT') try: From 147d2d46310be60f9acf70135c094f6db218f6bd Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 13 Oct 2021 17:40:50 +0200 Subject: [PATCH 6/9] fix default setting to not change current behaviour --- InvenTree/common/models.py | 4 ++-- InvenTree/common/test_views.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index f0c7c05b5a..8ccbf99986 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -726,8 +726,8 @@ class InvenTreeSetting(BaseInvenTreeSetting): 'PART_NAME_FORMAT': { 'name': _('Part Name Display Format'), 'description': _('Format to display the part name'), - 'default': "{{ part.IPN if part.IPN }} {{ '|' if part.IPN }} {{ part.name }} {{ '|' if part.revision }}" - " {{ part.revision }}", + 'default': "{{ part.IPN if part.IPN }}{{ ' | ' if part.IPN }}{{ part.name }}{{ ' | ' if part.revision }}" + "{{ part.revision if part.revision }}", 'validator': validate_part_name_format }, diff --git a/InvenTree/common/test_views.py b/InvenTree/common/test_views.py index 2c15f2e357..0f592e9bb3 100644 --- a/InvenTree/common/test_views.py +++ b/InvenTree/common/test_views.py @@ -144,8 +144,8 @@ class SettingsViewTest(TestCase): setting = InvenTreeSetting.get_setting_object('PART_NAME_FORMAT') # test default value - self.assertEqual(setting.value, "{{ part.IPN if part.IPN }} {{ '|' if part.IPN }} {{ part.name }} " - "{{ '|' if part.revision }} {{ part.revision }}") + self.assertEqual(setting.value, "{{ part.IPN if part.IPN }}{{ ' | ' if part.IPN }}{{ part.name }}" + "{{ ' | ' if part.revision }}{{ part.revision if part.revision }}") url = self.get_url(setting.pk) From 8cad687e43e848df1d2f7571c8b5ed5d62ec2301 Mon Sep 17 00:00:00 2001 From: rocheparadox Date: Thu, 14 Oct 2021 09:23:21 +0530 Subject: [PATCH 7/9] Moved part name format validation to InvenTree.validators.py from common.models validation to check if a field exists in part model --- InvenTree/InvenTree/validators.py | 31 +++++++++++++++++++++++++++++++ InvenTree/common/models.py | 22 +--------------------- InvenTree/common/test_views.py | 2 +- 3 files changed, 33 insertions(+), 22 deletions(-) diff --git a/InvenTree/InvenTree/validators.py b/InvenTree/InvenTree/validators.py index 1b6a6b3f0b..6ad49bc837 100644 --- a/InvenTree/InvenTree/validators.py +++ b/InvenTree/InvenTree/validators.py @@ -5,6 +5,7 @@ Custom field validators for InvenTree from django.conf import settings from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ +from django.core.exceptions import FieldDoesNotExist from moneyed import CURRENCIES @@ -156,3 +157,33 @@ def validate_overage(value): raise ValidationError( _("Overage must be an integer value or a percentage") ) + + +def validate_part_name_format(self): + """ + Validate part name format. + Make sure that each template container has a field of Part Model + """ + + jinja_template_regex = re.compile('{{.*?}}') + field_name_regex = re.compile('(?<=part\\.)[A-z]*') + for jinja_template in jinja_template_regex.findall(str(self)): + # make sure at least one and only one field is present inside the parser + field_names = field_name_regex.findall(jinja_template) + if len(field_names) < 1: + raise ValidationError({ + 'value': 'At least one field must be present inside a jinja template container i.e {{}}' + }) + + # Make sure that the field_name exists in Part model + from part.models import Part + + for field_name in field_names: + try: + Part._meta.get_field(field_name) + except FieldDoesNotExist: + raise ValidationError({ + 'value': f'{field_name} does not exist in Part Model' + }) + + return True diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 8ccbf99986..f85f6d9607 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -488,26 +488,6 @@ class InvenTreeSetting(BaseInvenTreeSetting): even if that key does not exist. """ - def validate_part_name_format(self): - """ - Validate part name format. - Make sure that each template container has a field of Part Model - """ - - jinja_template_regex = re.compile('{{.*?}}') - field_name_regex = re.compile('(?<=part\\.)[A-z]*') - for jinja_template in jinja_template_regex.findall(str(self)): - # make sure at least one and only one field is present inside the parser - field_name = field_name_regex.findall(jinja_template) - if len(field_name) < 1: - raise ValidationError({ - 'value': 'At least one field must be present inside a jinja template container i.e {{}}' - }) - - # TODO: Make sure that the field_name exists in Part model - - return True - """ Dict of all global settings values: @@ -728,7 +708,7 @@ class InvenTreeSetting(BaseInvenTreeSetting): 'description': _('Format to display the part name'), 'default': "{{ part.IPN if part.IPN }}{{ ' | ' if part.IPN }}{{ part.name }}{{ ' | ' if part.revision }}" "{{ part.revision if part.revision }}", - 'validator': validate_part_name_format + 'validator': InvenTree.fields.validate_part_name_format }, 'REPORT_DEBUG_MODE': { diff --git a/InvenTree/common/test_views.py b/InvenTree/common/test_views.py index 0f592e9bb3..76a0a4516e 100644 --- a/InvenTree/common/test_views.py +++ b/InvenTree/common/test_views.py @@ -150,7 +150,7 @@ class SettingsViewTest(TestCase): url = self.get_url(setting.pk) # Try posting an invalid part name format - invalid_values = ['{{asset.IPN}}', '{{part}}', '{{"|"}}'] + invalid_values = ['{{asset.IPN}}', '{{part}}', '{{"|"}}', '{{part.falcon}}'] for invalid_value in invalid_values: self.post(url, {'value': invalid_value}, valid=False) From ee9e01fc226723c01ae59bf164c23b824bb694c3 Mon Sep 17 00:00:00 2001 From: rocheparadox Date: Thu, 14 Oct 2021 09:26:26 +0530 Subject: [PATCH 8/9] removed unused import --- InvenTree/common/models.py | 1 - 1 file changed, 1 deletion(-) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index f85f6d9607..cadb64814d 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -9,7 +9,6 @@ from __future__ import unicode_literals import os import decimal import math -import re from django.db import models, transaction from django.contrib.auth.models import User From 5a6bea3452bb76ebdc131b8a9e6d774d1a94ccc7 Mon Sep 17 00:00:00 2001 From: rocheparadox Date: Thu, 14 Oct 2021 09:35:26 +0530 Subject: [PATCH 9/9] improve regex for part name format validation proper import of validation --- InvenTree/InvenTree/validators.py | 2 +- InvenTree/common/models.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/InvenTree/InvenTree/validators.py b/InvenTree/InvenTree/validators.py index 6ad49bc837..76d485cef9 100644 --- a/InvenTree/InvenTree/validators.py +++ b/InvenTree/InvenTree/validators.py @@ -166,7 +166,7 @@ def validate_part_name_format(self): """ jinja_template_regex = re.compile('{{.*?}}') - field_name_regex = re.compile('(?<=part\\.)[A-z]*') + field_name_regex = re.compile('(?<=part\\.)[A-z]+') for jinja_template in jinja_template_regex.findall(str(self)): # make sure at least one and only one field is present inside the parser field_names = field_name_regex.findall(jinja_template) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index cadb64814d..d4f26af739 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -25,6 +25,7 @@ from django.core.exceptions import ValidationError import InvenTree.helpers import InvenTree.fields +import InvenTree.validators import logging @@ -707,7 +708,7 @@ class InvenTreeSetting(BaseInvenTreeSetting): 'description': _('Format to display the part name'), 'default': "{{ part.IPN if part.IPN }}{{ ' | ' if part.IPN }}{{ part.name }}{{ ' | ' if part.revision }}" "{{ part.revision if part.revision }}", - 'validator': InvenTree.fields.validate_part_name_format + 'validator': InvenTree.validators.validate_part_name_format }, 'REPORT_DEBUG_MODE': {