Browse Source

Fixes #1765: Improved rendering of null options for model choice fields in filter forms

Jeremy Stretch 7 years ago
parent
commit
78ed85943b

+ 1 - 1
netbox/circuits/forms.py

@@ -174,7 +174,7 @@ class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
     tenant = FilterChoiceField(
         queryset=Tenant.objects.annotate(filter_count=Count('circuits')),
         to_field_name='slug',
-        null_option=(0, 'None')
+        null_label='-- None --'
     )
     site = FilterChoiceField(
         queryset=Site.objects.annotate(filter_count=Count('circuit_terminations')),

+ 8 - 8
netbox/dcim/forms.py

@@ -163,7 +163,7 @@ class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm):
     tenant = FilterChoiceField(
         queryset=Tenant.objects.annotate(filter_count=Count('sites')),
         to_field_name='slug',
-        null_option=(0, 'None')
+        null_label='-- None --'
     )
 
 
@@ -359,17 +359,17 @@ class RackFilterForm(BootstrapMixin, CustomFieldFilterForm):
     group_id = FilterChoiceField(
         queryset=RackGroup.objects.select_related('site').annotate(filter_count=Count('racks')),
         label='Rack group',
-        null_option=(0, 'None')
+        null_label='-- None --'
     )
     tenant = FilterChoiceField(
         queryset=Tenant.objects.annotate(filter_count=Count('racks')),
         to_field_name='slug',
-        null_option=(0, 'None')
+        null_label='-- None --'
     )
     role = FilterChoiceField(
         queryset=RackRole.objects.annotate(filter_count=Count('racks')),
         to_field_name='slug',
-        null_option=(0, 'None')
+        null_label='-- None --'
     )
 
 
@@ -411,7 +411,7 @@ class RackReservationFilterForm(BootstrapMixin, forms.Form):
     group_id = FilterChoiceField(
         queryset=RackGroup.objects.select_related('site').annotate(filter_count=Count('racks__reservations')),
         label='Rack group',
-        null_option=(0, 'None')
+        null_label='-- None --'
     )
 
 
@@ -1031,7 +1031,7 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
     rack_id = FilterChoiceField(
         queryset=Rack.objects.annotate(filter_count=Count('devices')),
         label='Rack',
-        null_option=(0, 'None'),
+        null_label='-- None --',
     )
     role = FilterChoiceField(
         queryset=DeviceRole.objects.annotate(filter_count=Count('devices')),
@@ -1040,7 +1040,7 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
     tenant = FilterChoiceField(
         queryset=Tenant.objects.annotate(filter_count=Count('devices')),
         to_field_name='slug',
-        null_option=(0, 'None'),
+        null_label='-- None --',
     )
     manufacturer_id = FilterChoiceField(queryset=Manufacturer.objects.all(), label='Manufacturer')
     device_type_id = FilterChoiceField(
@@ -1052,7 +1052,7 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
     platform = FilterChoiceField(
         queryset=Platform.objects.annotate(filter_count=Count('devices')),
         to_field_name='slug',
-        null_option=(0, 'None'),
+        null_label='-- None --',
     )
     status = forms.MultipleChoiceField(choices=device_status_choices, required=False)
     mac_address = forms.CharField(required=False, label='MAC address')

+ 16 - 13
netbox/ipam/forms.py

@@ -78,8 +78,11 @@ class VRFBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
 class VRFFilterForm(BootstrapMixin, CustomFieldFilterForm):
     model = VRF
     q = forms.CharField(required=False, label='Search')
-    tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('vrfs')), to_field_name='slug',
-                               null_option=(0, None))
+    tenant = FilterChoiceField(
+        queryset=Tenant.objects.annotate(filter_count=Count('vrfs')),
+        to_field_name='slug',
+        null_label='-- None --'
+    )
 
 
 #
@@ -368,23 +371,23 @@ class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm):
         queryset=VRF.objects.annotate(filter_count=Count('prefixes')),
         to_field_name='rd',
         label='VRF',
-        null_option=(0, 'Global')
+        null_label='-- Global --'
     )
     tenant = FilterChoiceField(
         queryset=Tenant.objects.annotate(filter_count=Count('prefixes')),
         to_field_name='slug',
-        null_option=(0, 'None')
+        null_label='-- None --'
     )
     status = forms.MultipleChoiceField(choices=prefix_status_choices, required=False)
     site = FilterChoiceField(
         queryset=Site.objects.annotate(filter_count=Count('prefixes')),
         to_field_name='slug',
-        null_option=(0, 'None')
+        null_label='-- None --'
     )
     role = FilterChoiceField(
         queryset=Role.objects.annotate(filter_count=Count('prefixes')),
         to_field_name='slug',
-        null_option=(0, 'None')
+        null_label='-- None --'
     )
     expand = forms.BooleanField(required=False, label='Expand prefix hierarchy')
 
@@ -719,12 +722,12 @@ class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm):
         queryset=VRF.objects.annotate(filter_count=Count('ip_addresses')),
         to_field_name='rd',
         label='VRF',
-        null_option=(0, 'Global')
+        null_label='-- Global --'
     )
     tenant = FilterChoiceField(
         queryset=Tenant.objects.annotate(filter_count=Count('ip_addresses')),
         to_field_name='slug',
-        null_option=(0, 'None')
+        null_label='-- None --'
     )
     status = forms.MultipleChoiceField(choices=ipaddress_status_choices, required=False)
     role = forms.MultipleChoiceField(choices=ipaddress_role_choices, required=False)
@@ -766,7 +769,7 @@ class VLANGroupFilterForm(BootstrapMixin, forms.Form):
     site = FilterChoiceField(
         queryset=Site.objects.annotate(filter_count=Count('vlan_groups')),
         to_field_name='slug',
-        null_option=(0, 'Global')
+        null_label='-- Global --'
     )
 
 
@@ -896,23 +899,23 @@ class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm):
     site = FilterChoiceField(
         queryset=Site.objects.annotate(filter_count=Count('vlans')),
         to_field_name='slug',
-        null_option=(0, 'Global')
+        null_label='-- Global --'
     )
     group_id = FilterChoiceField(
         queryset=VLANGroup.objects.annotate(filter_count=Count('vlans')),
         label='VLAN group',
-        null_option=(0, 'None')
+        null_label='-- None --'
     )
     tenant = FilterChoiceField(
         queryset=Tenant.objects.annotate(filter_count=Count('vlans')),
         to_field_name='slug',
-        null_option=(0, 'None')
+        null_label='-- None --'
     )
     status = forms.MultipleChoiceField(choices=vlan_status_choices, required=False)
     role = FilterChoiceField(
         queryset=Role.objects.annotate(filter_count=Count('vlans')),
         to_field_name='slug',
-        null_option=(0, 'None')
+        null_label='-- None --'
     )
 
 

+ 1 - 1
netbox/tenancy/forms.py

@@ -81,7 +81,7 @@ class TenantFilterForm(BootstrapMixin, CustomFieldFilterForm):
     group = FilterChoiceField(
         queryset=TenantGroup.objects.annotate(filter_count=Count('tenants')),
         to_field_name='slug',
-        null_option=(0, 'None')
+        null_label='-- None --'
     )
 
 

+ 1 - 1
netbox/utilities/filters.py

@@ -42,7 +42,7 @@ class NullableModelMultipleChoiceField(forms.ModelMultipleChoiceField):
     """
     iterator = forms.models.ModelChoiceIterator
 
-    def __init__(self, null_value=0, null_label='None', *args, **kwargs):
+    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)

+ 17 - 12
netbox/utilities/forms.py

@@ -407,11 +407,25 @@ class SlugField(forms.SlugField):
         self.widget.attrs['slug-source'] = slug_source
 
 
+class FilterChoiceIterator(forms.models.ModelChoiceIterator):
+
+    def __iter__(self):
+        # Filter on "empty" choice using FILTERS_NULL_CHOICE_VALUE (instead of an empty string)
+        if self.field.null_label is not None:
+            yield (settings.FILTERS_NULL_CHOICE_VALUE, self.field.null_label)
+        queryset = self.queryset.all()
+        # Can't use iterator() when queryset uses prefetch_related()
+        if not queryset._prefetch_related_lookups:
+            queryset = queryset.iterator()
+        for obj in queryset:
+            yield self.choice(obj)
+
+
 class FilterChoiceFieldMixin(object):
-    iterator = forms.models.ModelChoiceIterator
+    iterator = FilterChoiceIterator
 
-    def __init__(self, null_option=None, *args, **kwargs):
-        self.null_option = null_option
+    def __init__(self, null_label=None, *args, **kwargs):
+        self.null_label = null_label
         if 'required' not in kwargs:
             kwargs['required'] = False
         if 'widget' not in kwargs:
@@ -424,15 +438,6 @@ class FilterChoiceFieldMixin(object):
             return '{} ({})'.format(label, obj.filter_count)
         return label
 
-    def _get_choices(self):
-        if hasattr(self, '_choices'):
-            return self._choices
-        if self.null_option is not None:
-            return itertools.chain([self.null_option], self.iterator(self))
-        return self.iterator(self)
-
-    choices = property(_get_choices, forms.ChoiceField._set_choices)
-
 
 class FilterChoiceField(FilterChoiceFieldMixin, forms.ModelMultipleChoiceField):
     pass

+ 8 - 8
netbox/virtualization/forms.py

@@ -137,13 +137,13 @@ class ClusterFilterForm(BootstrapMixin, CustomFieldFilterForm):
     group = FilterChoiceField(
         queryset=ClusterGroup.objects.annotate(filter_count=Count('clusters')),
         to_field_name='slug',
-        null_option=(0, 'None'),
+        null_label='-- None --',
         required=False,
     )
     site = FilterChoiceField(
         queryset=Site.objects.annotate(filter_count=Count('clusters')),
         to_field_name='slug',
-        null_option=(0, 'None'),
+        null_label='-- None --',
         required=False,
     )
 
@@ -338,12 +338,12 @@ class VirtualMachineFilterForm(BootstrapMixin, CustomFieldFilterForm):
     cluster_group = FilterChoiceField(
         queryset=ClusterGroup.objects.all(),
         to_field_name='slug',
-        null_option=(0, 'None')
+        null_label='-- None --'
     )
     cluster_type = FilterChoiceField(
         queryset=ClusterType.objects.all(),
         to_field_name='slug',
-        null_option=(0, 'None')
+        null_label='-- None --'
     )
     cluster_id = FilterChoiceField(
         queryset=Cluster.objects.annotate(filter_count=Count('virtual_machines')),
@@ -352,23 +352,23 @@ class VirtualMachineFilterForm(BootstrapMixin, CustomFieldFilterForm):
     site = FilterChoiceField(
         queryset=Site.objects.annotate(filter_count=Count('clusters__virtual_machines')),
         to_field_name='slug',
-        null_option=(0, 'None')
+        null_label='-- None --'
     )
     role = FilterChoiceField(
         queryset=DeviceRole.objects.filter(vm_role=True).annotate(filter_count=Count('virtual_machines')),
         to_field_name='slug',
-        null_option=(0, 'None')
+        null_label='-- None --'
     )
     status = forms.MultipleChoiceField(choices=vm_status_choices, required=False)
     tenant = FilterChoiceField(
         queryset=Tenant.objects.annotate(filter_count=Count('virtual_machines')),
         to_field_name='slug',
-        null_option=(0, 'None')
+        null_label='-- None --'
     )
     platform = FilterChoiceField(
         queryset=Platform.objects.annotate(filter_count=Count('virtual_machines')),
         to_field_name='slug',
-        null_option=(0, 'None')
+        null_label='-- None --'
     )