Browse Source

Fixes #1649: Correct fitlering on null values (e.g. ?tenant_id=0) for django-filters v1.1.0+

Jeremy Stretch 7 years ago
parent
commit
4668149943

+ 3 - 3
netbox/circuits/filters.py

@@ -7,7 +7,7 @@ from django.db.models import Q
 from dcim.models import Site
 from extras.filters import CustomFieldFilterSet
 from tenancy.models import Tenant
-from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
+from utilities.filters import NumericInFilter
 from .models import Provider, Circuit, CircuitTermination, CircuitType
 
 
@@ -78,11 +78,11 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
         to_field_name='slug',
         label='Circuit type (slug)',
     )
-    tenant_id = NullableModelMultipleChoiceFilter(
+    tenant_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Tenant.objects.all(),
         label='Tenant (ID)',
     )
-    tenant = NullableModelMultipleChoiceFilter(
+    tenant = django_filters.ModelMultipleChoiceFilter(
         name='tenant',
         queryset=Tenant.objects.all(),
         to_field_name='slug',

+ 22 - 22
netbox/dcim/filters.py

@@ -9,7 +9,7 @@ from django.db.models import Q
 
 from extras.filters import CustomFieldFilterSet
 from tenancy.models import Tenant
-from utilities.filters import NullableCharFieldFilter, NullableModelMultipleChoiceFilter, NumericInFilter
+from utilities.filters import NullableCharFieldFilter, NumericInFilter
 from virtualization.models import Cluster
 from .models import (
     ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
@@ -21,11 +21,11 @@ from .models import (
 
 
 class RegionFilter(django_filters.FilterSet):
-    parent_id = NullableModelMultipleChoiceFilter(
+    parent_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Region.objects.all(),
         label='Parent region (ID)',
     )
-    parent = NullableModelMultipleChoiceFilter(
+    parent = django_filters.ModelMultipleChoiceFilter(
         queryset=Region.objects.all(),
         to_field_name='slug',
         label='Parent region (slug)',
@@ -42,20 +42,20 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
         method='search',
         label='Search',
     )
-    region_id = NullableModelMultipleChoiceFilter(
+    region_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Region.objects.all(),
         label='Region (ID)',
     )
-    region = NullableModelMultipleChoiceFilter(
+    region = django_filters.ModelMultipleChoiceFilter(
         queryset=Region.objects.all(),
         to_field_name='slug',
         label='Region (slug)',
     )
-    tenant_id = NullableModelMultipleChoiceFilter(
+    tenant_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Tenant.objects.all(),
         label='Tenant (ID)',
     )
-    tenant = NullableModelMultipleChoiceFilter(
+    tenant = django_filters.ModelMultipleChoiceFilter(
         queryset=Tenant.objects.all(),
         to_field_name='slug',
         label='Tenant (slug)',
@@ -126,31 +126,31 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
         to_field_name='slug',
         label='Site (slug)',
     )
-    group_id = NullableModelMultipleChoiceFilter(
+    group_id = django_filters.ModelMultipleChoiceFilter(
         queryset=RackGroup.objects.all(),
         label='Group (ID)',
     )
-    group = NullableModelMultipleChoiceFilter(
+    group = django_filters.ModelMultipleChoiceFilter(
         name='group',
         queryset=RackGroup.objects.all(),
         to_field_name='slug',
         label='Group',
     )
-    tenant_id = NullableModelMultipleChoiceFilter(
+    tenant_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Tenant.objects.all(),
         label='Tenant (ID)',
     )
-    tenant = NullableModelMultipleChoiceFilter(
+    tenant = django_filters.ModelMultipleChoiceFilter(
         name='tenant',
         queryset=Tenant.objects.all(),
         to_field_name='slug',
         label='Tenant (slug)',
     )
-    role_id = NullableModelMultipleChoiceFilter(
+    role_id = django_filters.ModelMultipleChoiceFilter(
         queryset=RackRole.objects.all(),
         label='Role (ID)',
     )
-    role = NullableModelMultipleChoiceFilter(
+    role = django_filters.ModelMultipleChoiceFilter(
         name='role',
         queryset=RackRole.objects.all(),
         to_field_name='slug',
@@ -193,12 +193,12 @@ class RackReservationFilter(django_filters.FilterSet):
         to_field_name='slug',
         label='Site (slug)',
     )
-    group_id = NullableModelMultipleChoiceFilter(
+    group_id = django_filters.ModelMultipleChoiceFilter(
         name='rack__group',
         queryset=RackGroup.objects.all(),
         label='Group (ID)',
     )
-    group = NullableModelMultipleChoiceFilter(
+    group = django_filters.ModelMultipleChoiceFilter(
         name='rack__group',
         queryset=RackGroup.objects.all(),
         to_field_name='slug',
@@ -368,21 +368,21 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
         to_field_name='slug',
         label='Role (slug)',
     )
-    tenant_id = NullableModelMultipleChoiceFilter(
+    tenant_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Tenant.objects.all(),
         label='Tenant (ID)',
     )
-    tenant = NullableModelMultipleChoiceFilter(
+    tenant = django_filters.ModelMultipleChoiceFilter(
         name='tenant',
         queryset=Tenant.objects.all(),
         to_field_name='slug',
         label='Tenant (slug)',
     )
-    platform_id = NullableModelMultipleChoiceFilter(
+    platform_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Platform.objects.all(),
         label='Platform (ID)',
     )
-    platform = NullableModelMultipleChoiceFilter(
+    platform = django_filters.ModelMultipleChoiceFilter(
         name='platform',
         queryset=Platform.objects.all(),
         to_field_name='slug',
@@ -405,12 +405,12 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
         queryset=RackGroup.objects.all(),
         label='Rack group (ID)',
     )
-    rack_id = NullableModelMultipleChoiceFilter(
+    rack_id = django_filters.ModelMultipleChoiceFilter(
         name='rack',
         queryset=Rack.objects.all(),
         label='Rack (ID)',
     )
-    cluster_id = NullableModelMultipleChoiceFilter(
+    cluster_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Cluster.objects.all(),
         label='VM cluster (ID)',
     )
@@ -595,7 +595,7 @@ class DeviceBayFilter(DeviceComponentFilterSet):
 
 
 class InventoryItemFilter(DeviceComponentFilterSet):
-    parent_id = NullableModelMultipleChoiceFilter(
+    parent_id = django_filters.ModelMultipleChoiceFilter(
         queryset=InventoryItem.objects.all(),
         label='Parent inventory item (ID)',
     )

+ 26 - 26
netbox/ipam/filters.py

@@ -9,7 +9,7 @@ from django.db.models import Q
 from dcim.models import Site, Device, Interface
 from extras.filters import CustomFieldFilterSet
 from tenancy.models import Tenant
-from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
+from utilities.filters import NumericInFilter
 from virtualization.models import VirtualMachine
 from .models import (
     Aggregate, IPAddress, IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role,
@@ -23,11 +23,11 @@ class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
         method='search',
         label='Search',
     )
-    tenant_id = NullableModelMultipleChoiceFilter(
+    tenant_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Tenant.objects.all(),
         label='Tenant (ID)',
     )
-    tenant = NullableModelMultipleChoiceFilter(
+    tenant = django_filters.ModelMultipleChoiceFilter(
         name='tenant',
         queryset=Tenant.objects.all(),
         to_field_name='slug',
@@ -110,37 +110,37 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
         method='filter_mask_length',
         label='Mask length',
     )
-    vrf_id = NullableModelMultipleChoiceFilter(
+    vrf_id = django_filters.ModelMultipleChoiceFilter(
         queryset=VRF.objects.all(),
         label='VRF',
     )
-    vrf = NullableModelMultipleChoiceFilter(
+    vrf = django_filters.ModelMultipleChoiceFilter(
         name='vrf',
         queryset=VRF.objects.all(),
         to_field_name='rd',
         label='VRF (RD)',
     )
-    tenant_id = NullableModelMultipleChoiceFilter(
+    tenant_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Tenant.objects.all(),
         label='Tenant (ID)',
     )
-    tenant = NullableModelMultipleChoiceFilter(
+    tenant = django_filters.ModelMultipleChoiceFilter(
         name='tenant',
         queryset=Tenant.objects.all(),
         to_field_name='slug',
         label='Tenant (slug)',
     )
-    site_id = NullableModelMultipleChoiceFilter(
+    site_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Site.objects.all(),
         label='Site (ID)',
     )
-    site = NullableModelMultipleChoiceFilter(
+    site = django_filters.ModelMultipleChoiceFilter(
         name='site',
         queryset=Site.objects.all(),
         to_field_name='slug',
         label='Site (slug)',
     )
-    vlan_id = NullableModelMultipleChoiceFilter(
+    vlan_id = django_filters.ModelMultipleChoiceFilter(
         queryset=VLAN.objects.all(),
         label='VLAN (ID)',
     )
@@ -148,11 +148,11 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
         name='vlan__vid',
         label='VLAN number (1-4095)',
     )
-    role_id = NullableModelMultipleChoiceFilter(
+    role_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Role.objects.all(),
         label='Role (ID)',
     )
-    role = NullableModelMultipleChoiceFilter(
+    role = django_filters.ModelMultipleChoiceFilter(
         name='role',
         queryset=Role.objects.all(),
         to_field_name='slug',
@@ -207,21 +207,21 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
         method='filter_mask_length',
         label='Mask length',
     )
-    vrf_id = NullableModelMultipleChoiceFilter(
+    vrf_id = django_filters.ModelMultipleChoiceFilter(
         queryset=VRF.objects.all(),
         label='VRF',
     )
-    vrf = NullableModelMultipleChoiceFilter(
+    vrf = django_filters.ModelMultipleChoiceFilter(
         name='vrf',
         queryset=VRF.objects.all(),
         to_field_name='rd',
         label='VRF (RD)',
     )
-    tenant_id = NullableModelMultipleChoiceFilter(
+    tenant_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Tenant.objects.all(),
         label='Tenant (ID)',
     )
-    tenant = NullableModelMultipleChoiceFilter(
+    tenant = django_filters.ModelMultipleChoiceFilter(
         name='tenant',
         queryset=Tenant.objects.all(),
         to_field_name='slug',
@@ -290,11 +290,11 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
 
 
 class VLANGroupFilter(django_filters.FilterSet):
-    site_id = NullableModelMultipleChoiceFilter(
+    site_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Site.objects.all(),
         label='Site (ID)',
     )
-    site = NullableModelMultipleChoiceFilter(
+    site = django_filters.ModelMultipleChoiceFilter(
         name='site',
         queryset=Site.objects.all(),
         to_field_name='slug',
@@ -312,41 +312,41 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
         method='search',
         label='Search',
     )
-    site_id = NullableModelMultipleChoiceFilter(
+    site_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Site.objects.all(),
         label='Site (ID)',
     )
-    site = NullableModelMultipleChoiceFilter(
+    site = django_filters.ModelMultipleChoiceFilter(
         name='site',
         queryset=Site.objects.all(),
         to_field_name='slug',
         label='Site (slug)',
     )
-    group_id = NullableModelMultipleChoiceFilter(
+    group_id = django_filters.ModelMultipleChoiceFilter(
         queryset=VLANGroup.objects.all(),
         label='Group (ID)',
     )
-    group = NullableModelMultipleChoiceFilter(
+    group = django_filters.ModelMultipleChoiceFilter(
         name='group',
         queryset=VLANGroup.objects.all(),
         to_field_name='slug',
         label='Group',
     )
-    tenant_id = NullableModelMultipleChoiceFilter(
+    tenant_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Tenant.objects.all(),
         label='Tenant (ID)',
     )
-    tenant = NullableModelMultipleChoiceFilter(
+    tenant = django_filters.ModelMultipleChoiceFilter(
         name='tenant',
         queryset=Tenant.objects.all(),
         to_field_name='slug',
         label='Tenant (slug)',
     )
-    role_id = NullableModelMultipleChoiceFilter(
+    role_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Role.objects.all(),
         label='Role (ID)',
     )
-    role = NullableModelMultipleChoiceFilter(
+    role = django_filters.ModelMultipleChoiceFilter(
         name='role',
         queryset=Role.objects.all(),
         to_field_name='slug',

+ 4 - 0
netbox/netbox/settings.py

@@ -206,6 +206,10 @@ LOGIN_URL = '/{}login/'.format(BASE_PATH)
 # Secrets
 SECRETS_MIN_PUBKEY_SIZE = 2048
 
+# Django filters
+FILTERS_NULL_CHOICE_LABEL = 'None'
+FILTERS_NULL_CHOICE_VALUE = '0'  # Must be a string
+
 # Django REST framework (API)
 REST_FRAMEWORK_VERSION = VERSION[0:3]  # Use major.minor as API version
 REST_FRAMEWORK = {

+ 3 - 3
netbox/tenancy/filters.py

@@ -5,7 +5,7 @@ import django_filters
 from django.db.models import Q
 
 from extras.filters import CustomFieldFilterSet
-from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
+from utilities.filters import NumericInFilter
 from .models import Tenant, TenantGroup
 
 
@@ -22,11 +22,11 @@ class TenantFilter(CustomFieldFilterSet, django_filters.FilterSet):
         method='search',
         label='Search',
     )
-    group_id = NullableModelMultipleChoiceFilter(
+    group_id = django_filters.ModelMultipleChoiceFilter(
         queryset=TenantGroup.objects.all(),
         label='Group (ID)',
     )
-    group = NullableModelMultipleChoiceFilter(
+    group = django_filters.ModelMultipleChoiceFilter(
         name='group',
         queryset=TenantGroup.objects.all(),
         to_field_name='slug',

+ 0 - 49
netbox/utilities/filters.py

@@ -4,7 +4,6 @@ import django_filters
 import itertools
 
 from django import forms
-from django.db.models import Q
 from django.utils.encoding import force_text
 
 
@@ -66,51 +65,3 @@ class NullableModelMultipleChoiceField(forms.ModelMultipleChoiceField):
             stripped_value = value
         super(NullableModelMultipleChoiceField, self).clean(stripped_value)
         return value
-
-
-class NullableModelMultipleChoiceFilter(django_filters.ModelMultipleChoiceFilter):
-    """
-    This class extends ModelMultipleChoiceFilter to accept an additional value which implies "is null". The default
-    queryset filter argument is:
-
-        .filter(fieldname=value)
-
-    When filtering by the value representing "is null" ('0' by default) the argument is modified to:
-
-        .filter(fieldname__isnull=True)
-    """
-    field_class = NullableModelMultipleChoiceField
-
-    def __init__(self, *args, **kwargs):
-        self.null_value = kwargs.get('null_value', 0)
-        super(NullableModelMultipleChoiceFilter, self).__init__(*args, **kwargs)
-
-    def filter(self, qs, value):
-        value = value or ()  # Make sure we have an iterable
-
-        if self.is_noop(qs, value):
-            return qs
-
-        # Even though not a noop, no point filtering if empty
-        if not value:
-            return qs
-
-        q = Q()
-        for v in set(value):
-            # Filtering by "is null"
-            if v == force_text(self.null_value):
-                arg = {'{}__isnull'.format(self.name): True}
-            # Filtering by a related field (e.g. slug)
-            elif self.field.to_field_name is not None:
-                arg = {'{}__{}'.format(self.name, self.field.to_field_name): v}
-            # Filtering by primary key (default)
-            else:
-                arg = {self.name: v}
-            if self.conjoined:
-                qs = self.get_method(qs)(**arg)
-            else:
-                q |= Q(**arg)
-        if self.distinct:
-            return self.get_method(qs)(q).distinct()
-
-        return self.get_method(qs)(q)

+ 11 - 11
netbox/virtualization/filters.py

@@ -9,7 +9,7 @@ from django.db.models import Q
 from dcim.models import DeviceRole, Interface, Platform, Site
 from extras.filters import CustomFieldFilterSet
 from tenancy.models import Tenant
-from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
+from utilities.filters import NumericInFilter
 from .constants import STATUS_CHOICES
 from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
 
@@ -20,11 +20,11 @@ class ClusterFilter(CustomFieldFilterSet):
         method='search',
         label='Search',
     )
-    group_id = NullableModelMultipleChoiceFilter(
+    group_id = django_filters.ModelMultipleChoiceFilter(
         queryset=ClusterGroup.objects.all(),
         label='Parent group (ID)',
     )
-    group = NullableModelMultipleChoiceFilter(
+    group = django_filters.ModelMultipleChoiceFilter(
         queryset=ClusterGroup.objects.all(),
         to_field_name='slug',
         label='Parent group (slug)',
@@ -72,12 +72,12 @@ class VirtualMachineFilter(CustomFieldFilterSet):
     status = django_filters.MultipleChoiceFilter(
         choices=STATUS_CHOICES
     )
-    cluster_group_id = NullableModelMultipleChoiceFilter(
+    cluster_group_id = django_filters.ModelMultipleChoiceFilter(
         name='cluster__group',
         queryset=ClusterGroup.objects.all(),
         label='Cluster group (ID)',
     )
-    cluster_group = NullableModelMultipleChoiceFilter(
+    cluster_group = django_filters.ModelMultipleChoiceFilter(
         name='cluster__group',
         queryset=ClusterGroup.objects.all(),
         to_field_name='slug',
@@ -87,29 +87,29 @@ class VirtualMachineFilter(CustomFieldFilterSet):
         queryset=Cluster.objects.all(),
         label='Cluster (ID)',
     )
-    role_id = NullableModelMultipleChoiceFilter(
+    role_id = django_filters.ModelMultipleChoiceFilter(
         queryset=DeviceRole.objects.all(),
         label='Role (ID)',
     )
-    role = NullableModelMultipleChoiceFilter(
+    role = django_filters.ModelMultipleChoiceFilter(
         queryset=DeviceRole.objects.all(),
         to_field_name='slug',
         label='Role (slug)',
     )
-    tenant_id = NullableModelMultipleChoiceFilter(
+    tenant_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Tenant.objects.all(),
         label='Tenant (ID)',
     )
-    tenant = NullableModelMultipleChoiceFilter(
+    tenant = django_filters.ModelMultipleChoiceFilter(
         queryset=Tenant.objects.all(),
         to_field_name='slug',
         label='Tenant (slug)',
     )
-    platform_id = NullableModelMultipleChoiceFilter(
+    platform_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Platform.objects.all(),
         label='Platform (ID)',
     )
-    platform = NullableModelMultipleChoiceFilter(
+    platform = django_filters.ModelMultipleChoiceFilter(
         queryset=Platform.objects.all(),
         to_field_name='slug',
         label='Platform (slug)',

+ 1 - 1
requirements.txt

@@ -1,7 +1,7 @@
 Django>=1.11,<2.0
 django-cors-headers>=2.1
 django-debug-toolbar>=1.8
-django-filter>=1.0.4
+django-filter>=1.1.0
 django-mptt==0.8.7
 django-rest-swagger>=2.1.0
 django-tables2>=1.10.0