Quellcode durchsuchen

Fixes: #13682 - Fix custom field exceptions and validation (#13685)

* Fixes: #13682 - Fix custom field exceptions and validation

* Add tests

* Remove default setting for multi-select/multi-object and return slice of choices and annotate.

* Remove redundant default choice valiadtion; introduce values property on CustomFieldChoiceSet

* Refactor test

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
Daniel Sheppard vor 2 Jahren
Ursprung
Commit
2d1457b94b
2 geänderte Dateien mit 108 neuen und 15 gelöschten Zeilen
  1. 17 15
      netbox/extras/models/customfields.py
  2. 91 0
      netbox/extras/tests/test_customfields.py

+ 17 - 15
netbox/extras/models/customfields.py

@@ -282,7 +282,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
                 raise ValidationError({
                 raise ValidationError({
                     'default': _(
                     'default': _(
                         'Invalid default value "{default}": {message}'
                         'Invalid default value "{default}": {message}'
-                    ).format(default=self.default, message=self.message)
+                    ).format(default=self.default, message=err.message)
                 })
                 })
 
 
         # Minimum/maximum values can be set only for numeric fields
         # Minimum/maximum values can be set only for numeric fields
@@ -317,14 +317,6 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
                 'choice_set': _("Choices may be set only on selection fields.")
                 'choice_set': _("Choices may be set only on selection fields.")
             })
             })
 
 
-        # A selection field's default (if any) must be present in its available choices
-        if self.type == CustomFieldTypeChoices.TYPE_SELECT and self.default and self.default not in self.choices:
-            raise ValidationError({
-                'default': _(
-                    "The specified default value ({default}) is not listed as an available choice."
-                ).format(default=self.default)
-            })
-
         # Object fields must define an object_type; other fields must not
         # Object fields must define an object_type; other fields must not
         if self.type in (CustomFieldTypeChoices.TYPE_OBJECT, CustomFieldTypeChoices.TYPE_MULTIOBJECT):
         if self.type in (CustomFieldTypeChoices.TYPE_OBJECT, CustomFieldTypeChoices.TYPE_MULTIOBJECT):
             if not self.object_type:
             if not self.object_type:
@@ -650,19 +642,22 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
 
 
             # Validate selected choice
             # Validate selected choice
             elif self.type == CustomFieldTypeChoices.TYPE_SELECT:
             elif self.type == CustomFieldTypeChoices.TYPE_SELECT:
-                if value not in [c[0] for c in self.choices]:
+                if value not in self.choice_set.values:
                     raise ValidationError(
                     raise ValidationError(
-                        _("Invalid choice ({value}). Available choices are: {choices}").format(
-                            value=value, choices=', '.join(self.choices)
+                        _("Invalid choice ({value}) for choice set {choiceset}.").format(
+                            value=value,
+                            choiceset=self.choice_set
                         )
                         )
                     )
                     )
 
 
             # Validate all selected choices
             # Validate all selected choices
             elif self.type == CustomFieldTypeChoices.TYPE_MULTISELECT:
             elif self.type == CustomFieldTypeChoices.TYPE_MULTISELECT:
-                if not set(value).issubset([c[0] for c in self.choices]):
+                if not set(value).issubset(self.choice_set.values):
                     raise ValidationError(
                     raise ValidationError(
-                        _("Invalid choice(s) ({invalid_choices}). Available choices are: {available_choices}").format(
-                            invalid_choices=', '.join(value), available_choices=', '.join(self.choices))
+                        _("Invalid choice(s) ({value}) for choice set {choiceset}.").format(
+                            value=value,
+                            choiceset=self.choice_set
+                        )
                     )
                     )
 
 
             # Validate selected object
             # Validate selected object
@@ -747,6 +742,13 @@ class CustomFieldChoiceSet(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel
     def choices_count(self):
     def choices_count(self):
         return len(self.choices)
         return len(self.choices)
 
 
+    @property
+    def values(self):
+        """
+        Returns an iterator of the valid choice values.
+        """
+        return (x[0] for x in self.choices)
+
     def clean(self):
     def clean(self):
         if not self.base_choices and not self.extra_choices:
         if not self.base_choices and not self.extra_choices:
             raise ValidationError(_("Must define base or extra choices."))
             raise ValidationError(_("Must define base or extra choices."))

+ 91 - 0
netbox/extras/tests/test_customfields.py

@@ -427,6 +427,97 @@ class CustomFieldTest(TestCase):
         self.assertNotIn('field1', site.custom_field_data)
         self.assertNotIn('field1', site.custom_field_data)
         self.assertEqual(site.custom_field_data['field2'], FIELD_DATA)
         self.assertEqual(site.custom_field_data['field2'], FIELD_DATA)
 
 
+    def test_default_value_validation(self):
+        choiceset = CustomFieldChoiceSet.objects.create(
+            name="Test Choice Set",
+            extra_choices=(
+                ('choice1', 'Choice 1'),
+                ('choice2', 'Choice 2'),
+            )
+        )
+        site = Site.objects.create(name='Site 1', slug='site-1')
+        object_type = ContentType.objects.get_for_model(Site)
+
+        # Text
+        CustomField(name='test', type='text', required=True, default="Default text").full_clean()
+
+        # Integer
+        CustomField(name='test', type='integer', required=True, default=1).full_clean()
+        with self.assertRaises(ValidationError):
+            CustomField(name='test', type='integer', required=True, default='xxx').full_clean()
+
+        # Boolean
+        CustomField(name='test', type='boolean', required=True, default=True).full_clean()
+        with self.assertRaises(ValidationError):
+            CustomField(name='test', type='boolean', required=True, default='xxx').full_clean()
+
+        # Date
+        CustomField(name='test', type='date', required=True, default="2023-02-25").full_clean()
+        with self.assertRaises(ValidationError):
+            CustomField(name='test', type='date', required=True, default='xxx').full_clean()
+
+        # Datetime
+        CustomField(name='test', type='datetime', required=True, default="2023-02-25 02:02:02").full_clean()
+        with self.assertRaises(ValidationError):
+            CustomField(name='test', type='datetime', required=True, default='xxx').full_clean()
+
+        # URL
+        CustomField(name='test', type='url', required=True, default="https://www.netbox.dev").full_clean()
+
+        # JSON
+        CustomField(name='test', type='json', required=True, default='{"test": "object"}').full_clean()
+
+        # Selection
+        CustomField(name='test', type='select', required=True, choice_set=choiceset, default='choice1').full_clean()
+        with self.assertRaises(ValidationError):
+            CustomField(name='test', type='select', required=True, choice_set=choiceset, default='xxx').full_clean()
+
+        # Multi-select
+        CustomField(
+            name='test',
+            type='multiselect',
+            required=True,
+            choice_set=choiceset,
+            default=['choice1']  # Single default choice
+        ).full_clean()
+        CustomField(
+            name='test',
+            type='multiselect',
+            required=True,
+            choice_set=choiceset,
+            default=['choice1', 'choice2']  # Multiple default choices
+        ).full_clean()
+        with self.assertRaises(ValidationError):
+            CustomField(
+                name='test',
+                type='multiselect',
+                required=True,
+                choice_set=choiceset,
+                default=['xxx']
+            ).full_clean()
+
+        # Object
+        CustomField(name='test', type='object', required=True, object_type=object_type, default=site.pk).full_clean()
+        with self.assertRaises(ValidationError):
+            CustomField(name='test', type='object', required=True, object_type=object_type, default="xxx").full_clean()
+
+        # Multi-object
+        CustomField(
+            name='test',
+            type='multiobject',
+            required=True,
+            object_type=object_type,
+            default=[site.pk]
+        ).full_clean()
+        with self.assertRaises(ValidationError):
+            CustomField(
+                name='test',
+                type='multiobject',
+                required=True,
+                object_type=object_type,
+                default=["xxx"]
+            ).full_clean()
+
 
 
 class CustomFieldManagerTest(TestCase):
 class CustomFieldManagerTest(TestCase):