diff --git a/docs/release-notes/version-3.0.md b/docs/release-notes/version-3.0.md index 727d067ac..b4dff6676 100644 --- a/docs/release-notes/version-3.0.md +++ b/docs/release-notes/version-3.0.md @@ -12,6 +12,7 @@ * [#7083](https://github.com/netbox-community/netbox/issues/7083) - Correct labeling for VM memory attribute * [#7084](https://github.com/netbox-community/netbox/issues/7084) - Fix KeyError exception when editing access VLAN on an interface * [#7089](https://github.com/netbox-community/netbox/issues/7089) - Fix ContentTypeFilterSet not filtering on q filter +* [#7093](https://github.com/netbox-community/netbox/issues/7093) - Multi-select custom field filters should employ exact match * [#7096](https://github.com/netbox-community/netbox/issues/7096) - Home links should honor `BASE_PATH` configuration * [#7101](https://github.com/netbox-community/netbox/issues/7101) - Enforce `MAX_PAGE_SIZE` for table and REST API pagination diff --git a/netbox/extras/filters.py b/netbox/extras/filters.py index aef2046fd..b37aaf40e 100644 --- a/netbox/extras/filters.py +++ b/netbox/extras/filters.py @@ -14,6 +14,7 @@ EXACT_FILTER_TYPES = ( CustomFieldTypeChoices.TYPE_DATE, CustomFieldTypeChoices.TYPE_INTEGER, CustomFieldTypeChoices.TYPE_SELECT, + CustomFieldTypeChoices.TYPE_MULTISELECT, ) @@ -35,7 +36,9 @@ class CustomFieldFilter(django_filters.Filter): self.field_name = f'custom_field_data__{self.field_name}' - if custom_field.type not in EXACT_FILTER_TYPES: + if custom_field.type == CustomFieldTypeChoices.TYPE_MULTISELECT: + self.lookup_expr = 'has_key' + elif custom_field.type not in EXACT_FILTER_TYPES: if custom_field.filter_logic == CustomFieldFilterLogicChoices.FILTER_LOOSE: self.lookup_expr = 'icontains' diff --git a/netbox/extras/tests/test_customfields.py b/netbox/extras/tests/test_customfields.py index b1bf10be6..c2a2da3dc 100644 --- a/netbox/extras/tests/test_customfields.py +++ b/netbox/extras/tests/test_customfields.py @@ -681,7 +681,12 @@ class CustomFieldFilterTest(TestCase): cf.content_types.set([obj_type]) # Selection filtering - cf = CustomField(name='cf8', type=CustomFieldTypeChoices.TYPE_URL, choices=['Foo', 'Bar', 'Baz']) + cf = CustomField(name='cf8', type=CustomFieldTypeChoices.TYPE_SELECT, choices=['Foo', 'Bar', 'Baz']) + cf.save() + cf.content_types.set([obj_type]) + + # Multiselect filtering + cf = CustomField(name='cf9', type=CustomFieldTypeChoices.TYPE_MULTISELECT, choices=['A', 'AA', 'B', 'C']) cf.save() cf.content_types.set([obj_type]) @@ -695,6 +700,7 @@ class CustomFieldFilterTest(TestCase): 'cf6': 'http://foo.example.com/', 'cf7': 'http://foo.example.com/', 'cf8': 'Foo', + 'cf9': ['A', 'B'], }), Site(name='Site 2', slug='site-2', custom_field_data={ 'cf1': 200, @@ -705,9 +711,9 @@ class CustomFieldFilterTest(TestCase): 'cf6': 'http://bar.example.com/', 'cf7': 'http://bar.example.com/', 'cf8': 'Bar', + 'cf9': ['AA', 'B'], }), - Site(name='Site 3', slug='site-3', custom_field_data={ - }), + Site(name='Site 3', slug='site-3'), ]) def test_filter_integer(self): @@ -730,3 +736,10 @@ class CustomFieldFilterTest(TestCase): def test_filter_select(self): self.assertEqual(self.filterset({'cf_cf8': 'Foo'}, self.queryset).qs.count(), 1) + self.assertEqual(self.filterset({'cf_cf8': 'Bar'}, self.queryset).qs.count(), 1) + self.assertEqual(self.filterset({'cf_cf8': 'Baz'}, self.queryset).qs.count(), 0) + + def test_filter_multiselect(self): + self.assertEqual(self.filterset({'cf_cf9': 'A'}, self.queryset).qs.count(), 1) + self.assertEqual(self.filterset({'cf_cf9': 'B'}, self.queryset).qs.count(), 2) + self.assertEqual(self.filterset({'cf_cf9': 'C'}, self.queryset).qs.count(), 0)