Skip to content

Commit 634681a

Browse files
committed
Fixes #13606: Fix filtering by null for multiselect custom fields
1 parent 031b754 commit 634681a

File tree

3 files changed

+24
-9
lines changed

3 files changed

+24
-9
lines changed

netbox/extras/models/customfields.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
from django.core.validators import RegexValidator, ValidationError
1111
from django.db import models
1212
from django.urls import reverse
13-
from django.utils.html import escape
1413
from django.utils.safestring import mark_safe
1514
from django.utils.translation import gettext_lazy as _
1615

@@ -571,8 +570,7 @@ def to_filter(self, lookup_expr=None):
571570

572571
# Multiselect
573572
elif self.type == CustomFieldTypeChoices.TYPE_MULTISELECT:
574-
filter_class = filters.MultiValueCharFilter
575-
kwargs['lookup_expr'] = 'has_key'
573+
filter_class = filters.MultiValueArrayFilter
576574

577575
# Object
578576
elif self.type == CustomFieldTypeChoices.TYPE_OBJECT:

netbox/extras/tests/test_customfields.py

+7-6
Original file line numberDiff line numberDiff line change
@@ -1329,7 +1329,7 @@ def setUpTestData(cls):
13291329

13301330
choice_set = CustomFieldChoiceSet.objects.create(
13311331
name='Custom Field Choice Set 1',
1332-
extra_choices=(('a', 'A'), ('b', 'B'), ('c', 'C'), ('x', 'X'))
1332+
extra_choices=(('a', 'A'), ('b', 'B'), ('c', 'C'))
13331333
)
13341334

13351335
# Integer filtering
@@ -1435,7 +1435,7 @@ def setUpTestData(cls):
14351435
'cf7': 'http://a.example.com',
14361436
'cf8': 'http://a.example.com',
14371437
'cf9': 'A',
1438-
'cf10': ['A', 'X'],
1438+
'cf10': ['A', 'B'],
14391439
'cf11': manufacturers[0].pk,
14401440
'cf12': [manufacturers[0].pk, manufacturers[3].pk],
14411441
}),
@@ -1449,7 +1449,7 @@ def setUpTestData(cls):
14491449
'cf7': 'http://b.example.com',
14501450
'cf8': 'http://b.example.com',
14511451
'cf9': 'B',
1452-
'cf10': ['B', 'X'],
1452+
'cf10': ['B', 'C'],
14531453
'cf11': manufacturers[1].pk,
14541454
'cf12': [manufacturers[1].pk, manufacturers[3].pk],
14551455
}),
@@ -1463,7 +1463,7 @@ def setUpTestData(cls):
14631463
'cf7': 'http://c.example.com',
14641464
'cf8': 'http://c.example.com',
14651465
'cf9': 'C',
1466-
'cf10': ['C', 'X'],
1466+
'cf10': None,
14671467
'cf11': manufacturers[2].pk,
14681468
'cf12': [manufacturers[2].pk, manufacturers[3].pk],
14691469
}),
@@ -1531,8 +1531,9 @@ def test_filter_select(self):
15311531
self.assertEqual(self.filterset({'cf_cf9': ['A', 'B']}, self.queryset).qs.count(), 2)
15321532

15331533
def test_filter_multiselect(self):
1534-
self.assertEqual(self.filterset({'cf_cf10': ['A', 'B']}, self.queryset).qs.count(), 2)
1535-
self.assertEqual(self.filterset({'cf_cf10': ['X']}, self.queryset).qs.count(), 3)
1534+
self.assertEqual(self.filterset({'cf_cf10': ['A']}, self.queryset).qs.count(), 1)
1535+
self.assertEqual(self.filterset({'cf_cf10': ['A', 'C']}, self.queryset).qs.count(), 2)
1536+
self.assertEqual(self.filterset({'cf_cf10': ['null']}, self.queryset).qs.count(), 1)
15361537

15371538
def test_filter_object(self):
15381539
manufacturer_ids = Manufacturer.objects.values_list('id', flat=True)

netbox/utilities/filters.py

+16
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
__all__ = (
1010
'ContentTypeFilter',
1111
'MACAddressFilter',
12+
'MultiValueArrayFilter',
1213
'MultiValueCharFilter',
1314
'MultiValueDateFilter',
1415
'MultiValueDateTimeFilter',
@@ -85,6 +86,21 @@ class MultiValueTimeFilter(django_filters.MultipleChoiceFilter):
8586
field_class = multivalue_field_factory(forms.TimeField)
8687

8788

89+
@extend_schema_field(OpenApiTypes.STR)
90+
class MultiValueArrayFilter(django_filters.MultipleChoiceFilter):
91+
field_class = multivalue_field_factory(forms.CharField)
92+
93+
def __init__(self, *args, lookup_expr='contains', **kwargs):
94+
# Set default lookup_expr to 'contains'
95+
super().__init__(*args, lookup_expr=lookup_expr, **kwargs)
96+
97+
def get_filter_predicate(self, v):
98+
# If filtering for null values, ignore lookup_expr
99+
if v is None:
100+
return {self.field_name: None}
101+
return super().get_filter_predicate(v)
102+
103+
88104
class MACAddressFilter(django_filters.CharFilter):
89105
pass
90106

0 commit comments

Comments
 (0)