diff --git a/netbox/circuits/filters.py b/netbox/circuits/filters.py index f719290ed..152588c5a 100644 --- a/netbox/circuits/filters.py +++ b/netbox/circuits/filters.py @@ -5,6 +5,8 @@ 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 + from .models import Provider, Circuit, CircuitType @@ -64,12 +66,12 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet): to_field_name='slug', label='Circuit type (slug)', ) - tenant_id = django_filters.ModelMultipleChoiceFilter( + tenant_id = NullableModelMultipleChoiceFilter( name='tenant', queryset=Tenant.objects.all(), label='Tenant (ID)', ) - tenant = django_filters.ModelMultipleChoiceFilter( + tenant = NullableModelMultipleChoiceFilter( name='tenant', queryset=Tenant.objects.all(), to_field_name='slug', diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index c934ab630..233741be8 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -187,5 +187,6 @@ class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm): model = Circuit 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')) - 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')) diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index eac47445e..65831a974 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -4,6 +4,7 @@ from django.db.models import Q from extras.filters import CustomFieldFilterSet from tenancy.models import Tenant +from utilities.filters import NullableModelMultipleChoiceFilter from .models import ( ConsolePort, ConsoleServerPort, Device, DeviceRole, DeviceType, Interface, InterfaceConnection, Manufacturer, Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackRole, Site, @@ -15,12 +16,12 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet): action='search', label='Search', ) - tenant_id = django_filters.ModelMultipleChoiceFilter( + tenant_id = NullableModelMultipleChoiceFilter( name='tenant', queryset=Tenant.objects.all(), label='Tenant (ID)', ) - tenant = django_filters.ModelMultipleChoiceFilter( + tenant = NullableModelMultipleChoiceFilter( name='tenant', queryset=Tenant.objects.all(), to_field_name='slug', @@ -75,34 +76,34 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet): to_field_name='slug', label='Site (slug)', ) - group_id = django_filters.ModelMultipleChoiceFilter( + group_id = NullableModelMultipleChoiceFilter( name='group', queryset=RackGroup.objects.all(), label='Group (ID)', ) - group = django_filters.ModelMultipleChoiceFilter( + group = NullableModelMultipleChoiceFilter( name='group', queryset=RackGroup.objects.all(), to_field_name='slug', label='Group', ) - tenant_id = django_filters.ModelMultipleChoiceFilter( + tenant_id = NullableModelMultipleChoiceFilter( name='tenant', queryset=Tenant.objects.all(), label='Tenant (ID)', ) - tenant = django_filters.ModelMultipleChoiceFilter( + tenant = NullableModelMultipleChoiceFilter( name='tenant', queryset=Tenant.objects.all(), to_field_name='slug', label='Tenant (slug)', ) - role_id = django_filters.ModelMultipleChoiceFilter( + role_id = NullableModelMultipleChoiceFilter( name='role', queryset=RackRole.objects.all(), label='Role (ID)', ) - role = django_filters.ModelMultipleChoiceFilter( + role = NullableModelMultipleChoiceFilter( name='role', queryset=RackRole.objects.all(), to_field_name='slug', @@ -177,12 +178,12 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet): to_field_name='slug', label='Role (slug)', ) - tenant_id = django_filters.ModelMultipleChoiceFilter( + tenant_id = NullableModelMultipleChoiceFilter( name='tenant', queryset=Tenant.objects.all(), label='Tenant (ID)', ) - tenant = django_filters.ModelMultipleChoiceFilter( + tenant = NullableModelMultipleChoiceFilter( name='tenant', queryset=Tenant.objects.all(), to_field_name='slug', @@ -210,12 +211,12 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet): to_field_name='slug', label='Device model (slug)', ) - platform_id = django_filters.ModelMultipleChoiceFilter( + platform_id = NullableModelMultipleChoiceFilter( name='platform', queryset=Platform.objects.all(), label='Platform (ID)', ) - platform = django_filters.ModelMultipleChoiceFilter( + platform = NullableModelMultipleChoiceFilter( name='platform', queryset=Platform.objects.all(), to_field_name='slug', diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 7625955b4..e296a221b 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -120,7 +120,8 @@ class SiteBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm): 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): model = Rack 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') - 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'), label='Rack Group') 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'], count_field='instances'), 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)) diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py index be2d127b5..faae390b3 100644 --- a/netbox/ipam/filters.py +++ b/netbox/ipam/filters.py @@ -7,6 +7,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 from .models import RIR, Aggregate, VRF, Prefix, IPAddress, VLAN, VLANGroup, Role @@ -21,12 +22,12 @@ class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet): lookup_type='icontains', label='Name', ) - tenant_id = django_filters.ModelMultipleChoiceFilter( + tenant_id = NullableModelMultipleChoiceFilter( name='tenant', queryset=Tenant.objects.all(), label='Tenant (ID)', ) - tenant = django_filters.ModelMultipleChoiceFilter( + tenant = NullableModelMultipleChoiceFilter( name='tenant', queryset=Tenant.objects.all(), to_field_name='slug', @@ -85,29 +86,34 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet): action='search_by_parent', label='Parent prefix', ) - vrf = django_filters.MethodFilter( - action='_vrf', + vrf = NullableModelMultipleChoiceFilter( + name='vrf', + queryset=VRF.objects.all(), label='VRF', ) # 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', ) - tenant_id = django_filters.MethodFilter( - action='_tenant_id', + tenant_id = NullableModelMultipleChoiceFilter( + name='tenant', + queryset=Tenant.objects.all(), 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', queryset=Site.objects.all(), label='Site (ID)', ) - site = django_filters.ModelMultipleChoiceFilter( + site = NullableModelMultipleChoiceFilter( name='site', queryset=Site.objects.all(), to_field_name='slug', @@ -122,12 +128,12 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet): name='vlan__vid', label='VLAN number (1-4095)', ) - role_id = django_filters.ModelMultipleChoiceFilter( + role_id = NullableModelMultipleChoiceFilter( name='role', queryset=Role.objects.all(), label='Role (ID)', ) - role = django_filters.ModelMultipleChoiceFilter( + role = NullableModelMultipleChoiceFilter( name='role', queryset=Role.objects.all(), to_field_name='slug', @@ -136,7 +142,7 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet): class Meta: 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): qs_filter = Q(description__icontains=value) @@ -157,17 +163,6 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet): except AddrFormatError: 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): if str(value) == '': return queryset @@ -196,22 +191,27 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet): action='search_by_parent', label='Parent prefix', ) - vrf = django_filters.MethodFilter( - action='_vrf', + vrf = NullableModelMultipleChoiceFilter( + name='vrf', + queryset=VRF.objects.all(), label='VRF', ) # 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', ) - tenant_id = django_filters.MethodFilter( - action='_tenant_id', + tenant_id = NullableModelMultipleChoiceFilter( + name='tenant', + queryset=Tenant.objects.all(), 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( name='interface__device', @@ -232,7 +232,7 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet): class Meta: 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): qs_filter = Q(description__icontains=value) @@ -317,12 +317,12 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet): to_field_name='slug', label='Site (slug)', ) - group_id = django_filters.ModelMultipleChoiceFilter( + group_id = NullableModelMultipleChoiceFilter( name='group', queryset=VLANGroup.objects.all(), label='Group (ID)', ) - group = django_filters.ModelMultipleChoiceFilter( + group = NullableModelMultipleChoiceFilter( name='group', queryset=VLANGroup.objects.all(), to_field_name='slug', @@ -337,23 +337,23 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet): name='vid', label='VLAN number (1-4095)', ) - tenant_id = django_filters.ModelMultipleChoiceFilter( + tenant_id = NullableModelMultipleChoiceFilter( name='tenant', queryset=Tenant.objects.all(), label='Tenant (ID)', ) - tenant = django_filters.ModelMultipleChoiceFilter( + tenant = NullableModelMultipleChoiceFilter( name='tenant', queryset=Tenant.objects.all(), to_field_name='slug', label='Tenant (slug)', ) - role_id = django_filters.ModelMultipleChoiceFilter( + role_id = NullableModelMultipleChoiceFilter( name='role', queryset=Role.objects.all(), label='Role (ID)', ) - role = django_filters.ModelMultipleChoiceFilter( + role = NullableModelMultipleChoiceFilter( name='role', queryset=Role.objects.all(), to_field_name='slug', diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 45fed46f2..480777aa6 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -74,7 +74,8 @@ class VRFBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): class VRFFilterForm(BootstrapMixin, CustomFieldFilterForm): 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', })) 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') - 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') 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') @@ -415,8 +419,10 @@ class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm): 'placeholder': 'Prefix', })) 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') @@ -521,8 +527,10 @@ def vlan_status_choices(): class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm): model = VLAN 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') - 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) - 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')) diff --git a/netbox/tenancy/filters.py b/netbox/tenancy/filters.py index 01d2d578d..6eeeb1e7b 100644 --- a/netbox/tenancy/filters.py +++ b/netbox/tenancy/filters.py @@ -3,6 +3,7 @@ import django_filters from django.db.models import Q from extras.filters import CustomFieldFilterSet +from utilities.filters import NullableModelMultipleChoiceFilter from .models import Tenant, TenantGroup @@ -11,12 +12,12 @@ class TenantFilter(CustomFieldFilterSet, django_filters.FilterSet): action='search', label='Search', ) - group_id = django_filters.ModelMultipleChoiceFilter( + group_id = NullableModelMultipleChoiceFilter( name='group', queryset=TenantGroup.objects.all(), label='Group (ID)', ) - group = django_filters.ModelMultipleChoiceFilter( + group = NullableModelMultipleChoiceFilter( name='group', queryset=TenantGroup.objects.all(), to_field_name='slug', diff --git a/netbox/tenancy/forms.py b/netbox/tenancy/forms.py index 8bbd05372..b011456df 100644 --- a/netbox/tenancy/forms.py +++ b/netbox/tenancy/forms.py @@ -77,4 +77,5 @@ class TenantBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): class TenantFilterForm(BootstrapMixin, CustomFieldFilterForm): 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')) diff --git a/netbox/utilities/filters.py b/netbox/utilities/filters.py new file mode 100644 index 000000000..861bb11dd --- /dev/null +++ b/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) diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index ed1ce1533..dbbee9f28 100644 --- a/netbox/utilities/forms.py +++ b/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 select_related: Any related tables to include :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() if select_related: @@ -54,7 +54,7 @@ def get_filter_choices(model, id_field='pk', select_related=[], count_field=None else: choices = [(getattr(obj, id_field), u'{}'.format(obj)) for obj in queryset] if null_option: - choices = [null_option] + choices + choices = [(0, null_option)] + choices return choices