Parcourir la source

Closes #1866: Introduced AnnotatedMultipleChoiceField for filter forms

Jeremy Stretch il y a 7 ans
Parent
commit
546f17ab50
5 fichiers modifiés avec 93 ajouts et 76 suppressions
  1. 8 10
      netbox/circuits/forms.py
  2. 17 21
      netbox/dcim/forms.py
  3. 27 35
      netbox/ipam/forms.py
  4. 33 0
      netbox/utilities/forms.py
  5. 8 10
      netbox/virtualization/forms.py

+ 8 - 10
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',

+ 17 - 21
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,

+ 27 - 35
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',

+ 33 - 0
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:

+ 8 - 10
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',