mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-26 18:38: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
|
### 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
|
* [#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
|
* [#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
|
* [#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', {
|
('Choices', {
|
||||||
'description': 'A selection field must have two or more choices assigned to it.',
|
'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.choices import *
|
||||||
from extras.utils import FeatureQuery
|
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.querysets import RestrictedQuerySet
|
||||||
from utilities.validators import validate_regex
|
from utilities.validators import validate_regex
|
||||||
|
|
||||||
@ -151,6 +151,10 @@ class CustomField(models.Model):
|
|||||||
null=True,
|
null=True,
|
||||||
help_text='Comma-separated list of available choices (for selection fields)'
|
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()
|
objects = CustomFieldManager()
|
||||||
|
|
||||||
@ -203,6 +207,12 @@ class CustomField(models.Model):
|
|||||||
'choices': "Choices may be set only for custom selection fields."
|
'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
|
# A selection field must have at least two choices defined
|
||||||
if self.type == CustomFieldTypeChoices.TYPE_SELECT and self.choices and len(self.choices) < 2:
|
if self.type == CustomFieldTypeChoices.TYPE_SELECT and self.choices and len(self.choices) < 2:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
@ -262,9 +272,13 @@ class CustomField(models.Model):
|
|||||||
if set_initial and default_choice:
|
if set_initial and default_choice:
|
||||||
initial = 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(
|
field = field_class(
|
||||||
choices=choices, required=required, initial=initial, widget=StaticSelect2()
|
choices=choices, required=required, initial=initial, widget=select_widget
|
||||||
)
|
)
|
||||||
|
|
||||||
# URL
|
# URL
|
||||||
@ -324,10 +338,14 @@ class CustomField(models.Model):
|
|||||||
|
|
||||||
# Validate selected choice
|
# Validate selected choice
|
||||||
if self.type == CustomFieldTypeChoices.TYPE_SELECT:
|
if self.type == CustomFieldTypeChoices.TYPE_SELECT:
|
||||||
if value not in self.choices:
|
if not isinstance(value, list):
|
||||||
raise ValidationError(
|
value = [value]
|
||||||
f"Invalid choice ({value}). Available choices are: {', '.join(self.choices)}"
|
|
||||||
)
|
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:
|
elif self.required:
|
||||||
raise ValidationError("Required field cannot be empty.")
|
raise ValidationError("Required field cannot be empty.")
|
||||||
|
@ -90,6 +90,38 @@ class CustomFieldTest(TestCase):
|
|||||||
# Delete the custom field
|
# Delete the custom field
|
||||||
cf.delete()
|
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):
|
class CustomFieldManagerTest(TestCase):
|
||||||
|
|
||||||
|
@ -15,6 +15,8 @@
|
|||||||
<i class="mdi mdi-close-thick text-danger" title="False"></i>
|
<i class="mdi mdi-close-thick text-danger" title="False"></i>
|
||||||
{% elif field.type == 'url' and value %}
|
{% elif field.type == 'url' and value %}
|
||||||
<a href="{{ value }}">{{ value|truncatechars:70 }}</a>
|
<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 %}
|
{% elif value is not None %}
|
||||||
{{ value }}
|
{{ value }}
|
||||||
{% elif field.required %}
|
{% elif field.required %}
|
||||||
|
Loading…
Reference in New Issue
Block a user