From 477828d4fb4ee9635db0c6f6e3a0b8c362f2de07 Mon Sep 17 00:00:00 2001 From: Sam King Date: Tue, 23 Jul 2024 14:38:19 +1000 Subject: [PATCH] Fixes #16782: Add object filtering for custom fields --- docs/customization/custom-fields.md | 2 ++ netbox/extras/api/serializers_/customfields.py | 2 +- netbox/extras/forms/model_forms.py | 2 +- .../0116_customfield_related_object_filter.py | 18 ++++++++++++++++++ netbox/extras/models/customfields.py | 12 +++++++++++- netbox/extras/tests/test_filtersets.py | 2 +- netbox/templates/extras/customfield.html | 4 ++++ 7 files changed, 38 insertions(+), 4 deletions(-) create mode 100644 netbox/extras/migrations/0116_customfield_related_object_filter.py diff --git a/docs/customization/custom-fields.md b/docs/customization/custom-fields.md index 1f9a4a8bf..1e500d203 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 only show 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/netbox/extras/api/serializers_/customfields.py b/netbox/extras/api/serializers_/customfields.py index 082047e94..0f393556c 100644 --- a/netbox/extras/api/serializers_/customfields.py +++ b/netbox/extras/api/serializers_/customfields.py @@ -64,7 +64,7 @@ class CustomFieldSerializer(ValidatedModelSerializer): fields = [ 'id', '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', 'validation_regex', + 'is_cloneable', 'default', 'related_object_filter', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', '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 ebd6e6c08..5c6bea9a8 100644 --- a/netbox/extras/forms/model_forms.py +++ b/netbox/extras/forms/model_forms.py @@ -63,7 +63,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', name=_('Validation')), ) diff --git a/netbox/extras/migrations/0116_customfield_related_object_filter.py b/netbox/extras/migrations/0116_customfield_related_object_filter.py new file mode 100644 index 000000000..6bf2abdeb --- /dev/null +++ b/netbox/extras/migrations/0116_customfield_related_object_filter.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.7 on 2024-07-19 07:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('extras', '0115_convert_dashboard_widgets'), + ] + + 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 d8f02ec6c..c1ebf3804 100644 --- a/netbox/extras/models/customfields.py +++ b/netbox/extras/models/customfields.py @@ -153,6 +153,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'), @@ -499,7 +507,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 @@ -510,6 +519,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 b68c02efc..88cfc4777 100644 --- a/netbox/extras/tests/test_filtersets.py +++ b/netbox/extras/tests/test_filtersets.py @@ -26,7 +26,7 @@ User = get_user_model() 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 4efd1e4d0..86883347d 100644 --- a/netbox/templates/extras/customfield.html +++ b/netbox/templates/extras/customfield.html @@ -52,6 +52,10 @@ {% trans "Default Value" %} {{ object.default }} + + {% trans "Related object filter" %} + {{ object.related_object_filter }} +