From 650898719ed123eec821a351fa5e7973f5555059 Mon Sep 17 00:00:00 2001 From: samk-acw <55128650+samk-acw@users.noreply.github.com> Date: Tue, 30 Jul 2024 05:45:48 +1000 Subject: [PATCH] Fixes #16782: Add object filtering for custom fields (#16994) * Fixes #16782: Add object filtering for custom fields * Add validation for related_object_filter * Extend documentation & misc cleanup --------- Co-authored-by: Jeremy Stretch --- docs/customization/custom-fields.md | 2 ++ docs/models/extras/customfield.md | 7 ++++++ .../extras/api/serializers_/customfields.py | 2 +- netbox/extras/forms/model_forms.py | 2 +- .../0120_customfield_related_object_filter.py | 18 +++++++++++++++ netbox/extras/models/customfields.py | 23 ++++++++++++++++++- netbox/extras/tests/test_filtersets.py | 2 +- netbox/templates/extras/customfield.html | 8 +++++++ 8 files changed, 60 insertions(+), 4 deletions(-) create mode 100644 netbox/extras/migrations/0120_customfield_related_object_filter.py diff --git a/docs/customization/custom-fields.md b/docs/customization/custom-fields.md index 1f9a4a8bf..4658cc7e6 100644 --- a/docs/customization/custom-fields.md +++ b/docs/customization/custom-fields.md @@ -74,6 +74,8 @@ If a default value is specified for a selection field, it must exactly match one An object or multi-object custom field can be used to refer to a particular NetBox object or objects as the "value" for a custom field. These custom fields must define an `object_type`, which determines the type of object to which custom field instances point. +By default, an object choice field will make all objects of that type available for selection in the drop-down. The list choices can be filtered to show only objects with certain values by providing a `query_params` dict in the Related Object Filter field, as a JSON value. More information about `query_params` can be found [here](./custom-scripts.md#objectvar). + ## Custom Fields in Templates Several features within NetBox, such as export templates and webhooks, utilize Jinja2 templating. For convenience, objects which support custom field assignment expose custom field data through the `cf` property. This is a bit cleaner than accessing custom field data through the actual field (`custom_field_data`). diff --git a/docs/models/extras/customfield.md b/docs/models/extras/customfield.md index 2353bc2b9..164ce3a74 100644 --- a/docs/models/extras/customfield.md +++ b/docs/models/extras/customfield.md @@ -42,6 +42,13 @@ The type of data this field holds. This must be one of the following: For object and multiple-object fields only. Designates the type of NetBox object being referenced. +### Related Object Filter + +For object and multi-object custom fields, a filter may be defined to limit the available objects when populating a field value. This filter maps object attributes to values. For example, `{"status": "active"}` will include only objects with a status of "active." + +!!! warning + This setting is employed for convenience only, and should not be relied upon to enforce data integrity. + ### Weight A numeric weight used to override alphabetic ordering of fields by name. Custom fields with a lower weight will be listed before those with a higher weight. (Note that weight applies within the context of a custom field group, if defined.) diff --git a/netbox/extras/api/serializers_/customfields.py b/netbox/extras/api/serializers_/customfields.py index 9675cb173..2c8e7c127 100644 --- a/netbox/extras/api/serializers_/customfields.py +++ b/netbox/extras/api/serializers_/customfields.py @@ -62,7 +62,7 @@ class CustomFieldSerializer(ValidatedModelSerializer): fields = [ 'id', 'url', 'display_url', 'display', 'object_types', 'type', 'related_object_type', 'data_type', 'name', 'label', 'group_name', 'description', 'required', 'search_weight', 'filter_logic', 'ui_visible', - 'ui_editable', 'is_cloneable', 'default', 'weight', 'validation_minimum', 'validation_maximum', + 'ui_editable', 'is_cloneable', 'default', 'related_object_filter', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'validation_unique', 'choice_set', 'comments', 'created', 'last_updated', ] brief_fields = ('id', 'url', 'display', 'name', 'description') diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py index b4221b960..ce013f7c6 100644 --- a/netbox/extras/forms/model_forms.py +++ b/netbox/extras/forms/model_forms.py @@ -67,7 +67,7 @@ class CustomFieldForm(forms.ModelForm): FieldSet( 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'weight', 'is_cloneable', name=_('Behavior') ), - FieldSet('default', 'choice_set', name=_('Values')), + FieldSet('default', 'choice_set', 'related_object_filter', name=_('Values')), FieldSet( 'validation_minimum', 'validation_maximum', 'validation_regex', 'validation_unique', name=_('Validation') ), diff --git a/netbox/extras/migrations/0120_customfield_related_object_filter.py b/netbox/extras/migrations/0120_customfield_related_object_filter.py new file mode 100644 index 000000000..431ce5bf9 --- /dev/null +++ b/netbox/extras/migrations/0120_customfield_related_object_filter.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.7 on 2024-07-26 01:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('extras', '0119_eventrule_event_types'), + ] + + operations = [ + migrations.AddField( + model_name='customfield', + name='related_object_filter', + field=models.JSONField(blank=True, null=True), + ), + ] diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py index 1d84a3f4f..79ba75098 100644 --- a/netbox/extras/models/customfields.py +++ b/netbox/extras/models/customfields.py @@ -154,6 +154,14 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): 'Default value for the field (must be a JSON value). Encapsulate strings with double quotes (e.g. "Foo").' ) ) + related_object_filter = models.JSONField( + blank=True, + null=True, + help_text=_( + 'Filter the object selection choices using a query_params dict (must be a JSON value).' + 'Encapsulate strings with double quotes (e.g. "Foo").' + ) + ) weight = models.PositiveSmallIntegerField( default=100, verbose_name=_('display weight'), @@ -373,6 +381,17 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): .format(type=self.get_type_display()) }) + # Related object filter can be set only for object-type fields, and must contain a dictionary mapping (if set) + if self.related_object_filter is not None: + if self.type not in (CustomFieldTypeChoices.TYPE_OBJECT, CustomFieldTypeChoices.TYPE_MULTIOBJECT): + raise ValidationError({ + 'related_object_filter': _("A related object filter can be defined only for object fields.") + }) + if type(self.related_object_filter) is not dict: + raise ValidationError({ + 'related_object_filter': _("Filter must be defined as a dictionary mapping attributes to values.") + }) + def serialize(self, value): """ Prepare a value for storage as JSON data. @@ -511,7 +530,8 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): field = field_class( queryset=model.objects.all(), required=required, - initial=initial + initial=initial, + query_params=self.related_object_filter ) # Multiple objects @@ -522,6 +542,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): queryset=model.objects.all(), required=required, initial=initial, + query_params=self.related_object_filter ) # Text diff --git a/netbox/extras/tests/test_filtersets.py b/netbox/extras/tests/test_filtersets.py index 144dec5d0..9048d5fd9 100644 --- a/netbox/extras/tests/test_filtersets.py +++ b/netbox/extras/tests/test_filtersets.py @@ -23,7 +23,7 @@ from virtualization.models import Cluster, ClusterGroup, ClusterType class CustomFieldTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = CustomField.objects.all() filterset = CustomFieldFilterSet - ignore_fields = ('default',) + ignore_fields = ('default', 'related_object_filter') @classmethod def setUpTestData(cls): diff --git a/netbox/templates/extras/customfield.html b/netbox/templates/extras/customfield.html index fa0986778..dffdbbd93 100644 --- a/netbox/templates/extras/customfield.html +++ b/netbox/templates/extras/customfield.html @@ -52,6 +52,14 @@ {% trans "Default Value" %} {{ object.default }} + + {% trans "Related object filter" %} + {% if object.related_object_filter %} +
{{ object.related_object_filter|json }}
+ {% else %} + {{ ''|placeholder }} + {% endif %} +