From 06688d8a5674f596394c8f9d1c6b2cffcc8a0130 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 10 Nov 2025 10:29:24 +0000 Subject: [PATCH] Add validator for ParameterTemplate model type - Update migrations - Make Parameter class abstract (for now) - Validators --- .../migrations/0040_parametertemplate.py | 12 ++++++ .../migrations/0041_auto_20251028_1112.py | 8 ++++ src/backend/InvenTree/common/models.py | 29 ++++++++++++- src/backend/InvenTree/common/serializers.py | 20 +++++++++ src/backend/InvenTree/common/validators.py | 41 +++++++++++++++++++ 5 files changed, 108 insertions(+), 2 deletions(-) diff --git a/src/backend/InvenTree/common/migrations/0040_parametertemplate.py b/src/backend/InvenTree/common/migrations/0040_parametertemplate.py index 8590fc329f..99d8411c20 100644 --- a/src/backend/InvenTree/common/migrations/0040_parametertemplate.py +++ b/src/backend/InvenTree/common/migrations/0040_parametertemplate.py @@ -1,5 +1,6 @@ # Generated by Django 4.2.25 on 2025-10-28 11:11 +import common.validators import InvenTree.models import InvenTree.validators from django.db import migrations, models @@ -62,6 +63,17 @@ class Migration(migrations.Migration): verbose_name="Description", ), ), + ( + "model_type", + models.CharField( + blank=True, + default="", + help_text="Target model type for this parameter", + max_length=100, + validators=[common.validators.validate_parameter_template_model_type], + verbose_name="Model type", + ), + ), ( "checkbox", models.BooleanField( diff --git a/src/backend/InvenTree/common/migrations/0041_auto_20251028_1112.py b/src/backend/InvenTree/common/migrations/0041_auto_20251028_1112.py index 204fb9afe7..76693fd110 100644 --- a/src/backend/InvenTree/common/migrations/0041_auto_20251028_1112.py +++ b/src/backend/InvenTree/common/migrations/0041_auto_20251028_1112.py @@ -8,6 +8,11 @@ def copy_templates(old_model, new_model): templates = [] + # Clear out all existing instances + new_model.objects.all().delete() + + assert new_model.objects.count() == 0 + for template in old_model.objects.all(): templates.append(new_model( name=template.name, @@ -23,6 +28,8 @@ def copy_templates(old_model, new_model): new_model.objects.bulk_create(templates) print(f"Migrated {len(templates)} ParameterTemplate instances.") + assert new_model.objects.count() == len(templates) + def forward_copy_templates(apps, schema_editor): """Forward migration: copy from PartParameterTemplate to ParameterTemplate.""" @@ -43,6 +50,7 @@ def reverse_copy_templates(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ + ("part", "0132_partparametertemplate_selectionlist"), ("common", "0040_parametertemplate"), ] diff --git a/src/backend/InvenTree/common/models.py b/src/backend/InvenTree/common/models.py index e5c8612662..a9162f2a92 100644 --- a/src/backend/InvenTree/common/models.py +++ b/src/backend/InvenTree/common/models.py @@ -72,14 +72,21 @@ class RenderMeta(enums.ChoicesMeta): """Metaclass for rendering choices.""" choice_fnc = None + allow_blank: bool = False + blank_label: str = '------' @property def choices(self): """Return a list of choices for the enum class.""" fnc = getattr(self, 'choice_fnc', None) if fnc: - return fnc() - return [] + options = fnc() + options = [] + + if self.allow_blank: + options.insert(0, ('', self.blank_label)) + + return options class RenderChoices(models.TextChoices, metaclass=RenderMeta): # type: ignore @@ -2385,6 +2392,12 @@ class ParameterTemplate( class Meta: """Metaclass options for the ParameterTemplate model.""" + class ModelChoices(RenderChoices): + """Model choices for parameter templates.""" + + allow_blank = True + choice_fnc = common.validators.parameter_model_options + @staticmethod def get_api_url() -> str: """Return the API URL associated with the ParameterTemplate model.""" @@ -2467,6 +2480,15 @@ class ParameterTemplate( return [x.strip() for x in self.choices.split(',') if x.strip()] + model_type = models.CharField( + max_length=100, + default='', + blank=True, + validators=[common.validators.validate_parameter_template_model_type], + verbose_name=_('Model type'), + help_text=_('Target model type for this parameter'), + ) + name = models.CharField( max_length=100, verbose_name=_('Name'), @@ -2532,6 +2554,9 @@ class Parameter( class Meta: """Meta options for Parameter model.""" + # TODO: Make this non-abstract, actually implement... + abstract = True + verbose_name = _('Parameter') verbose_name_plural = _('Parameters') unique_together = [['model_type', 'model_id', 'template']] diff --git a/src/backend/InvenTree/common/serializers.py b/src/backend/InvenTree/common/serializers.py index cd21b0ff63..b7461cfb77 100644 --- a/src/backend/InvenTree/common/serializers.py +++ b/src/backend/InvenTree/common/serializers.py @@ -711,11 +711,31 @@ class ParameterTemplateSerializer( 'name', 'units', 'description', + 'model_type', 'checkbox', 'choices', 'selectionlist', ] + def __init__(self, *args, **kwargs): + """Override the model_type field to provide dynamic choices.""" + super().__init__(*args, **kwargs) + + if len(self.fields['model_type'].choices) == 0: + self.fields[ + 'model_type' + ].choices = common.validators.attachment_model_options() + + # Note: The choices are overridden at run-time on class initialization + model_type = serializers.ChoiceField( + label=_('Model Type'), + default='', + choices=common.validators.attachment_model_options(), + required=False, + allow_blank=True, + allow_null=True, + ) + class IconSerializer(serializers.Serializer): """Serializer for an icon.""" diff --git a/src/backend/InvenTree/common/validators.py b/src/backend/InvenTree/common/validators.py index 02f8f7781a..dcdcd693d0 100644 --- a/src/backend/InvenTree/common/validators.py +++ b/src/backend/InvenTree/common/validators.py @@ -10,6 +10,47 @@ import common.icons from common.settings import get_global_setting +def parameter_model_types(): + """Return a list of valid parameter model choices.""" + import InvenTree.models + + return list( + InvenTree.helpers_model.getModelsWithMixin( + InvenTree.models.InvenTreeParameterMixin + ) + ) + + +def parameter_model_options(): + """Return a list of options for models which support parameters.""" + return [ + (model.__name__.lower(), model._meta.verbose_name) + for model in parameter_model_types() + ] + + +def validate_parameter_model_type(value: str): + """Ensure that the provided parameter model is valid.""" + model_names = [el[0] for el in parameter_model_options()] + if value not in model_names: + raise ValidationError('Model type does not support parameters') + + +def validate_parameter_template_model_type(value: str): + """Ensure that the provided model type is valid. + + Note: A ParameterTemplate may have a blank model type. + """ + value = str(value).strip() + + if not value: + # Empty values are allowed + return + + # Pass any other value to the Parameter model type validator + validate_parameter_model_type(value) + + def attachment_model_types(): """Return a list of valid attachment model choices.""" import InvenTree.models