From 116a423d8fe05b528ea9aac7e6c76aeda1729307 Mon Sep 17 00:00:00 2001 From: bctiemann Date: Tue, 24 Sep 2024 10:02:49 -0400 Subject: [PATCH] Closes: #16837 - Fix type__empty filter in character-based filters (#17574) * Fix type__empty filter in character-based filters * Add tests --- netbox/dcim/tests/test_filtersets.py | 4 ++++ netbox/netbox/filtersets.py | 7 +++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py index e2d52a609..afb360d76 100644 --- a/netbox/dcim/tests/test_filtersets.py +++ b/netbox/dcim/tests/test_filtersets.py @@ -5247,6 +5247,10 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests): def test_type(self): params = {'type': [CableTypeChoices.TYPE_CAT3, CableTypeChoices.TYPE_CAT5E]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + params = {'type__empty': 'true'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8) + params = {'type__empty': 'false'} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6) def test_status(self): params = {'status': [LinkStatusChoices.STATUS_CONNECTED]} diff --git a/netbox/netbox/filtersets.py b/netbox/netbox/filtersets.py index ac43fe57f..e3bd33298 100644 --- a/netbox/netbox/filtersets.py +++ b/netbox/netbox/filtersets.py @@ -133,7 +133,7 @@ class BaseFilterSet(django_filters.FilterSet): django_filters.ModelChoiceFilter, django_filters.ModelMultipleChoiceFilter, TagFilter - )) or existing_filter.extra.get('choices'): + )): # These filter types support only negation return FILTER_NEGATION_LOOKUP_MAP @@ -172,6 +172,7 @@ class BaseFilterSet(django_filters.FilterSet): # Create new filters for each lookup expression in the map for lookup_name, lookup_expr in lookup_map.items(): new_filter_name = f'{existing_filter_name}__{lookup_name}' + existing_filter_extra = deepcopy(existing_filter.extra) try: if existing_filter_name in cls.declared_filters: @@ -179,6 +180,8 @@ class BaseFilterSet(django_filters.FilterSet): # create the new filter with the same type because there is no guarantee the defined type # is the same as the default type for the field resolve_field(field, lookup_expr) # Will raise FieldLookupError if the lookup is invalid + for field_to_remove in ('choices', 'null_value'): + existing_filter_extra.pop(field_to_remove, None) filter_cls = django_filters.BooleanFilter if lookup_expr == 'empty' else type(existing_filter) new_filter = filter_cls( field_name=field_name, @@ -186,7 +189,7 @@ class BaseFilterSet(django_filters.FilterSet): label=existing_filter.label, exclude=existing_filter.exclude, distinct=existing_filter.distinct, - **existing_filter.extra + **existing_filter_extra ) elif hasattr(existing_filter, 'custom_field'): # Filter is for a custom field