Przeglądaj źródła

Fixes #5510: Fix filtering by boolean custom fields

Jeremy Stretch 5 lat temu
rodzic
commit
b09112941a

+ 1 - 0
docs/release-notes/version-2.10.md

@@ -19,6 +19,7 @@
 * [#5498](https://github.com/netbox-community/netbox/issues/5498) - Fix filtering rack reservations by username
 * [#5499](https://github.com/netbox-community/netbox/issues/5499) - Fix filtering of displayed device/VM interfaces by regex
 * [#5507](https://github.com/netbox-community/netbox/issues/5507) - Fix custom field data assignment via UI for IP addresses, secrets
+* [#5510](https://github.com/netbox-community/netbox/issues/5510) - Fix filtering by boolean custom fields
 
 ---
 

+ 12 - 14
netbox/extras/filters.py

@@ -2,6 +2,7 @@ import django_filters
 from django.contrib.auth.models import User
 from django.contrib.contenttypes.models import ContentType
 from django.db.models import Q
+from django.forms import DateField, IntegerField, NullBooleanField
 
 from dcim.models import DeviceRole, Platform, Region, Site
 from tenancy.models import Tenant, TenantGroup
@@ -38,24 +39,21 @@ class CustomFieldFilter(django_filters.Filter):
     """
     def __init__(self, custom_field, *args, **kwargs):
         self.custom_field = custom_field
-        super().__init__(*args, **kwargs)
 
-    def filter(self, queryset, value):
+        if custom_field.type == CustomFieldTypeChoices.TYPE_INTEGER:
+            self.field_class = IntegerField
+        elif custom_field.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
+            self.field_class = NullBooleanField
+        elif custom_field.type == CustomFieldTypeChoices.TYPE_DATE:
+            self.field_class = DateField
 
-        # Skip filter on empty value
-        if value is None or not value.strip():
-            return queryset
+        super().__init__(*args, **kwargs)
 
-        # Apply the assigned filter logic (exact or loose)
-        if (
-            self.custom_field.type in EXACT_FILTER_TYPES or
-            self.custom_field.filter_logic == CustomFieldFilterLogicChoices.FILTER_EXACT
-        ):
-            kwargs = {f'custom_field_data__{self.field_name}': value}
-        else:
-            kwargs = {f'custom_field_data__{self.field_name}__icontains': value}
+        self.field_name = f'custom_field_data__{self.field_name}'
 
-        return queryset.filter(**kwargs)
+        if custom_field.type not in EXACT_FILTER_TYPES:
+            if custom_field.filter_logic == CustomFieldFilterLogicChoices.FILTER_LOOSE:
+                self.lookup_expr = 'icontains'
 
 
 class CustomFieldModelFilterSet(django_filters.FilterSet):

+ 100 - 0
netbox/extras/tests/test_customfields.py

@@ -3,6 +3,7 @@ from django.core.exceptions import ValidationError
 from django.urls import reverse
 from rest_framework import status
 
+from dcim.filters import SiteFilterSet
 from dcim.forms import SiteCSVForm
 from dcim.models import Site, Rack
 from extras.choices import *
@@ -597,3 +598,102 @@ class CustomFieldModelTest(TestCase):
 
         site.cf['baz'] = 'def'
         site.clean()
+
+
+class CustomFieldFilterTest(TestCase):
+    queryset = Site.objects.all()
+    filterset = SiteFilterSet
+
+    @classmethod
+    def setUpTestData(cls):
+        obj_type = ContentType.objects.get_for_model(Site)
+
+        # Integer filtering
+        cf = CustomField(name='cf1', type=CustomFieldTypeChoices.TYPE_INTEGER)
+        cf.save()
+        cf.content_types.set([obj_type])
+
+        # Boolean filtering
+        cf = CustomField(name='cf2', type=CustomFieldTypeChoices.TYPE_BOOLEAN)
+        cf.save()
+        cf.content_types.set([obj_type])
+
+        # Exact text filtering
+        cf = CustomField(name='cf3', type=CustomFieldTypeChoices.TYPE_TEXT,
+                         filter_logic=CustomFieldFilterLogicChoices.FILTER_EXACT)
+        cf.save()
+        cf.content_types.set([obj_type])
+
+        # Loose text filtering
+        cf = CustomField(name='cf4', type=CustomFieldTypeChoices.TYPE_TEXT,
+                         filter_logic=CustomFieldFilterLogicChoices.FILTER_LOOSE)
+        cf.save()
+        cf.content_types.set([obj_type])
+
+        # Date filtering
+        cf = CustomField(name='cf5', type=CustomFieldTypeChoices.TYPE_DATE)
+        cf.save()
+        cf.content_types.set([obj_type])
+
+        # Exact URL filtering
+        cf = CustomField(name='cf6', type=CustomFieldTypeChoices.TYPE_URL,
+                         filter_logic=CustomFieldFilterLogicChoices.FILTER_EXACT)
+        cf.save()
+        cf.content_types.set([obj_type])
+
+        # Loose URL filtering
+        cf = CustomField(name='cf7', type=CustomFieldTypeChoices.TYPE_URL,
+                         filter_logic=CustomFieldFilterLogicChoices.FILTER_LOOSE)
+        cf.save()
+        cf.content_types.set([obj_type])
+
+        # Selection filtering
+        cf = CustomField(name='cf8', type=CustomFieldTypeChoices.TYPE_URL, choices=['Foo', 'Bar', 'Baz'])
+        cf.save()
+        cf.content_types.set([obj_type])
+
+        Site.objects.bulk_create([
+            Site(name='Site 1', slug='site-1', custom_field_data={
+                'cf1': 100,
+                'cf2': True,
+                'cf3': 'foo',
+                'cf4': 'foo',
+                'cf5': '2016-06-26',
+                'cf6': 'http://foo.example.com/',
+                'cf7': 'http://foo.example.com/',
+                'cf8': 'Foo',
+            }),
+            Site(name='Site 2', slug='site-2', custom_field_data={
+                'cf1': 200,
+                'cf2': False,
+                'cf3': 'foobar',
+                'cf4': 'foobar',
+                'cf5': '2016-06-27',
+                'cf6': 'http://bar.example.com/',
+                'cf7': 'http://bar.example.com/',
+                'cf8': 'Bar',
+            }),
+            Site(name='Site 3', slug='site-3', custom_field_data={
+            }),
+        ])
+
+    def test_filter_integer(self):
+        self.assertEqual(self.filterset({'cf_cf1': 100}, self.queryset).qs.count(), 1)
+
+    def test_filter_boolean(self):
+        self.assertEqual(self.filterset({'cf_cf2': True}, self.queryset).qs.count(), 1)
+        self.assertEqual(self.filterset({'cf_cf2': False}, self.queryset).qs.count(), 1)
+
+    def test_filter_text(self):
+        self.assertEqual(self.filterset({'cf_cf3': 'foo'}, self.queryset).qs.count(), 1)
+        self.assertEqual(self.filterset({'cf_cf4': 'foo'}, self.queryset).qs.count(), 2)
+
+    def test_filter_date(self):
+        self.assertEqual(self.filterset({'cf_cf5': '2016-06-26'}, self.queryset).qs.count(), 1)
+
+    def test_filter_url(self):
+        self.assertEqual(self.filterset({'cf_cf6': 'http://foo.example.com/'}, self.queryset).qs.count(), 1)
+        self.assertEqual(self.filterset({'cf_cf7': 'example.com'}, self.queryset).qs.count(), 2)
+
+    def test_filter_select(self):
+        self.assertEqual(self.filterset({'cf_cf8': 'Foo'}, self.queryset).qs.count(), 1)