From 546f17ab50a6b0ca2021e613334ba0be59cb7b21 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 7 Mar 2018 14:16:38 -0500 Subject: [PATCH] Closes #1866: Introduced AnnotatedMultipleChoiceField for filter forms --- netbox/circuits/forms.py | 18 +++++----- netbox/dcim/forms.py | 38 ++++++++++----------- netbox/ipam/forms.py | 62 +++++++++++++++------------------- netbox/utilities/forms.py | 33 ++++++++++++++++++ netbox/virtualization/forms.py | 18 +++++----- 5 files changed, 93 insertions(+), 76 deletions(-) diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index 29203fc8a..bfcfa7187 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -8,8 +8,8 @@ from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFi from tenancy.forms import TenancyForm from tenancy.models import Tenant from utilities.forms import ( - APISelect, add_blank_choice, BootstrapMixin, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, - CSVChoiceField, FilterChoiceField, SmallTextarea, SlugField, + AnnotatedMultipleChoiceField, APISelect, add_blank_choice, BootstrapMixin, ChainedFieldsMixin, + ChainedModelChoiceField, CommentField, CSVChoiceField, FilterChoiceField, SmallTextarea, SlugField, ) from .constants import CIRCUIT_STATUS_CHOICES from .models import Circuit, CircuitTermination, CircuitType, Provider @@ -169,13 +169,6 @@ class CircuitBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): nullable_fields = ['tenant', 'commit_rate', 'description', 'comments'] -def circuit_status_choices(): - status_counts = {} - for status in Circuit.objects.values('status').annotate(count=Count('status')).order_by('status'): - status_counts[status['status']] = status['count'] - return [(s[0], '{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in CIRCUIT_STATUS_CHOICES] - - class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm): model = Circuit q = forms.CharField(required=False, label='Search') @@ -187,7 +180,12 @@ class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm): queryset=Provider.objects.annotate(filter_count=Count('circuits')), to_field_name='slug' ) - status = forms.MultipleChoiceField(choices=circuit_status_choices, required=False) + status = AnnotatedMultipleChoiceField( + choices=CIRCUIT_STATUS_CHOICES, + annotate=Circuit.objects.all(), + annotate_field='status', + required=False + ) tenant = FilterChoiceField( queryset=Tenant.objects.annotate(filter_count=Count('circuits')), to_field_name='slug', diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 7a45b8dd8..d5089cc53 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -14,11 +14,11 @@ from ipam.models import IPAddress, VLAN, VLANGroup from tenancy.forms import TenancyForm from tenancy.models import Tenant from utilities.forms import ( - APISelect, APISelectMultiple, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, - BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, ChainedModelMultipleChoiceField, - CommentField, ComponentForm, ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, - FilterTreeNodeMultipleChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SelectWithPK, - SmallTextarea, SlugField, + AnnotatedMultipleChoiceField, APISelect, APISelectMultiple, add_blank_choice, ArrayFieldSelectMultiple, + BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, + ChainedModelMultipleChoiceField, CommentField, ComponentForm, ConfirmationForm, CSVChoiceField, ExpandableNameField, + FilterChoiceField, FilterTreeNodeMultipleChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, + SelectWithPK, SmallTextarea, SlugField, ) from virtualization.models import Cluster from .constants import ( @@ -172,17 +172,15 @@ class SiteBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): nullable_fields = ['region', 'tenant', 'asn', 'description', 'time_zone'] -def site_status_choices(): - status_counts = {} - for status in Site.objects.values('status').annotate(count=Count('status')).order_by('status'): - status_counts[status['status']] = status['count'] - return [(s[0], '{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in SITE_STATUS_CHOICES] - - class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm): model = Site q = forms.CharField(required=False, label='Search') - status = forms.MultipleChoiceField(choices=site_status_choices, required=False) + status = AnnotatedMultipleChoiceField( + choices=SITE_STATUS_CHOICES, + annotate=Site.objects.all(), + annotate_field='status', + required=False + ) region = FilterTreeNodeMultipleChoiceField( queryset=Region.objects.annotate(filter_count=Count('sites')), to_field_name='slug', @@ -1048,13 +1046,6 @@ class DeviceBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): nullable_fields = ['tenant', 'platform', 'serial'] -def device_status_choices(): - status_counts = {} - for status in Device.objects.values('status').annotate(count=Count('status')).order_by('status'): - status_counts[status['status']] = status['count'] - return [(s[0], '{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in DEVICE_STATUS_CHOICES] - - class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm): model = Device q = forms.CharField(required=False, label='Search') @@ -1092,7 +1083,12 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm): to_field_name='slug', null_label='-- None --', ) - status = forms.MultipleChoiceField(choices=device_status_choices, required=False) + status = AnnotatedMultipleChoiceField( + choices=DEVICE_STATUS_CHOICES, + annotate=Device.objects.all(), + annotate_field='status', + required=False + ) mac_address = forms.CharField(required=False, label='MAC address') has_primary_ip = forms.NullBooleanField( required=False, diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index c6d73e6f4..5b2c6e672 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -9,9 +9,9 @@ from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFi from tenancy.forms import TenancyForm from tenancy.models import Tenant from utilities.forms import ( - APISelect, BootstrapMixin, BulkEditNullBooleanSelect, ChainedModelChoiceField, CSVChoiceField, - ExpandableIPAddressField, FilterChoiceField, FlexibleModelChoiceField, Livesearch, ReturnURLForm, SlugField, - add_blank_choice, + AnnotatedMultipleChoiceField, APISelect, BootstrapMixin, BulkEditNullBooleanSelect, ChainedModelChoiceField, + CSVChoiceField, ExpandableIPAddressField, FilterChoiceField, FlexibleModelChoiceField, Livesearch, ReturnURLForm, + SlugField, add_blank_choice, ) from virtualization.models import VirtualMachine from .constants import IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, PREFIX_STATUS_CHOICES, VLAN_STATUS_CHOICES @@ -350,13 +350,6 @@ class PrefixBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): nullable_fields = ['site', 'vrf', 'tenant', 'role', 'description'] -def prefix_status_choices(): - status_counts = {} - for status in Prefix.objects.values('status').annotate(count=Count('status')).order_by('status'): - status_counts[status['status']] = status['count'] - return [(s[0], '{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in PREFIX_STATUS_CHOICES] - - class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm): model = Prefix q = forms.CharField(required=False, label='Search') @@ -376,7 +369,12 @@ class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm): to_field_name='slug', null_label='-- None --' ) - status = forms.MultipleChoiceField(choices=prefix_status_choices, required=False) + status = AnnotatedMultipleChoiceField( + choices=PREFIX_STATUS_CHOICES, + annotate=Prefix.objects.all(), + annotate_field='status', + required=False + ) site = FilterChoiceField( queryset=Site.objects.annotate(filter_count=Count('prefixes')), to_field_name='slug', @@ -688,20 +686,6 @@ class IPAddressAssignForm(BootstrapMixin, forms.Form): address = forms.CharField(label='IP Address') -def ipaddress_status_choices(): - status_counts = {} - for status in IPAddress.objects.values('status').annotate(count=Count('status')).order_by('status'): - status_counts[status['status']] = status['count'] - return [(s[0], '{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in IPADDRESS_STATUS_CHOICES] - - -def ipaddress_role_choices(): - role_counts = {} - for role in IPAddress.objects.values('role').annotate(count=Count('role')).order_by('role'): - role_counts[role['role']] = role['count'] - return [(r[0], '{} ({})'.format(r[1], role_counts.get(r[0], 0))) for r in IPADDRESS_ROLE_CHOICES] - - class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm): model = IPAddress q = forms.CharField(required=False, label='Search') @@ -721,8 +705,18 @@ class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm): to_field_name='slug', null_label='-- None --' ) - status = forms.MultipleChoiceField(choices=ipaddress_status_choices, required=False) - role = forms.MultipleChoiceField(choices=ipaddress_role_choices, required=False) + status = AnnotatedMultipleChoiceField( + choices=IPADDRESS_STATUS_CHOICES, + annotate=IPAddress.objects.all(), + annotate_field='status', + required=False + ) + role = AnnotatedMultipleChoiceField( + choices=IPADDRESS_ROLE_CHOICES, + annotate=IPAddress.objects.all(), + annotate_field='role', + required=False + ) # @@ -878,13 +872,6 @@ class VLANBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): nullable_fields = ['site', 'group', 'tenant', 'role', 'description'] -def vlan_status_choices(): - status_counts = {} - for status in VLAN.objects.values('status').annotate(count=Count('status')).order_by('status'): - status_counts[status['status']] = status['count'] - return [(s[0], '{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in VLAN_STATUS_CHOICES] - - class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm): model = VLAN q = forms.CharField(required=False, label='Search') @@ -903,7 +890,12 @@ class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm): to_field_name='slug', null_label='-- None --' ) - status = forms.MultipleChoiceField(choices=vlan_status_choices, required=False) + status = AnnotatedMultipleChoiceField( + choices=VLAN_STATUS_CHOICES, + annotate=VLAN.objects.all(), + annotate_field='status', + required=False + ) role = FilterChoiceField( queryset=Role.objects.annotate(filter_count=Count('vlans')), to_field_name='slug', diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index a2bfef001..6bf84cbe0 100644 --- a/netbox/utilities/forms.py +++ b/netbox/utilities/forms.py @@ -6,6 +6,7 @@ import re from django import forms from django.conf import settings +from django.db.models import Count from django.urls import reverse_lazy from mptt.forms import TreeNodeMultipleChoiceField @@ -450,6 +451,38 @@ class FilterTreeNodeMultipleChoiceField(FilterChoiceFieldMixin, TreeNodeMultiple pass +class AnnotatedMultipleChoiceField(forms.MultipleChoiceField): + """ + Render a set of static choices with each choice annotated to include a count of related objects. For example, this + field can be used to display a list of all available device statuses along with the number of devices currently + assigned to each status. + """ + + def annotate_choices(self): + queryset = self.annotate.values( + self.annotate_field + ).annotate( + count=Count(self.annotate_field) + ).order_by( + self.annotate_field + ) + choice_counts = { + c[self.annotate_field]: c['count'] for c in queryset + } + annotated_choices = [ + (c[0], '{} ({})'.format(c[1], choice_counts.get(c[0], 0))) for c in self.static_choices + ] + + return annotated_choices + + def __init__(self, choices, annotate, annotate_field, *args, **kwargs): + self.annotate = annotate + self.annotate_field = annotate_field + self.static_choices = choices + + super(AnnotatedMultipleChoiceField, self).__init__(choices=self.annotate_choices, *args, **kwargs) + + class LaxURLField(forms.URLField): """ Modifies Django's built-in URLField in two ways: diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index 06b992203..e049767ae 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -13,9 +13,9 @@ from ipam.models import IPAddress from tenancy.forms import TenancyForm from tenancy.models import Tenant from utilities.forms import ( - add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, + AnnotatedMultipleChoiceField, APISelect, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, ChainedModelMultipleChoiceField, CommentField, ComponentForm, - ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, SlugField, SmallTextarea, + ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, SlugField, SmallTextarea, add_blank_choice ) from .constants import VM_STATUS_CHOICES from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine @@ -361,13 +361,6 @@ class VirtualMachineBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): nullable_fields = ['role', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments'] -def vm_status_choices(): - status_counts = {} - for status in VirtualMachine.objects.values('status').annotate(count=Count('status')).order_by('status'): - status_counts[status['status']] = status['count'] - return [(s[0], '{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in VM_STATUS_CHOICES] - - class VirtualMachineFilterForm(BootstrapMixin, CustomFieldFilterForm): model = VirtualMachine q = forms.CharField(required=False, label='Search') @@ -395,7 +388,12 @@ class VirtualMachineFilterForm(BootstrapMixin, CustomFieldFilterForm): to_field_name='slug', null_label='-- None --' ) - status = forms.MultipleChoiceField(choices=vm_status_choices, required=False) + status = AnnotatedMultipleChoiceField( + choices=VM_STATUS_CHOICES, + annotate=VirtualMachine.objects.all(), + annotate_field='status', + required=False + ) tenant = FilterChoiceField( queryset=Tenant.objects.annotate(filter_count=Count('virtual_machines')), to_field_name='slug',