mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Fixes #5510: Fix filtering by boolean custom fields
This commit is contained in:
@ -19,6 +19,7 @@
|
|||||||
* [#5498](https://github.com/netbox-community/netbox/issues/5498) - Fix filtering rack reservations by username
|
* [#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
|
* [#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
|
* [#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
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ import django_filters
|
|||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
from django.forms import DateField, IntegerField, NullBooleanField
|
||||||
|
|
||||||
from dcim.models import DeviceRole, Platform, Region, Site
|
from dcim.models import DeviceRole, Platform, Region, Site
|
||||||
from tenancy.models import Tenant, TenantGroup
|
from tenancy.models import Tenant, TenantGroup
|
||||||
@ -38,24 +39,21 @@ class CustomFieldFilter(django_filters.Filter):
|
|||||||
"""
|
"""
|
||||||
def __init__(self, custom_field, *args, **kwargs):
|
def __init__(self, custom_field, *args, **kwargs):
|
||||||
self.custom_field = custom_field
|
self.custom_field = custom_field
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
def filter(self, queryset, value):
|
self.field_name = f'custom_field_data__{self.field_name}'
|
||||||
|
|
||||||
# Skip filter on empty value
|
if custom_field.type not in EXACT_FILTER_TYPES:
|
||||||
if value is None or not value.strip():
|
if custom_field.filter_logic == CustomFieldFilterLogicChoices.FILTER_LOOSE:
|
||||||
return queryset
|
self.lookup_expr = 'icontains'
|
||||||
|
|
||||||
# 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}
|
|
||||||
|
|
||||||
return queryset.filter(**kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class CustomFieldModelFilterSet(django_filters.FilterSet):
|
class CustomFieldModelFilterSet(django_filters.FilterSet):
|
||||||
|
@ -3,6 +3,7 @@ from django.core.exceptions import ValidationError
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
|
from dcim.filters import SiteFilterSet
|
||||||
from dcim.forms import SiteCSVForm
|
from dcim.forms import SiteCSVForm
|
||||||
from dcim.models import Site, Rack
|
from dcim.models import Site, Rack
|
||||||
from extras.choices import *
|
from extras.choices import *
|
||||||
@ -597,3 +598,102 @@ class CustomFieldModelTest(TestCase):
|
|||||||
|
|
||||||
site.cf['baz'] = 'def'
|
site.cf['baz'] = 'def'
|
||||||
site.clean()
|
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)
|
||||||
|
Reference in New Issue
Block a user