From 0444ac7db9faa06bbe4125eb9a17a90dc8763a5f Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 20 Sep 2016 15:48:58 -0400 Subject: [PATCH] Introduced NullableModelMultipleChoiceField to allow null filtering without causing introspection issues during database migrations --- netbox/ipam/filters.py | 53 +++++++-------------------- netbox/ipam/forms.py | 2 +- netbox/utilities/filters.py | 72 +++++++++++++++++++++++++++++++------ netbox/utilities/forms.py | 1 - 4 files changed, 74 insertions(+), 54 deletions(-) diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py index faae390b3..a998bb2a0 100644 --- a/netbox/ipam/filters.py +++ b/netbox/ipam/filters.py @@ -86,17 +86,17 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet): action='search_by_parent', label='Parent prefix', ) - vrf = NullableModelMultipleChoiceFilter( - name='vrf', - queryset=VRF.objects.all(), - label='VRF', - ) - # Duplicate of `vrf` for backward-compatibility vrf_id = NullableModelMultipleChoiceFilter( name='vrf_id', queryset=VRF.objects.all(), label='VRF', ) + vrf = NullableModelMultipleChoiceFilter( + name='vrf', + queryset=VRF.objects.all(), + to_field_name='rd', + label='VRF (RD)', + ) tenant_id = NullableModelMultipleChoiceFilter( name='tenant', queryset=Tenant.objects.all(), @@ -191,17 +191,17 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet): action='search_by_parent', label='Parent prefix', ) - vrf = NullableModelMultipleChoiceFilter( - name='vrf', - queryset=VRF.objects.all(), - label='VRF', - ) - # Duplicate of `vrf` for backward-compatibility vrf_id = NullableModelMultipleChoiceFilter( name='vrf_id', queryset=VRF.objects.all(), label='VRF', ) + vrf = NullableModelMultipleChoiceFilter( + name='vrf', + queryset=VRF.objects.all(), + to_field_name='rd', + label='VRF (RD)', + ) tenant_id = NullableModelMultipleChoiceFilter( name='tenant', queryset=Tenant.objects.all(), @@ -253,35 +253,6 @@ class IPAddressFilter(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 - return queryset.filter( - Q(tenant__slug=value) | - Q(tenant__isnull=True, vrf__tenant__slug=value) - ) - - def _tenant_id(self, queryset, value): - try: - value = int(value) - except ValueError: - return queryset.none() - return queryset.filter( - Q(tenant__pk=value) | - Q(tenant__isnull=True, vrf__tenant__pk=value) - ) - class VLANGroupFilter(django_filters.FilterSet): site_id = django_filters.ModelMultipleChoiceFilter( diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index b2b39177e..6382895f8 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -418,7 +418,7 @@ class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm): 'placeholder': 'Prefix', })) family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address Family') - vrf = FilterChoiceField(queryset=VRF.objects.annotate(filter_count=Count('ip_addresses')), to_field_name='slug', + vrf = FilterChoiceField(queryset=VRF.objects.annotate(filter_count=Count('ip_addresses')), to_field_name='rd', label='VRF', null_option=(0, 'Global')) tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('ip_addresses')), to_field_name='slug', null_option=(0, 'None')) diff --git a/netbox/utilities/filters.py b/netbox/utilities/filters.py index 861bb11dd..d1dbf39b8 100644 --- a/netbox/utilities/filters.py +++ b/netbox/utilities/filters.py @@ -1,15 +1,65 @@ import django_filters +import itertools +from django import forms from django.db.models import Q +from django.utils.encoding import force_text -class NullableModelMultipleChoiceFilter(django_filters.MultipleChoiceFilter): +class NullableModelMultipleChoiceField(forms.ModelMultipleChoiceField): + """ + This field operates like a normal ModelMultipleChoiceField except that it allows for one additional choice which is + used to represent a value of Null. This is accomplished by creating a new iterator which first yields the null + choice before entering the queryset iterator, and by ignoring the null choice during cleaning. The effect is similar + to defining a MultipleChoiceField with: + + choices = [(0, 'None')] + [(x.id, x) for x in Foo.objects.all()] + + However, the above approach forces immediate evaluation of the queryset, which can cause issues when calculating + database migrations. + """ + iterator = forms.models.ModelChoiceIterator + + def __init__(self, null_value=0, null_label='None', *args, **kwargs): + self.null_value = null_value + self.null_label = null_label + super(NullableModelMultipleChoiceField, self).__init__(*args, **kwargs) + + def _get_choices(self): + if hasattr(self, '_choices'): + return self._choices + # Prepend the null choice to the queryset iterator + return itertools.chain( + [(self.null_value, self.null_label)], + self.iterator(self), + ) + choices = property(_get_choices, forms.ChoiceField._set_choices) + + def clean(self, value): + # Strip all instances of the null value before cleaning + if value is not None: + stripped_value = [x for x in value if x != force_text(self.null_value)] + else: + 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): - # 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] + self.null_value = kwargs.get('null_value', 0) super(NullableModelMultipleChoiceFilter, self).__init__(*args, **kwargs) def filter(self, qs, value): @@ -24,13 +74,13 @@ class NullableModelMultipleChoiceFilter(django_filters.MultipleChoiceFilter): q = Q() for v in set(value): - # Filtering on NULL - if v == str(0): + # Filtering by "is null" + if v == force_text(self.null_value): 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 + # 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: diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index 247ea9aed..1df25891b 100644 --- a/netbox/utilities/forms.py +++ b/netbox/utilities/forms.py @@ -4,7 +4,6 @@ import re from django import forms from django.core.urlresolvers import reverse_lazy -from django.db.models import Count from django.utils.encoding import force_text from django.utils.html import format_html from django.utils.safestring import mark_safe