Adds support for multi select custom fields

This commit is contained in:
TomGrozev 2020-12-11 13:10:17 +11:00
parent c1e56a3717
commit 7234b45a0c
5 changed files with 61 additions and 8 deletions

View File

@ -62,6 +62,7 @@ All end-to-end cable paths are now cached using the new CablePath backend model.
### Enhancements
* [#5451](https://github.com/netbox-community/netbox/issues/5451) - Add ability to create multi selection custom fields
* [#609](https://github.com/netbox-community/netbox/issues/609) - Add min/max value and regex validation for custom fields
* [#1503](https://github.com/netbox-community/netbox/issues/1503) - Allow assigment of secrets to virtual machines
* [#1692](https://github.com/netbox-community/netbox/issues/1692) - Allow assigment of inventory items to parent items in web UI

View File

@ -114,7 +114,7 @@ class CustomFieldAdmin(admin.ModelAdmin):
}),
('Choices', {
'description': 'A selection field must have two or more choices assigned to it.',
'fields': ('choices',)
'fields': ('choices','multiple_selection')
})
)

View File

@ -12,7 +12,7 @@ from django.utils.safestring import mark_safe
from extras.choices import *
from extras.utils import FeatureQuery
from utilities.forms import CSVChoiceField, DatePicker, LaxURLField, StaticSelect2, add_blank_choice
from utilities.forms import CSVChoiceField, DatePicker, LaxURLField, StaticSelect2Multiple, StaticSelect2, add_blank_choice
from utilities.querysets import RestrictedQuerySet
from utilities.validators import validate_regex
@ -151,6 +151,10 @@ class CustomField(models.Model):
null=True,
help_text='Comma-separated list of available choices (for selection fields)'
)
multiple_selection = models.BooleanField(
default=False,
help_text='Allow multiple choice selection'
)
objects = CustomFieldManager()
@ -203,6 +207,12 @@ class CustomField(models.Model):
'choices': "Choices may be set only for custom selection fields."
})
# Multiple selection can only be set for selection fields
if self.multiple_selection and self.type != CustomFieldTypeChoices.TYPE_SELECT:
raise ValidationError({
'multiple_selection': "Multiple selection can only be set for selection fields."
})
# A selection field must have at least two choices defined
if self.type == CustomFieldTypeChoices.TYPE_SELECT and self.choices and len(self.choices) < 2:
raise ValidationError({
@ -262,9 +272,13 @@ class CustomField(models.Model):
if set_initial and default_choice:
initial = default_choice
field_class = CSVChoiceField if for_csv_import else forms.ChoiceField
# Use multiple choice field if multi_selection is enabled
default_field_class = forms.MultipleChoiceField if self.multiple_selection else forms.ChoiceField
select_widget = StaticSelect2Multiple() if self.multiple_selection else StaticSelect2()
field_class = CSVChoiceField if for_csv_import else default_field_class
field = field_class(
choices=choices, required=required, initial=initial, widget=StaticSelect2()
choices=choices, required=required, initial=initial, widget=select_widget
)
# URL
@ -324,10 +338,14 @@ class CustomField(models.Model):
# Validate selected choice
if self.type == CustomFieldTypeChoices.TYPE_SELECT:
if value not in self.choices:
raise ValidationError(
f"Invalid choice ({value}). Available choices are: {', '.join(self.choices)}"
)
if not isinstance(value, list):
value = [value]
for val in value:
if val not in self.choices:
raise ValidationError(
f"Invalid choice ({value}). Available choices are: {', '.join(self.choices)}"
)
elif self.required:
raise ValidationError("Required field cannot be empty.")

View File

@ -90,6 +90,38 @@ class CustomFieldTest(TestCase):
# Delete the custom field
cf.delete()
def test_multiple_select_field(self):
obj_type = ContentType.objects.get_for_model(Site)
# Create a custom field
cf = CustomField(
type=CustomFieldTypeChoices.TYPE_SELECT,
name='my_field',
required=False,
choices=['Option A', 'Option B', 'Option C'],
multiple_selection=True
)
cf.save()
cf.content_types.set([obj_type])
# Assign a value to the first Site
site = Site.objects.first()
site.custom_field_data[cf.name] = ['Option A', 'Option B']
site.save()
# Retrieve the stored value
site.refresh_from_db()
self.assertEqual(site.custom_field_data[cf.name], ['Option A', 'Option B'])
# Delete the stored value
site.custom_field_data.pop(cf.name)
site.save()
site.refresh_from_db()
self.assertIsNone(site.custom_field_data.get(cf.name))
# Delete the custom field
cf.delete()
class CustomFieldManagerTest(TestCase):

View File

@ -15,6 +15,8 @@
<i class="mdi mdi-close-thick text-danger" title="False"></i>
{% elif field.type == 'url' and value %}
<a href="{{ value }}">{{ value|truncatechars:70 }}</a>
{% elif field.type == 'select' and field.multiple_selection == True and value is not None %}
{{ value|join:", " }}
{% elif value is not None %}
{{ value }}
{% elif field.required %}