Browse Source

Closes #5451: Add support for multiple-selection custom fields

Jeremy Stretch 5 năm trước cách đây
mục cha
commit
1ddc1a6781

+ 3 - 0
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:

+ 1 - 0
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
 

+ 2 - 0
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'),
     )
 
 

+ 25 - 7
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.")

+ 2 - 0
netbox/templates/inc/custom_fields_panel.html

@@ -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 == 'multiselect' and value %}
+                                {{ value|join:", " }}
                             {% elif value is not None %}
                                 {{ value }}
                             {% elif field.required %}