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>
This commit is contained in:
Daniel Sheppard 2023-09-06 09:47:18 -05:00 committed by GitHub
parent 9d851924c8
commit 2d1457b94b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 108 additions and 15 deletions

View File

@ -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( _("Invalid choice ({value}) for choice set {choiceset}.").format(
value=value, choices=', '.join(self.choices) 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 choice(s) ({value}) for choice set {choiceset}.").format(
invalid_choices=', '.join(value), available_choices=', '.join(self.choices)) 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."))

View File

@ -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):