From a40ab9ffb1bfc9404ceb38d607d98baaaa1ab78e Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 8 Jul 2022 14:59:16 -0400 Subject: [PATCH] Fixes #9657: Fix filtering for custom fields and webhooks in the UI --- docs/release-notes/version-3.2.md | 1 + netbox/extras/filtersets.py | 15 ++++-- netbox/extras/forms/filtersets.py | 14 +++--- netbox/extras/tests/test_filtersets.py | 65 +++++++++++++++++++++++++- 4 files changed, 85 insertions(+), 10 deletions(-) diff --git a/docs/release-notes/version-3.2.md b/docs/release-notes/version-3.2.md index 3bd3773cf..9b4dc800d 100644 --- a/docs/release-notes/version-3.2.md +++ b/docs/release-notes/version-3.2.md @@ -14,6 +14,7 @@ * [#8854](https://github.com/netbox-community/netbox/issues/8854) - Fix `REMOTE_AUTH_DEFAULT_GROUPS` for social-auth backends * [#9575](https://github.com/netbox-community/netbox/issues/9575) - Fix AttributeError exception for FHRP group with an IP address assigned * [#9597](https://github.com/netbox-community/netbox/issues/9597) - Include `installed_module` in module bay REST API serializer +* [#9657](https://github.com/netbox-community/netbox/issues/9657) - Fix filtering for custom fields and webhooks in the UI * [#9682](https://github.com/netbox-community/netbox/issues/9682) - Fix bulk assignment of ASNs to sites --- diff --git a/netbox/extras/filtersets.py b/netbox/extras/filtersets.py index 25477fbda..bb8d16c42 100644 --- a/netbox/extras/filtersets.py +++ b/netbox/extras/filtersets.py @@ -32,6 +32,9 @@ class WebhookFilterSet(BaseFilterSet): method='search', label='Search', ) + content_type_id = MultiValueNumberFilter( + field_name='content_types__id' + ) content_types = ContentTypeFilter() http_method = django_filters.MultipleChoiceFilter( choices=WebhookHttpMethodChoices @@ -40,8 +43,8 @@ class WebhookFilterSet(BaseFilterSet): class Meta: model = Webhook fields = [ - 'id', 'content_types', 'name', 'type_create', 'type_update', 'type_delete', 'payload_url', 'enabled', - 'http_method', 'http_content_type', 'secret', 'ssl_verification', 'ca_file_path', + 'id', 'name', 'type_create', 'type_update', 'type_delete', 'payload_url', 'enabled', 'http_method', + 'http_content_type', 'secret', 'ssl_verification', 'ca_file_path', ] def search(self, queryset, name, value): @@ -58,11 +61,17 @@ class CustomFieldFilterSet(BaseFilterSet): method='search', label='Search', ) + type = django_filters.MultipleChoiceFilter( + choices=CustomFieldTypeChoices + ) + content_type_id = MultiValueNumberFilter( + field_name='content_types__id' + ) content_types = ContentTypeFilter() class Meta: model = CustomField - fields = ['id', 'content_types', 'name', 'required', 'filter_logic', 'weight', 'description'] + fields = ['id', 'name', 'required', 'filter_logic', 'weight', 'description'] def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/extras/forms/filtersets.py b/netbox/extras/forms/filtersets.py index 5d66c8be8..71bcfd4c2 100644 --- a/netbox/extras/forms/filtersets.py +++ b/netbox/extras/forms/filtersets.py @@ -32,12 +32,13 @@ __all__ = ( class CustomFieldFilterForm(FilterForm): fieldsets = ( (None, ('q',)), - ('Attributes', ('type', 'content_types', 'weight', 'required')), + ('Attributes', ('type', 'content_type_id', 'weight', 'required')), ) - content_types = ContentTypeMultipleChoiceField( + content_type_id = ContentTypeMultipleChoiceField( queryset=ContentType.objects.all(), limit_choices_to=FeatureQuery('custom_fields'), - required=False + required=False, + label='Object type' ) type = MultipleChoiceField( choices=CustomFieldTypeChoices, @@ -110,13 +111,14 @@ class ExportTemplateFilterForm(FilterForm): class WebhookFilterForm(FilterForm): fieldsets = ( (None, ('q',)), - ('Attributes', ('content_types', 'http_method', 'enabled')), + ('Attributes', ('content_type_id', 'http_method', 'enabled')), ('Events', ('type_create', 'type_update', 'type_delete')), ) - content_types = ContentTypeMultipleChoiceField( + content_type_id = ContentTypeMultipleChoiceField( queryset=ContentType.objects.all(), limit_choices_to=FeatureQuery('webhooks'), - required=False + required=False, + label='Object type' ) http_method = MultipleChoiceField( choices=WebhookHttpMethodChoices, diff --git a/netbox/extras/tests/test_filtersets.py b/netbox/extras/tests/test_filtersets.py index bdb8de9db..aa9d724a4 100644 --- a/netbox/extras/tests/test_filtersets.py +++ b/netbox/extras/tests/test_filtersets.py @@ -7,7 +7,9 @@ from django.test import TestCase from circuits.models import Provider from dcim.models import DeviceRole, DeviceType, Manufacturer, Platform, Rack, Region, Site, SiteGroup -from extras.choices import JournalEntryKindChoices, ObjectChangeActionChoices +from extras.choices import ( + CustomFieldTypeChoices, CustomFieldFilterLogicChoices, JournalEntryKindChoices, ObjectChangeActionChoices, +) from extras.filtersets import * from extras.models import * from ipam.models import IPAddress @@ -16,6 +18,65 @@ from utilities.testing import BaseFilterSetTests, ChangeLoggedFilterSetTests, cr from virtualization.models import Cluster, ClusterGroup, ClusterType +class CustomFieldTestCase(TestCase, BaseFilterSetTests): + queryset = CustomField.objects.all() + filterset = CustomFieldFilterSet + + @classmethod + def setUpTestData(cls): + content_types = ContentType.objects.filter(model__in=['site', 'rack', 'device']) + + custom_fields = ( + CustomField( + name='Custom Field 1', + type=CustomFieldTypeChoices.TYPE_TEXT, + required=True, + weight=100, + filter_logic=CustomFieldFilterLogicChoices.FILTER_LOOSE + ), + CustomField( + name='Custom Field 2', + type=CustomFieldTypeChoices.TYPE_INTEGER, + required=False, + weight=200, + filter_logic=CustomFieldFilterLogicChoices.FILTER_EXACT + ), + CustomField( + name='Custom Field 3', + type=CustomFieldTypeChoices.TYPE_BOOLEAN, + required=False, + weight=300, + filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED + ), + ) + CustomField.objects.bulk_create(custom_fields) + custom_fields[0].content_types.add(content_types[0]) + custom_fields[1].content_types.add(content_types[1]) + custom_fields[2].content_types.add(content_types[2]) + + def test_name(self): + params = {'name': ['Custom Field 1', 'Custom Field 2']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_content_types(self): + params = {'content_types': 'dcim.site'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + params = {'content_type_id': [ContentType.objects.get_for_model(Site).pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + + def test_required(self): + params = {'required': True} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + + def test_weight(self): + params = {'weight': [100, 200]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_filter_logic(self): + params = {'filter_logic': CustomFieldFilterLogicChoices.FILTER_LOOSE} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + + class WebhookTestCase(TestCase, BaseFilterSetTests): queryset = Webhook.objects.all() filterset = WebhookFilterSet @@ -62,6 +123,8 @@ class WebhookTestCase(TestCase, BaseFilterSetTests): def test_content_types(self): params = {'content_types': 'dcim.site'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + params = {'content_type_id': [ContentType.objects.get_for_model(Site).pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) def test_type_create(self): params = {'type_create': True}