mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-25 18:08:38 -06:00
Adds support for multi select custom fields
This commit is contained in:
parent
c1e56a3717
commit
7234b45a0c
@ -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
|
||||
|
@ -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')
|
||||
})
|
||||
)
|
||||
|
||||
|
@ -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.")
|
||||
|
@ -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):
|
||||
|
||||
|
@ -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 %}
|
||||
|
Loading…
Reference in New Issue
Block a user