Browse Source

Added 'none' options to filters for optional fields

Jeremy Stretch 8 years ago
parent
commit
9dea5656ad

+ 4 - 2
netbox/circuits/filters.py

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

+ 2 - 1
netbox/circuits/forms.py

@@ -187,5 +187,6 @@ class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
     model = Circuit
     model = Circuit
     type = FilterChoiceField(choices=get_filter_choices(CircuitType, id_field='slug', count_field='circuits'))
     type = FilterChoiceField(choices=get_filter_choices(CircuitType, id_field='slug', count_field='circuits'))
     provider = FilterChoiceField(choices=get_filter_choices(Provider, id_field='slug', count_field='circuits'))
     provider = FilterChoiceField(choices=get_filter_choices(Provider, id_field='slug', count_field='circuits'))
-    tenant = FilterChoiceField(choices=get_filter_choices(Tenant, id_field='slug', count_field='circuits'))
+    tenant = FilterChoiceField(choices=get_filter_choices(Tenant, id_field='slug', count_field='circuits',
+                                                          null_option='None'))
     site = FilterChoiceField(choices=get_filter_choices(Site, id_field='slug', count_field='circuits'))
     site = FilterChoiceField(choices=get_filter_choices(Site, id_field='slug', count_field='circuits'))

+ 13 - 12
netbox/dcim/filters.py

@@ -4,6 +4,7 @@ from django.db.models import Q
 
 
 from extras.filters import CustomFieldFilterSet
 from extras.filters import CustomFieldFilterSet
 from tenancy.models import Tenant
 from tenancy.models import Tenant
+from utilities.filters import NullableModelMultipleChoiceFilter
 from .models import (
 from .models import (
     ConsolePort, ConsoleServerPort, Device, DeviceRole, DeviceType, Interface, InterfaceConnection, Manufacturer,
     ConsolePort, ConsoleServerPort, Device, DeviceRole, DeviceType, Interface, InterfaceConnection, Manufacturer,
     Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackRole, Site,
     Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackRole, Site,
@@ -15,12 +16,12 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
         action='search',
         action='search',
         label='Search',
         label='Search',
     )
     )
-    tenant_id = django_filters.ModelMultipleChoiceFilter(
+    tenant_id = NullableModelMultipleChoiceFilter(
         name='tenant',
         name='tenant',
         queryset=Tenant.objects.all(),
         queryset=Tenant.objects.all(),
         label='Tenant (ID)',
         label='Tenant (ID)',
     )
     )
-    tenant = django_filters.ModelMultipleChoiceFilter(
+    tenant = NullableModelMultipleChoiceFilter(
         name='tenant',
         name='tenant',
         queryset=Tenant.objects.all(),
         queryset=Tenant.objects.all(),
         to_field_name='slug',
         to_field_name='slug',
@@ -75,34 +76,34 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
         to_field_name='slug',
         to_field_name='slug',
         label='Site (slug)',
         label='Site (slug)',
     )
     )
-    group_id = django_filters.ModelMultipleChoiceFilter(
+    group_id = NullableModelMultipleChoiceFilter(
         name='group',
         name='group',
         queryset=RackGroup.objects.all(),
         queryset=RackGroup.objects.all(),
         label='Group (ID)',
         label='Group (ID)',
     )
     )
-    group = django_filters.ModelMultipleChoiceFilter(
+    group = NullableModelMultipleChoiceFilter(
         name='group',
         name='group',
         queryset=RackGroup.objects.all(),
         queryset=RackGroup.objects.all(),
         to_field_name='slug',
         to_field_name='slug',
         label='Group',
         label='Group',
     )
     )
-    tenant_id = django_filters.ModelMultipleChoiceFilter(
+    tenant_id = NullableModelMultipleChoiceFilter(
         name='tenant',
         name='tenant',
         queryset=Tenant.objects.all(),
         queryset=Tenant.objects.all(),
         label='Tenant (ID)',
         label='Tenant (ID)',
     )
     )
-    tenant = django_filters.ModelMultipleChoiceFilter(
+    tenant = NullableModelMultipleChoiceFilter(
         name='tenant',
         name='tenant',
         queryset=Tenant.objects.all(),
         queryset=Tenant.objects.all(),
         to_field_name='slug',
         to_field_name='slug',
         label='Tenant (slug)',
         label='Tenant (slug)',
     )
     )
-    role_id = django_filters.ModelMultipleChoiceFilter(
+    role_id = NullableModelMultipleChoiceFilter(
         name='role',
         name='role',
         queryset=RackRole.objects.all(),
         queryset=RackRole.objects.all(),
         label='Role (ID)',
         label='Role (ID)',
     )
     )
-    role = django_filters.ModelMultipleChoiceFilter(
+    role = NullableModelMultipleChoiceFilter(
         name='role',
         name='role',
         queryset=RackRole.objects.all(),
         queryset=RackRole.objects.all(),
         to_field_name='slug',
         to_field_name='slug',
@@ -177,12 +178,12 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
         to_field_name='slug',
         to_field_name='slug',
         label='Role (slug)',
         label='Role (slug)',
     )
     )
-    tenant_id = django_filters.ModelMultipleChoiceFilter(
+    tenant_id = NullableModelMultipleChoiceFilter(
         name='tenant',
         name='tenant',
         queryset=Tenant.objects.all(),
         queryset=Tenant.objects.all(),
         label='Tenant (ID)',
         label='Tenant (ID)',
     )
     )
-    tenant = django_filters.ModelMultipleChoiceFilter(
+    tenant = NullableModelMultipleChoiceFilter(
         name='tenant',
         name='tenant',
         queryset=Tenant.objects.all(),
         queryset=Tenant.objects.all(),
         to_field_name='slug',
         to_field_name='slug',
@@ -210,12 +211,12 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
         to_field_name='slug',
         to_field_name='slug',
         label='Device model (slug)',
         label='Device model (slug)',
     )
     )
-    platform_id = django_filters.ModelMultipleChoiceFilter(
+    platform_id = NullableModelMultipleChoiceFilter(
         name='platform',
         name='platform',
         queryset=Platform.objects.all(),
         queryset=Platform.objects.all(),
         label='Platform (ID)',
         label='Platform (ID)',
     )
     )
-    platform = django_filters.ModelMultipleChoiceFilter(
+    platform = NullableModelMultipleChoiceFilter(
         name='platform',
         name='platform',
         queryset=Platform.objects.all(),
         queryset=Platform.objects.all(),
         to_field_name='slug',
         to_field_name='slug',

+ 12 - 6
netbox/dcim/forms.py

@@ -120,7 +120,8 @@ class SiteBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
 
 
 class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm):
 class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm):
     model = Site
     model = Site
-    tenant = FilterChoiceField(choices=get_filter_choices(Tenant, id_field='slug', count_field='sites'))
+    tenant = FilterChoiceField(choices=get_filter_choices(Tenant, id_field='slug', count_field='sites',
+                                                          null_option='None'))
 
 
 
 
 #
 #
@@ -246,10 +247,13 @@ class RackBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
 class RackFilterForm(BootstrapMixin, CustomFieldFilterForm):
 class RackFilterForm(BootstrapMixin, CustomFieldFilterForm):
     model = Rack
     model = Rack
     site = FilterChoiceField(choices=get_filter_choices(Site, id_field='slug', count_field='racks'))
     site = FilterChoiceField(choices=get_filter_choices(Site, id_field='slug', count_field='racks'))
-    group_id = FilterChoiceField(choices=get_filter_choices(RackGroup, select_related=['site'], count_field='racks'),
+    group_id = FilterChoiceField(choices=get_filter_choices(RackGroup, select_related=['site'], count_field='racks',
+                                                            null_option='None'),
                                  label='Rack Group')
                                  label='Rack Group')
-    tenant = FilterChoiceField(choices=get_filter_choices(Tenant, id_field='slug', count_field='racks'))
-    role = FilterChoiceField(choices=get_filter_choices(RackRole, id_field='slug', count_field='racks'))
+    tenant = FilterChoiceField(choices=get_filter_choices(Tenant, id_field='slug', count_field='racks',
+                                                          null_option='None'))
+    role = FilterChoiceField(choices=get_filter_choices(RackRole, id_field='slug', count_field='racks',
+                                                        null_option='None'))
 
 
 
 
 #
 #
@@ -595,11 +599,13 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
                                                                  count_field='racks__devices'),
                                                                  count_field='racks__devices'),
                                       label='Rack Group')
                                       label='Rack Group')
     role = FilterChoiceField(choices=get_filter_choices(DeviceRole, id_field='slug', count_field='devices'))
     role = FilterChoiceField(choices=get_filter_choices(DeviceRole, id_field='slug', count_field='devices'))
-    tenant = FilterChoiceField(choices=get_filter_choices(Tenant, id_field='slug', count_field='devices'))
+    tenant = FilterChoiceField(choices=get_filter_choices(Tenant, id_field='slug', count_field='devices',
+                                                          null_option='None'))
     device_type_id = FilterChoiceField(choices=get_filter_choices(DeviceType, select_related=['manufacturer'],
     device_type_id = FilterChoiceField(choices=get_filter_choices(DeviceType, select_related=['manufacturer'],
                                                                   count_field='instances'),
                                                                   count_field='instances'),
                                        label='Type')
                                        label='Type')
-    platform = FilterChoiceField(choices=get_filter_choices(Platform, id_field='slug', count_field='devices'))
+    platform = FilterChoiceField(choices=get_filter_choices(Platform, id_field='slug', count_field='devices',
+                                                            null_option='None'))
     status = forms.NullBooleanField(required=False, widget=forms.Select(choices=FORM_STATUS_CHOICES))
     status = forms.NullBooleanField(required=False, widget=forms.Select(choices=FORM_STATUS_CHOICES))
 
 
 
 

+ 43 - 43
netbox/ipam/filters.py

@@ -7,6 +7,7 @@ from django.db.models import Q
 from dcim.models import Site, Device, Interface
 from dcim.models import Site, Device, Interface
 from extras.filters import CustomFieldFilterSet
 from extras.filters import CustomFieldFilterSet
 from tenancy.models import Tenant
 from tenancy.models import Tenant
+from utilities.filters import NullableModelMultipleChoiceFilter
 
 
 from .models import RIR, Aggregate, VRF, Prefix, IPAddress, VLAN, VLANGroup, Role
 from .models import RIR, Aggregate, VRF, Prefix, IPAddress, VLAN, VLANGroup, Role
 
 
@@ -21,12 +22,12 @@ class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
         lookup_type='icontains',
         lookup_type='icontains',
         label='Name',
         label='Name',
     )
     )
-    tenant_id = django_filters.ModelMultipleChoiceFilter(
+    tenant_id = NullableModelMultipleChoiceFilter(
         name='tenant',
         name='tenant',
         queryset=Tenant.objects.all(),
         queryset=Tenant.objects.all(),
         label='Tenant (ID)',
         label='Tenant (ID)',
     )
     )
-    tenant = django_filters.ModelMultipleChoiceFilter(
+    tenant = NullableModelMultipleChoiceFilter(
         name='tenant',
         name='tenant',
         queryset=Tenant.objects.all(),
         queryset=Tenant.objects.all(),
         to_field_name='slug',
         to_field_name='slug',
@@ -85,29 +86,34 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
         action='search_by_parent',
         action='search_by_parent',
         label='Parent prefix',
         label='Parent prefix',
     )
     )
-    vrf = django_filters.MethodFilter(
-        action='_vrf',
+    vrf = NullableModelMultipleChoiceFilter(
+        name='vrf',
+        queryset=VRF.objects.all(),
         label='VRF',
         label='VRF',
     )
     )
     # Duplicate of `vrf` for backward-compatibility
     # Duplicate of `vrf` for backward-compatibility
-    vrf_id = django_filters.MethodFilter(
-        action='_vrf',
+    vrf_id = NullableModelMultipleChoiceFilter(
+        name='vrf_id',
+        queryset=VRF.objects.all(),
         label='VRF',
         label='VRF',
     )
     )
-    tenant_id = django_filters.MethodFilter(
-        action='_tenant_id',
+    tenant_id = NullableModelMultipleChoiceFilter(
+        name='tenant',
+        queryset=Tenant.objects.all(),
         label='Tenant (ID)',
         label='Tenant (ID)',
     )
     )
-    tenant = django_filters.MethodFilter(
-        action='_tenant',
-        label='Tenant',
+    tenant = NullableModelMultipleChoiceFilter(
+        name='tenant',
+        queryset=Tenant.objects.all(),
+        to_field_name='slug',
+        label='Tenant (slug)',
     )
     )
-    site_id = django_filters.ModelMultipleChoiceFilter(
+    site_id = NullableModelMultipleChoiceFilter(
         name='site',
         name='site',
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
         label='Site (ID)',
         label='Site (ID)',
     )
     )
-    site = django_filters.ModelMultipleChoiceFilter(
+    site = NullableModelMultipleChoiceFilter(
         name='site',
         name='site',
         queryset=Site.objects.all(),
         queryset=Site.objects.all(),
         to_field_name='slug',
         to_field_name='slug',
@@ -122,12 +128,12 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
         name='vlan__vid',
         name='vlan__vid',
         label='VLAN number (1-4095)',
         label='VLAN number (1-4095)',
     )
     )
-    role_id = django_filters.ModelMultipleChoiceFilter(
+    role_id = NullableModelMultipleChoiceFilter(
         name='role',
         name='role',
         queryset=Role.objects.all(),
         queryset=Role.objects.all(),
         label='Role (ID)',
         label='Role (ID)',
     )
     )
-    role = django_filters.ModelMultipleChoiceFilter(
+    role = NullableModelMultipleChoiceFilter(
         name='role',
         name='role',
         queryset=Role.objects.all(),
         queryset=Role.objects.all(),
         to_field_name='slug',
         to_field_name='slug',
@@ -136,7 +142,7 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
 
 
     class Meta:
     class Meta:
         model = Prefix
         model = Prefix
-        fields = ['family', 'site_id', 'site', 'vrf', 'vrf_id', 'vlan_id', 'vlan_vid', 'status', 'role_id', 'role']
+        fields = ['family', 'site_id', 'site', 'vlan_id', 'vlan_vid', 'status', 'role_id', 'role']
 
 
     def search(self, queryset, value):
     def search(self, queryset, value):
         qs_filter = Q(description__icontains=value)
         qs_filter = Q(description__icontains=value)
@@ -157,17 +163,6 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
         except AddrFormatError:
         except AddrFormatError:
             return queryset.none()
             return queryset.none()
 
 
-    def _vrf(self, queryset, value):
-        if str(value) == '':
-            return queryset
-        try:
-            vrf_id = int(value)
-        except ValueError:
-            return queryset.none()
-        if vrf_id == 0:
-            return queryset.filter(vrf__isnull=True)
-        return queryset.filter(vrf__pk=value)
-
     def _tenant(self, queryset, value):
     def _tenant(self, queryset, value):
         if str(value) == '':
         if str(value) == '':
             return queryset
             return queryset
@@ -196,22 +191,27 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
         action='search_by_parent',
         action='search_by_parent',
         label='Parent prefix',
         label='Parent prefix',
     )
     )
-    vrf = django_filters.MethodFilter(
-        action='_vrf',
+    vrf = NullableModelMultipleChoiceFilter(
+        name='vrf',
+        queryset=VRF.objects.all(),
         label='VRF',
         label='VRF',
     )
     )
     # Duplicate of `vrf` for backward-compatibility
     # Duplicate of `vrf` for backward-compatibility
-    vrf_id = django_filters.MethodFilter(
-        action='_vrf',
+    vrf_id = NullableModelMultipleChoiceFilter(
+        name='vrf_id',
+        queryset=VRF.objects.all(),
         label='VRF',
         label='VRF',
     )
     )
-    tenant_id = django_filters.MethodFilter(
-        action='_tenant_id',
+    tenant_id = NullableModelMultipleChoiceFilter(
+        name='tenant',
+        queryset=Tenant.objects.all(),
         label='Tenant (ID)',
         label='Tenant (ID)',
     )
     )
-    tenant = django_filters.MethodFilter(
-        action='_tenant',
-        label='Tenant',
+    tenant = NullableModelMultipleChoiceFilter(
+        name='tenant',
+        queryset=Tenant.objects.all(),
+        to_field_name='slug',
+        label='Tenant (slug)',
     )
     )
     device_id = django_filters.ModelMultipleChoiceFilter(
     device_id = django_filters.ModelMultipleChoiceFilter(
         name='interface__device',
         name='interface__device',
@@ -232,7 +232,7 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
 
 
     class Meta:
     class Meta:
         model = IPAddress
         model = IPAddress
-        fields = ['q', 'family', 'vrf_id', 'vrf', 'device_id', 'device', 'interface_id']
+        fields = ['q', 'family', 'device_id', 'device', 'interface_id']
 
 
     def search(self, queryset, value):
     def search(self, queryset, value):
         qs_filter = Q(description__icontains=value)
         qs_filter = Q(description__icontains=value)
@@ -317,12 +317,12 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
         to_field_name='slug',
         to_field_name='slug',
         label='Site (slug)',
         label='Site (slug)',
     )
     )
-    group_id = django_filters.ModelMultipleChoiceFilter(
+    group_id = NullableModelMultipleChoiceFilter(
         name='group',
         name='group',
         queryset=VLANGroup.objects.all(),
         queryset=VLANGroup.objects.all(),
         label='Group (ID)',
         label='Group (ID)',
     )
     )
-    group = django_filters.ModelMultipleChoiceFilter(
+    group = NullableModelMultipleChoiceFilter(
         name='group',
         name='group',
         queryset=VLANGroup.objects.all(),
         queryset=VLANGroup.objects.all(),
         to_field_name='slug',
         to_field_name='slug',
@@ -337,23 +337,23 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
         name='vid',
         name='vid',
         label='VLAN number (1-4095)',
         label='VLAN number (1-4095)',
     )
     )
-    tenant_id = django_filters.ModelMultipleChoiceFilter(
+    tenant_id = NullableModelMultipleChoiceFilter(
         name='tenant',
         name='tenant',
         queryset=Tenant.objects.all(),
         queryset=Tenant.objects.all(),
         label='Tenant (ID)',
         label='Tenant (ID)',
     )
     )
-    tenant = django_filters.ModelMultipleChoiceFilter(
+    tenant = NullableModelMultipleChoiceFilter(
         name='tenant',
         name='tenant',
         queryset=Tenant.objects.all(),
         queryset=Tenant.objects.all(),
         to_field_name='slug',
         to_field_name='slug',
         label='Tenant (slug)',
         label='Tenant (slug)',
     )
     )
-    role_id = django_filters.ModelMultipleChoiceFilter(
+    role_id = NullableModelMultipleChoiceFilter(
         name='role',
         name='role',
         queryset=Role.objects.all(),
         queryset=Role.objects.all(),
         label='Role (ID)',
         label='Role (ID)',
     )
     )
-    role = django_filters.ModelMultipleChoiceFilter(
+    role = NullableModelMultipleChoiceFilter(
         name='role',
         name='role',
         queryset=Role.objects.all(),
         queryset=Role.objects.all(),
         to_field_name='slug',
         to_field_name='slug',

+ 18 - 10
netbox/ipam/forms.py

@@ -74,7 +74,8 @@ class VRFBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
 
 
 class VRFFilterForm(BootstrapMixin, CustomFieldFilterForm):
 class VRFFilterForm(BootstrapMixin, CustomFieldFilterForm):
     model = VRF
     model = VRF
-    tenant = FilterChoiceField(choices=get_filter_choices(Tenant, id_field='slug', count_field='vrfs'))
+    tenant = FilterChoiceField(choices=get_filter_choices(Tenant, id_field='slug', count_field='vrfs',
+                                                          null_option='None'))
 
 
 
 
 #
 #
@@ -272,13 +273,16 @@ class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm):
         'placeholder': 'Network',
         'placeholder': 'Network',
     }))
     }))
     family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address Family')
     family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address Family')
-    vrf = FilterChoiceField(choices=get_filter_choices(VRF, count_field='prefixes', null_option=(0, 'Global')),
+    vrf = FilterChoiceField(choices=get_filter_choices(VRF, count_field='prefixes', null_option='Global'),
                             label='VRF')
                             label='VRF')
-    tenant = FilterChoiceField(choices=get_filter_choices(Tenant, id_field='slug', count_field='prefixes'),
+    tenant = FilterChoiceField(choices=get_filter_choices(Tenant, id_field='slug', count_field='prefixes',
+                                                          null_option='None'),
                                label='Tenant')
                                label='Tenant')
     status = FilterChoiceField(choices=prefix_status_choices)
     status = FilterChoiceField(choices=prefix_status_choices)
-    site = FilterChoiceField(choices=get_filter_choices(Site, id_field='slug', count_field='prefixes'))
-    role = FilterChoiceField(choices=get_filter_choices(Role, id_field='slug', count_field='prefixes'))
+    site = FilterChoiceField(choices=get_filter_choices(Site, id_field='slug', count_field='prefixes',
+                                                        null_option='None'))
+    role = FilterChoiceField(choices=get_filter_choices(Role, id_field='slug', count_field='prefixes',
+                                                        null_option='None'))
     expand = forms.BooleanField(required=False, label='Expand prefix hierarchy')
     expand = forms.BooleanField(required=False, label='Expand prefix hierarchy')
 
 
 
 
@@ -415,8 +419,10 @@ class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm):
         'placeholder': 'Prefix',
         'placeholder': 'Prefix',
     }))
     }))
     family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address Family')
     family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address Family')
-    vrf = FilterChoiceField(choices=get_filter_choices(VRF, count_field='ip_addresses'), label='VRF')
-    tenant = FilterChoiceField(choices=get_filter_choices(Tenant, id_field='slug', count_field='prefixes'),
+    vrf = FilterChoiceField(choices=get_filter_choices(VRF, count_field='ip_addresses', null_option='None'),
+                            label='VRF')
+    tenant = FilterChoiceField(choices=get_filter_choices(Tenant, id_field='slug', count_field='ip_addresses',
+                                                          null_option='None'),
                                label='Tenant')
                                label='Tenant')
 
 
 
 
@@ -521,8 +527,10 @@ def vlan_status_choices():
 class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm):
 class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm):
     model = VLAN
     model = VLAN
     site = FilterChoiceField(choices=get_filter_choices(Site, id_field='slug', count_field='vlans'))
     site = FilterChoiceField(choices=get_filter_choices(Site, id_field='slug', count_field='vlans'))
-    group_id = FilterChoiceField(choices=get_filter_choices(VLANGroup, select_related=['site'], count_field='vlans'),
+    group_id = FilterChoiceField(choices=get_filter_choices(VLANGroup, select_related=['site'], count_field='vlans',
+                                                            null_option='None'),
                                  label='VLAN Group')
                                  label='VLAN Group')
-    tenant = FilterChoiceField(choices=get_filter_choices(Tenant, id_field='slug', count_field='vlans'))
+    tenant = FilterChoiceField(choices=get_filter_choices(Tenant, id_field='slug', count_field='vlans',
+                                                          null_option='None'))
     status = FilterChoiceField(choices=vlan_status_choices)
     status = FilterChoiceField(choices=vlan_status_choices)
-    role = FilterChoiceField(choices=get_filter_choices(Role, id_field='slug', count_field='vlans'))
+    role = FilterChoiceField(choices=get_filter_choices(Role, id_field='slug', count_field='vlans', null_option='None'))

+ 3 - 2
netbox/tenancy/filters.py

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

+ 2 - 1
netbox/tenancy/forms.py

@@ -77,4 +77,5 @@ class TenantBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
 
 
 class TenantFilterForm(BootstrapMixin, CustomFieldFilterForm):
 class TenantFilterForm(BootstrapMixin, CustomFieldFilterForm):
     model = Tenant
     model = Tenant
-    group = FilterChoiceField(choices=get_filter_choices(TenantGroup, id_field='slug', count_field='tenants'))
+    group = FilterChoiceField(choices=get_filter_choices(TenantGroup, id_field='slug', count_field='tenants',
+                                                         null_option='None'))

+ 43 - 0
netbox/utilities/filters.py

@@ -0,0 +1,43 @@
+import django_filters
+
+from django.db.models import Q
+
+
+class NullableModelMultipleChoiceFilter(django_filters.MultipleChoiceFilter):
+
+    def __init__(self, *args, **kwargs):
+        # Convert the queryset to a list of choices prefixed with a "None" option
+        queryset = kwargs.pop('queryset')
+        self.to_field_name = kwargs.pop('to_field_name', 'pk')
+        kwargs['choices'] = [(0, 'None')] + [(getattr(o, self.to_field_name), o) for o in queryset]
+        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 on NULL
+            if v == str(0):
+                arg = {'{}__isnull'.format(self.name): True}
+            # Filtering on a related field (e.g. slug)
+            elif self.to_field_name != 'pk':
+                arg = {'{}__{}'.format(self.name, self.to_field_name): v}
+            # Filtering on primary key
+            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)

+ 2 - 2
netbox/utilities/forms.py

@@ -43,7 +43,7 @@ def get_filter_choices(model, id_field='pk', select_related=[], count_field=None
     :param id_field: Field to use as the object identifier
     :param id_field: Field to use as the object identifier
     :param select_related: Any related tables to include
     :param select_related: Any related tables to include
     :param count_field: The field to use for a child COUNT() (optional)
     :param count_field: The field to use for a child COUNT() (optional)
-    :param null_option: A (value, label) tuple to include at the beginning of the list serving as "null"
+    :param null_option: A choice to include at the beginning of the list serving as "null"
     """
     """
     queryset = model.objects.all()
     queryset = model.objects.all()
     if select_related:
     if select_related:
@@ -54,7 +54,7 @@ def get_filter_choices(model, id_field='pk', select_related=[], count_field=None
     else:
     else:
         choices = [(getattr(obj, id_field), u'{}'.format(obj)) for obj in queryset]
         choices = [(getattr(obj, id_field), u'{}'.format(obj)) for obj in queryset]
     if null_option:
     if null_option:
-        choices = [null_option] + choices
+        choices = [(0, null_option)] + choices
     return choices
     return choices