From 1ddc1a6781faefbcc3c0dc1d80b9419519ce9a96 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 1 Mar 2021 14:51:29 -0500 Subject: [PATCH] Closes #5451: Add support for multiple-selection custom fields --- docs/additional-features/custom-fields.md | 3 ++ docs/release-notes/version-2.11.md | 1 + netbox/extras/choices.py | 2 ++ netbox/extras/models/customfields.py | 32 +++++++++++++++---- netbox/templates/inc/custom_fields_panel.html | 2 ++ 5 files changed, 33 insertions(+), 7 deletions(-) diff --git a/docs/additional-features/custom-fields.md b/docs/additional-features/custom-fields.md index 91bf03379..5c74c6744 100644 --- a/docs/additional-features/custom-fields.md +++ b/docs/additional-features/custom-fields.md @@ -16,6 +16,7 @@ Custom fields must be created through the admin UI under Extras > Custom Fields. * Date: A date in ISO 8601 format (YYYY-MM-DD) * URL: This will be presented as a link in the web UI * Selection: A selection of one of several pre-defined custom choices +* Multiple selection: A selection field which supports the assignment of multiple values Each custom field must have a name; this should be a simple database-friendly string, e.g. `tps_report`. You may also assign a corresponding human-friendly label (e.g. "TPS report"); the label will be displayed on web forms. A weight is also required: Higher-weight fields will be ordered lower within a form. (The default weight is 100.) If a description is provided, it will appear beneath the field in a form. @@ -39,6 +40,8 @@ Each custom selection field must have at least two choices. These are specified If a default value is specified for a selection field, it must exactly match one of the provided choices. +The value of a multiple selection field will always return a list, even if only one value is selected. + ## Custom Fields and the REST API When retrieving an object via the REST API, all of its custom data will be included within the `custom_fields` attribute. For example, below is the partial output of a site with two custom fields defined: diff --git a/docs/release-notes/version-2.11.md b/docs/release-notes/version-2.11.md index 856b61910..20ca17b4e 100644 --- a/docs/release-notes/version-2.11.md +++ b/docs/release-notes/version-2.11.md @@ -8,6 +8,7 @@ * [#5370](https://github.com/netbox-community/netbox/issues/5370) - Extend custom field support to organizational models * [#5401](https://github.com/netbox-community/netbox/issues/5401) - Extend custom field support to device component models +* [#5451](https://github.com/netbox-community/netbox/issues/5451) - Add support for multiple-selection custom fields ### Other Changes diff --git a/netbox/extras/choices.py b/netbox/extras/choices.py index 45f8ac31f..47c3a1039 100644 --- a/netbox/extras/choices.py +++ b/netbox/extras/choices.py @@ -13,6 +13,7 @@ class CustomFieldTypeChoices(ChoiceSet): TYPE_DATE = 'date' TYPE_URL = 'url' TYPE_SELECT = 'select' + TYPE_MULTISELECT = 'multiselect' CHOICES = ( (TYPE_TEXT, 'Text'), @@ -21,6 +22,7 @@ class CustomFieldTypeChoices(ChoiceSet): (TYPE_DATE, 'Date'), (TYPE_URL, 'URL'), (TYPE_SELECT, 'Selection'), + (TYPE_MULTISELECT, 'Multiple selection'), ) diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py index ecc11e52b..8b97877a4 100644 --- a/netbox/extras/models/customfields.py +++ b/netbox/extras/models/customfields.py @@ -11,7 +11,9 @@ from django.utils.safestring import mark_safe from extras.choices import * from extras.utils import FeatureQuery from netbox.models import BigIDModel -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 @@ -153,7 +155,10 @@ class CustomField(BigIDModel): }) # Choices can be set only on selection fields - if self.choices and self.type != CustomFieldTypeChoices.TYPE_SELECT: + if self.choices and self.type not in ( + CustomFieldTypeChoices.TYPE_SELECT, + CustomFieldTypeChoices.TYPE_MULTISELECT + ): raise ValidationError({ 'choices': "Choices may be set only for custom selection fields." }) @@ -206,7 +211,7 @@ class CustomField(BigIDModel): field = forms.DateField(required=required, initial=initial, widget=DatePicker()) # Select - elif self.type == CustomFieldTypeChoices.TYPE_SELECT: + elif self.type in (CustomFieldTypeChoices.TYPE_SELECT, CustomFieldTypeChoices.TYPE_MULTISELECT): choices = [(c, c) for c in self.choices] default_choice = self.default if self.default in self.choices else None @@ -217,10 +222,16 @@ class CustomField(BigIDModel): if set_initial and default_choice: initial = default_choice - field_class = CSVChoiceField if for_csv_import else forms.ChoiceField - field = field_class( - choices=choices, required=required, initial=initial, widget=StaticSelect2() - ) + if self.type == CustomFieldTypeChoices.TYPE_SELECT: + field_class = CSVChoiceField if for_csv_import else forms.ChoiceField + field = field_class( + choices=choices, required=required, initial=initial, widget=StaticSelect2() + ) + else: + field_class = CSVChoiceField if for_csv_import else forms.MultipleChoiceField + field = field_class( + choices=choices, required=required, initial=initial, widget=StaticSelect2Multiple() + ) # URL elif self.type == CustomFieldTypeChoices.TYPE_URL: @@ -285,5 +296,12 @@ class CustomField(BigIDModel): f"Invalid choice ({value}). Available choices are: {', '.join(self.choices)}" ) + # Validate all selected choices + if self.type == CustomFieldTypeChoices.TYPE_MULTISELECT: + if not set(value).issubset(self.choices): + raise ValidationError( + f"Invalid choice(s) ({', '.join(value)}). Available choices are: {', '.join(self.choices)}" + ) + elif self.required: raise ValidationError("Required field cannot be empty.") diff --git a/netbox/templates/inc/custom_fields_panel.html b/netbox/templates/inc/custom_fields_panel.html index d5f858f15..bd80974eb 100644 --- a/netbox/templates/inc/custom_fields_panel.html +++ b/netbox/templates/inc/custom_fields_panel.html @@ -15,6 +15,8 @@ {% elif field.type == 'url' and value %} {{ value|truncatechars:70 }} + {% elif field.type == 'multiselect' and value %} + {{ value|join:", " }} {% elif value is not None %} {{ value }} {% elif field.required %}