Просмотр исходного кода

Merge pull request #3068 from digitalocean/1863-api-child-counts

Add child objects counts to API serializers
Jeremy Stretch 6 лет назад
Родитель
Сommit
c484b27a35

+ 3 - 0
CHANGELOG.md

@@ -53,6 +53,7 @@ to now use "Extras | Tag."
 
 * [#323](https://github.com/digitalocean/netbox/issues/323) - Enforce per-object type view permissions
 * [#1792](https://github.com/digitalocean/netbox/issues/1792) - Add CustomFieldChoices API endpoint
+* [#1863](https://github.com/digitalocean/netbox/issues/1863) - Add child object counts to API representation of organizational objects
 * [#2324](https://github.com/digitalocean/netbox/issues/2324) - Add `color` option for tags
 * [#2643](https://github.com/digitalocean/netbox/issues/2643) - Add `description` field to console/power components and device bays
 * [#2791](https://github.com/digitalocean/netbox/issues/2791) - Add a `comment` field for tags
@@ -63,6 +64,8 @@ to now use "Extras | Tag."
 
 * dcim.Interface: `form_factor` has been renamed to `type`. Backward-compatibile support for `form_factor` will be maintained until NetBox v2.7.
 * dcim.Interface: The `type` filter has been renamed to `kind`.
+* dcim.DeviceType: `instance_count` has been renamed to `device_count`.
+* Organizational objects now include child object counts. For example, the Role serializer includes `prefix_count` and `vlan_count`.
 
 ## Bug Fixes
 

+ 4 - 2
netbox/circuits/api/nested_serializers.py

@@ -17,10 +17,11 @@ __all__ = [
 
 class NestedProviderSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
+    circuit_count = serializers.IntegerField(read_only=True)
 
     class Meta:
         model = Provider
-        fields = ['id', 'url', 'name', 'slug']
+        fields = ['id', 'url', 'name', 'slug', 'circuit_count']
 
 
 #
@@ -29,10 +30,11 @@ class NestedProviderSerializer(WritableNestedSerializer):
 
 class NestedCircuitTypeSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
+    circuit_count = serializers.IntegerField(read_only=True)
 
     class Meta:
         model = CircuitType
-        fields = ['id', 'url', 'name', 'slug']
+        fields = ['id', 'url', 'name', 'slug', 'circuit_count']
 
 
 class NestedCircuitSerializer(WritableNestedSerializer):

+ 5 - 2
netbox/circuits/api/serializers.py

@@ -1,3 +1,4 @@
+from rest_framework import serializers
 from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
 
 from circuits.constants import CIRCUIT_STATUS_CHOICES
@@ -16,12 +17,13 @@ from .nested_serializers import *
 
 class ProviderSerializer(TaggitSerializer, CustomFieldModelSerializer):
     tags = TagListSerializerField(required=False)
+    circuit_count = serializers.IntegerField(read_only=True)
 
     class Meta:
         model = Provider
         fields = [
             'id', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', 'tags',
-            'custom_fields', 'created', 'last_updated',
+            'custom_fields', 'created', 'last_updated', 'circuit_count',
         ]
 
 
@@ -30,10 +32,11 @@ class ProviderSerializer(TaggitSerializer, CustomFieldModelSerializer):
 #
 
 class CircuitTypeSerializer(ValidatedModelSerializer):
+    circuit_count = serializers.IntegerField(read_only=True)
 
     class Meta:
         model = CircuitType
-        fields = ['id', 'name', 'slug']
+        fields = ['id', 'name', 'slug', 'circuit_count']
 
 
 class CircuitSerializer(TaggitSerializer, CustomFieldModelSerializer):

+ 7 - 2
netbox/circuits/api/views.py

@@ -1,3 +1,4 @@
+from django.db.models import Count
 from django.shortcuts import get_object_or_404
 from rest_framework.decorators import action
 from rest_framework.response import Response
@@ -27,7 +28,9 @@ class CircuitsFieldChoicesViewSet(FieldChoicesViewSet):
 #
 
 class ProviderViewSet(CustomFieldModelViewSet):
-    queryset = Provider.objects.prefetch_related('tags')
+    queryset = Provider.objects.prefetch_related('tags').annotate(
+        circuit_count=Count('circuits')
+    )
     serializer_class = serializers.ProviderSerializer
     filterset_class = filters.ProviderFilter
 
@@ -47,7 +50,9 @@ class ProviderViewSet(CustomFieldModelViewSet):
 #
 
 class CircuitTypeViewSet(ModelViewSet):
-    queryset = CircuitType.objects.all()
+    queryset = CircuitType.objects.annotate(
+        circuit_count=Count('circuits')
+    )
     serializer_class = serializers.CircuitTypeSerializer
     filterset_class = filters.CircuitTypeFilter
 

+ 2 - 2
netbox/circuits/tests/test_api.py

@@ -61,7 +61,7 @@ class ProviderTest(APITestCase):
 
         self.assertEqual(
             sorted(response.data['results'][0]),
-            ['id', 'name', 'slug', 'url']
+            ['circuit_count', 'id', 'name', 'slug', 'url']
         )
 
     def test_create_provider(self):
@@ -162,7 +162,7 @@ class CircuitTypeTest(APITestCase):
 
         self.assertEqual(
             sorted(response.data['results'][0]),
-            ['id', 'name', 'slug', 'url']
+            ['circuit_count', 'id', 'name', 'slug', 'url']
         )
 
     def test_create_circuittype(self):

+ 22 - 10
netbox/dcim/api/nested_serializers.py

@@ -42,10 +42,11 @@ __all__ = [
 
 class NestedRegionSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail')
+    site_count = serializers.IntegerField(read_only=True)
 
     class Meta:
         model = Region
-        fields = ['id', 'url', 'name', 'slug']
+        fields = ['id', 'url', 'name', 'slug', 'site_count']
 
 
 class NestedSiteSerializer(WritableNestedSerializer):
@@ -62,26 +63,29 @@ class NestedSiteSerializer(WritableNestedSerializer):
 
 class NestedRackGroupSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackgroup-detail')
+    rack_count = serializers.IntegerField(read_only=True)
 
     class Meta:
         model = RackGroup
-        fields = ['id', 'url', 'name', 'slug']
+        fields = ['id', 'url', 'name', 'slug', 'rack_count']
 
 
 class NestedRackRoleSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
+    rack_count = serializers.IntegerField(read_only=True)
 
     class Meta:
         model = RackRole
-        fields = ['id', 'url', 'name', 'slug']
+        fields = ['id', 'url', 'name', 'slug', 'rack_count']
 
 
 class NestedRackSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail')
+    device_count = serializers.IntegerField(read_only=True)
 
     class Meta:
         model = Rack
-        fields = ['id', 'url', 'name', 'display_name']
+        fields = ['id', 'url', 'name', 'display_name', 'device_count']
 
 
 #
@@ -90,19 +94,21 @@ class NestedRackSerializer(WritableNestedSerializer):
 
 class NestedManufacturerSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
+    devicetype_count = serializers.IntegerField(read_only=True)
 
     class Meta:
         model = Manufacturer
-        fields = ['id', 'url', 'name', 'slug']
+        fields = ['id', 'url', 'name', 'slug', 'devicetype_count']
 
 
 class NestedDeviceTypeSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail')
     manufacturer = NestedManufacturerSerializer(read_only=True)
+    device_count = serializers.IntegerField(read_only=True)
 
     class Meta:
         model = DeviceType
-        fields = ['id', 'url', 'manufacturer', 'model', 'slug', 'display_name']
+        fields = ['id', 'url', 'manufacturer', 'model', 'slug', 'display_name', 'device_count']
 
 
 class NestedRearPortTemplateSerializer(WritableNestedSerializer):
@@ -127,18 +133,22 @@ class NestedFrontPortTemplateSerializer(WritableNestedSerializer):
 
 class NestedDeviceRoleSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
+    device_count = serializers.IntegerField(read_only=True)
+    virtualmachine_count = serializers.IntegerField(read_only=True)
 
     class Meta:
         model = DeviceRole
-        fields = ['id', 'url', 'name', 'slug']
+        fields = ['id', 'url', 'name', 'slug', 'device_count', 'virtualmachine_count']
 
 
 class NestedPlatformSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
+    device_count = serializers.IntegerField(read_only=True)
+    virtualmachine_count = serializers.IntegerField(read_only=True)
 
     class Meta:
         model = Platform
-        fields = ['id', 'url', 'name', 'slug']
+        fields = ['id', 'url', 'name', 'slug', 'device_count', 'virtualmachine_count']
 
 
 class NestedDeviceSerializer(WritableNestedSerializer):
@@ -245,10 +255,11 @@ class NestedCableSerializer(serializers.ModelSerializer):
 class NestedVirtualChassisSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
     master = NestedDeviceSerializer()
+    member_count = serializers.IntegerField(read_only=True)
 
     class Meta:
         model = VirtualChassis
-        fields = ['id', 'url', 'master']
+        fields = ['id', 'url', 'master', 'member_count']
 
 
 #
@@ -257,10 +268,11 @@ class NestedVirtualChassisSerializer(WritableNestedSerializer):
 
 class NestedPowerPanelSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerpanel-detail')
+    powerfeed_count = serializers.IntegerField(read_only=True)
 
     class Meta:
         model = PowerPanel
-        fields = ['id', 'url', 'name']
+        fields = ['id', 'url', 'name', 'powerfeed_count']
 
 
 class NestedPowerFeedSerializer(WritableNestedSerializer):

+ 25 - 12
netbox/dcim/api/serializers.py

@@ -59,10 +59,11 @@ class ConnectedEndpointSerializer(ValidatedModelSerializer):
 
 class RegionSerializer(serializers.ModelSerializer):
     parent = NestedRegionSerializer(required=False, allow_null=True)
+    site_count = serializers.IntegerField(read_only=True)
 
     class Meta:
         model = Region
-        fields = ['id', 'name', 'slug', 'parent']
+        fields = ['id', 'name', 'slug', 'parent', 'site_count']
 
 
 class SiteSerializer(TaggitSerializer, CustomFieldModelSerializer):
@@ -93,17 +94,19 @@ class SiteSerializer(TaggitSerializer, CustomFieldModelSerializer):
 
 class RackGroupSerializer(ValidatedModelSerializer):
     site = NestedSiteSerializer()
+    rack_count = serializers.IntegerField(read_only=True)
 
     class Meta:
         model = RackGroup
-        fields = ['id', 'name', 'slug', 'site']
+        fields = ['id', 'name', 'slug', 'site', 'rack_count']
 
 
 class RackRoleSerializer(ValidatedModelSerializer):
+    rack_count = serializers.IntegerField(read_only=True)
 
     class Meta:
         model = RackRole
-        fields = ['id', 'name', 'slug', 'color']
+        fields = ['id', 'name', 'slug', 'color', 'rack_count']
 
 
 class RackSerializer(TaggitSerializer, CustomFieldModelSerializer):
@@ -116,13 +119,14 @@ class RackSerializer(TaggitSerializer, CustomFieldModelSerializer):
     width = ChoiceField(choices=RACK_WIDTH_CHOICES, required=False)
     outer_unit = ChoiceField(choices=RACK_DIMENSION_UNIT_CHOICES, required=False)
     tags = TagListSerializerField(required=False)
+    device_count = serializers.IntegerField(read_only=True)
 
     class Meta:
         model = Rack
         fields = [
             'id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'status', 'role', 'serial',
             'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit',
-            'comments', 'tags', 'custom_fields', 'created', 'last_updated',
+            'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count',
         ]
         # Omit the UniqueTogetherValidator that would be automatically added to validate (group, facility_id). This
         # prevents facility_id from being interpreted as a required field.
@@ -169,23 +173,24 @@ class RackReservationSerializer(ValidatedModelSerializer):
 #
 
 class ManufacturerSerializer(ValidatedModelSerializer):
+    devicetype_count = serializers.IntegerField(read_only=True)
 
     class Meta:
         model = Manufacturer
-        fields = ['id', 'name', 'slug']
+        fields = ['id', 'name', 'slug', 'devicetype_count']
 
 
 class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer):
     manufacturer = NestedManufacturerSerializer()
     subdevice_role = ChoiceField(choices=SUBDEVICE_ROLE_CHOICES, required=False, allow_null=True)
-    instance_count = serializers.IntegerField(source='instances.count', read_only=True)
     tags = TagListSerializerField(required=False)
+    device_count = serializers.IntegerField(read_only=True)
 
     class Meta:
         model = DeviceType
         fields = [
             'id', 'manufacturer', 'model', 'slug', 'display_name', 'part_number', 'u_height', 'is_full_depth',
-            'subdevice_role', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'instance_count',
+            'subdevice_role', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count',
         ]
 
 
@@ -272,18 +277,25 @@ class DeviceBayTemplateSerializer(ValidatedModelSerializer):
 #
 
 class DeviceRoleSerializer(ValidatedModelSerializer):
+    device_count = serializers.IntegerField(read_only=True)
+    virtualmachine_count = serializers.IntegerField(read_only=True)
 
     class Meta:
         model = DeviceRole
-        fields = ['id', 'name', 'slug', 'color', 'vm_role']
+        fields = ['id', 'name', 'slug', 'color', 'vm_role', 'device_count', 'virtualmachine_count']
 
 
 class PlatformSerializer(ValidatedModelSerializer):
     manufacturer = NestedManufacturerSerializer(required=False, allow_null=True)
+    device_count = serializers.IntegerField(read_only=True)
+    virtualmachine_count = serializers.IntegerField(read_only=True)
 
     class Meta:
         model = Platform
-        fields = ['id', 'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args']
+        fields = [
+            'id', 'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'device_count',
+            'virtualmachine_count',
+        ]
 
 
 class DeviceSerializer(TaggitSerializer, CustomFieldModelSerializer):
@@ -613,17 +625,17 @@ class InterfaceConnectionSerializer(ValidatedModelSerializer):
 class VirtualChassisSerializer(TaggitSerializer, ValidatedModelSerializer):
     master = NestedDeviceSerializer()
     tags = TagListSerializerField(required=False)
+    member_count = serializers.IntegerField(read_only=True)
 
     class Meta:
         model = VirtualChassis
-        fields = ['id', 'master', 'domain', 'tags']
+        fields = ['id', 'master', 'domain', 'tags', 'member_count']
 
 
 #
 # Power panels
 #
 
-
 class PowerPanelSerializer(ValidatedModelSerializer):
     site = NestedSiteSerializer()
     rack_group = NestedRackGroupSerializer(
@@ -631,10 +643,11 @@ class PowerPanelSerializer(ValidatedModelSerializer):
         allow_null=True,
         default=None
     )
+    powerfeed_count = serializers.IntegerField(read_only=True)
 
     class Meta:
         model = PowerPanel
-        fields = ['id', 'site', 'rack_group', 'name']
+        fields = ['id', 'site', 'rack_group', 'name', 'powerfeed_count']
 
 
 class PowerFeedSerializer(TaggitSerializer, CustomFieldModelSerializer):

+ 52 - 11
netbox/dcim/api/views.py

@@ -1,7 +1,7 @@
 from collections import OrderedDict
 
 from django.conf import settings
-from django.db.models import F
+from django.db.models import Count, F, OuterRef, Subquery
 from django.http import HttpResponseForbidden
 from django.shortcuts import get_object_or_404
 from drf_yasg import openapi
@@ -26,6 +26,7 @@ from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
 from utilities.api import (
     get_serializer_for_model, IsAuthenticatedOrLoginNotRequired, FieldChoicesViewSet, ModelViewSet, ServiceUnavailable,
 )
+from virtualization.models import VirtualMachine
 from . import serializers
 from .exceptions import MissingFilterException
 
@@ -93,7 +94,9 @@ class CableTraceMixin(object):
 #
 
 class RegionViewSet(ModelViewSet):
-    queryset = Region.objects.all()
+    queryset = Region.objects.annotate(
+        site_count=Count('sites')
+    )
     serializer_class = serializers.RegionSerializer
     filterset_class = filters.RegionFilter
 
@@ -123,7 +126,9 @@ class SiteViewSet(CustomFieldModelViewSet):
 #
 
 class RackGroupViewSet(ModelViewSet):
-    queryset = RackGroup.objects.select_related('site')
+    queryset = RackGroup.objects.select_related('site').annotate(
+        rack_count=Count('racks')
+    )
     serializer_class = serializers.RackGroupSerializer
     filterset_class = filters.RackGroupFilter
 
@@ -133,7 +138,9 @@ class RackGroupViewSet(ModelViewSet):
 #
 
 class RackRoleViewSet(ModelViewSet):
-    queryset = RackRole.objects.all()
+    queryset = RackRole.objects.annotate(
+        rack_count=Count('racks')
+    )
     serializer_class = serializers.RackRoleSerializer
     filterset_class = filters.RackRoleFilter
 
@@ -143,7 +150,13 @@ class RackRoleViewSet(ModelViewSet):
 #
 
 class RackViewSet(CustomFieldModelViewSet):
-    queryset = Rack.objects.select_related('site', 'group__site', 'tenant').prefetch_related('tags')
+    queryset = Rack.objects.select_related(
+        'site', 'group__site', 'tenant'
+    ).prefetch_related(
+        'tags'
+    ).annotate(
+        device_count=Count('devices')
+    )
     serializer_class = serializers.RackSerializer
     filterset_class = filters.RackFilter
 
@@ -192,7 +205,9 @@ class RackReservationViewSet(ModelViewSet):
 #
 
 class ManufacturerViewSet(ModelViewSet):
-    queryset = Manufacturer.objects.all()
+    queryset = Manufacturer.objects.annotate(
+        devicetype_count=Count('device_types')
+    )
     serializer_class = serializers.ManufacturerSerializer
     filterset_class = filters.ManufacturerFilter
 
@@ -202,7 +217,9 @@ class ManufacturerViewSet(ModelViewSet):
 #
 
 class DeviceTypeViewSet(CustomFieldModelViewSet):
-    queryset = DeviceType.objects.select_related('manufacturer').prefetch_related('tags')
+    queryset = DeviceType.objects.select_related('manufacturer').prefetch_related('tags').annotate(
+        device_count=Count('instances')
+    )
     serializer_class = serializers.DeviceTypeSerializer
     filterset_class = filters.DeviceTypeFilter
 
@@ -264,7 +281,16 @@ class DeviceBayTemplateViewSet(ModelViewSet):
 #
 
 class DeviceRoleViewSet(ModelViewSet):
-    queryset = DeviceRole.objects.all()
+    device_count = Device.objects.filter(
+        device_role=OuterRef('pk')
+    ).order_by().values('device_role').annotate(c=Count('*')).values('c')
+    virtualmachine_count = VirtualMachine.objects.filter(
+        role=OuterRef('pk')
+    ).order_by().values('role').annotate(c=Count('*')).values('c')
+    queryset = DeviceRole.objects.annotate(
+        device_count=Subquery(device_count),
+        virtualmachine_count=Subquery(virtualmachine_count)
+    )
     serializer_class = serializers.DeviceRoleSerializer
     filterset_class = filters.DeviceRoleFilter
 
@@ -274,7 +300,16 @@ class DeviceRoleViewSet(ModelViewSet):
 #
 
 class PlatformViewSet(ModelViewSet):
-    queryset = Platform.objects.all()
+    device_count = Device.objects.filter(
+        platform=OuterRef('pk')
+    ).order_by().values('platform').annotate(c=Count('*')).values('c')
+    virtualmachine_count = VirtualMachine.objects.filter(
+        platform=OuterRef('pk')
+    ).order_by().values('platform').annotate(c=Count('*')).values('c')
+    queryset = Platform.objects.annotate(
+        device_count=Subquery(device_count),
+        virtualmachine_count=Subquery(virtualmachine_count)
+    )
     serializer_class = serializers.PlatformSerializer
     filterset_class = filters.PlatformFilter
 
@@ -535,7 +570,9 @@ class CableViewSet(ModelViewSet):
 #
 
 class VirtualChassisViewSet(ModelViewSet):
-    queryset = VirtualChassis.objects.prefetch_related('tags')
+    queryset = VirtualChassis.objects.prefetch_related('tags').annotate(
+        member_count=Count('members')
+    )
     serializer_class = serializers.VirtualChassisSerializer
 
 
@@ -544,7 +581,11 @@ class VirtualChassisViewSet(ModelViewSet):
 #
 
 class PowerPanelViewSet(ModelViewSet):
-    queryset = PowerPanel.objects.select_related('site', 'rack_group')
+    queryset = PowerPanel.objects.select_related(
+        'site', 'rack_group'
+    ).annotate(
+        powerfeed_count=Count('powerfeeds')
+    )
     serializer_class = serializers.PowerPanelSerializer
     filterset_class = filters.PowerPanelFilter
 

+ 10 - 10
netbox/dcim/tests/test_api.py

@@ -47,7 +47,7 @@ class RegionTest(APITestCase):
 
         self.assertEqual(
             sorted(response.data['results'][0]),
-            ['id', 'name', 'slug', 'url']
+            ['id', 'name', 'site_count', 'slug', 'url']
         )
 
     def test_create_region(self):
@@ -285,7 +285,7 @@ class RackGroupTest(APITestCase):
 
         self.assertEqual(
             sorted(response.data['results'][0]),
-            ['id', 'name', 'slug', 'url']
+            ['id', 'name', 'rack_count', 'slug', 'url']
         )
 
     def test_create_rackgroup(self):
@@ -393,7 +393,7 @@ class RackRoleTest(APITestCase):
 
         self.assertEqual(
             sorted(response.data['results'][0]),
-            ['id', 'name', 'slug', 'url']
+            ['id', 'name', 'rack_count', 'slug', 'url']
         )
 
     def test_create_rackrole(self):
@@ -520,7 +520,7 @@ class RackTest(APITestCase):
 
         self.assertEqual(
             sorted(response.data['results'][0]),
-            ['display_name', 'id', 'name', 'url']
+            ['device_count', 'display_name', 'id', 'name', 'url']
         )
 
     def test_create_rack(self):
@@ -746,7 +746,7 @@ class ManufacturerTest(APITestCase):
 
         self.assertEqual(
             sorted(response.data['results'][0]),
-            ['id', 'name', 'slug', 'url']
+            ['devicetype_count', 'id', 'name', 'slug', 'url']
         )
 
     def test_create_manufacturer(self):
@@ -855,7 +855,7 @@ class DeviceTypeTest(APITestCase):
 
         self.assertEqual(
             sorted(response.data['results'][0]),
-            ['display_name', 'id', 'manufacturer', 'model', 'slug', 'url']
+            ['device_count', 'display_name', 'id', 'manufacturer', 'model', 'slug', 'url']
         )
 
     def test_create_devicetype(self):
@@ -1569,7 +1569,7 @@ class DeviceRoleTest(APITestCase):
 
         self.assertEqual(
             sorted(response.data['results'][0]),
-            ['id', 'name', 'slug', 'url']
+            ['device_count', 'id', 'name', 'slug', 'url', 'virtualmachine_count']
         )
 
     def test_create_devicerole(self):
@@ -1677,7 +1677,7 @@ class PlatformTest(APITestCase):
 
         self.assertEqual(
             sorted(response.data['results'][0]),
-            ['id', 'name', 'slug', 'url']
+            ['device_count', 'id', 'name', 'slug', 'url', 'virtualmachine_count']
         )
 
     def test_create_platform(self):
@@ -3457,7 +3457,7 @@ class VirtualChassisTest(APITestCase):
 
         self.assertEqual(
             sorted(response.data['results'][0]),
-            ['id', 'master', 'url']
+            ['id', 'master', 'member_count', 'url']
         )
 
     def test_create_virtualchassis(self):
@@ -3575,7 +3575,7 @@ class PowerPanelTest(APITestCase):
 
         self.assertEqual(
             sorted(response.data['results'][0]),
-            ['id', 'name', 'url']
+            ['id', 'name', 'powerfeed_count', 'url']
         )
 
     def test_create_powerpanel(self):

+ 9 - 4
netbox/ipam/api/nested_serializers.py

@@ -21,10 +21,11 @@ __all__ = [
 
 class NestedVRFSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vrf-detail')
+    prefix_count = serializers.IntegerField(read_only=True)
 
     class Meta:
         model = VRF
-        fields = ['id', 'url', 'name', 'rd']
+        fields = ['id', 'url', 'name', 'rd', 'prefix_count']
 
 
 #
@@ -33,10 +34,11 @@ class NestedVRFSerializer(WritableNestedSerializer):
 
 class NestedRIRSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail')
+    aggregate_count = serializers.IntegerField(read_only=True)
 
     class Meta:
         model = RIR
-        fields = ['id', 'url', 'name', 'slug']
+        fields = ['id', 'url', 'name', 'slug', 'aggregate_count']
 
 
 class NestedAggregateSerializer(WritableNestedSerializer):
@@ -53,18 +55,21 @@ class NestedAggregateSerializer(WritableNestedSerializer):
 
 class NestedRoleSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:role-detail')
+    prefix_count = serializers.IntegerField(read_only=True)
+    vlan_count = serializers.IntegerField(read_only=True)
 
     class Meta:
         model = Role
-        fields = ['id', 'url', 'name', 'slug']
+        fields = ['id', 'url', 'name', 'slug', 'prefix_count', 'vlan_count']
 
 
 class NestedVLANGroupSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail')
+    vlan_count = serializers.IntegerField(read_only=True)
 
     class Meta:
         model = VLANGroup
-        fields = ['id', 'url', 'name', 'slug']
+        fields = ['id', 'url', 'name', 'slug', 'vlan_count']
 
 
 class NestedVLANSerializer(WritableNestedSerializer):

+ 9 - 4
netbox/ipam/api/serializers.py

@@ -25,12 +25,13 @@ from .nested_serializers import *
 class VRFSerializer(TaggitSerializer, CustomFieldModelSerializer):
     tenant = NestedTenantSerializer(required=False, allow_null=True)
     tags = TagListSerializerField(required=False)
+    prefix_count = serializers.IntegerField(read_only=True)
 
     class Meta:
         model = VRF
         fields = [
             'id', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'tags', 'display_name', 'custom_fields',
-            'created', 'last_updated',
+            'created', 'last_updated', 'prefix_count',
         ]
 
 
@@ -39,10 +40,11 @@ class VRFSerializer(TaggitSerializer, CustomFieldModelSerializer):
 #
 
 class RIRSerializer(ValidatedModelSerializer):
+    aggregate_count = serializers.IntegerField(read_only=True)
 
     class Meta:
         model = RIR
-        fields = ['id', 'name', 'slug', 'is_private']
+        fields = ['id', 'name', 'slug', 'is_private', 'aggregate_count']
 
 
 class AggregateSerializer(TaggitSerializer, CustomFieldModelSerializer):
@@ -63,18 +65,21 @@ class AggregateSerializer(TaggitSerializer, CustomFieldModelSerializer):
 #
 
 class RoleSerializer(ValidatedModelSerializer):
+    prefix_count = serializers.IntegerField(read_only=True)
+    vlan_count = serializers.IntegerField(read_only=True)
 
     class Meta:
         model = Role
-        fields = ['id', 'name', 'slug', 'weight']
+        fields = ['id', 'name', 'slug', 'weight', 'prefix_count', 'vlan_count']
 
 
 class VLANGroupSerializer(ValidatedModelSerializer):
     site = NestedSiteSerializer(required=False, allow_null=True)
+    vlan_count = serializers.IntegerField(read_only=True)
 
     class Meta:
         model = VLANGroup
-        fields = ['id', 'name', 'slug', 'site']
+        fields = ['id', 'name', 'slug', 'site', 'vlan_count']
         validators = []
 
     def validate(self, data):

+ 25 - 5
netbox/ipam/api/views.py

@@ -1,4 +1,5 @@
 from django.conf import settings
+from django.db.models import Count, OuterRef, Subquery
 from django.shortcuts import get_object_or_404
 from rest_framework import status
 from rest_framework.decorators import action
@@ -31,7 +32,9 @@ class IPAMFieldChoicesViewSet(FieldChoicesViewSet):
 #
 
 class VRFViewSet(CustomFieldModelViewSet):
-    queryset = VRF.objects.select_related('tenant').prefetch_related('tags')
+    queryset = VRF.objects.select_related('tenant').prefetch_related('tags').annotate(
+        prefix_count=Count('prefixes')
+    )
     serializer_class = serializers.VRFSerializer
     filterset_class = filters.VRFFilter
 
@@ -41,7 +44,9 @@ class VRFViewSet(CustomFieldModelViewSet):
 #
 
 class RIRViewSet(ModelViewSet):
-    queryset = RIR.objects.all()
+    queryset = RIR.objects.annotate(
+        aggregate_count=Count('aggregates')
+    )
     serializer_class = serializers.RIRSerializer
     filterset_class = filters.RIRFilter
 
@@ -61,7 +66,16 @@ class AggregateViewSet(CustomFieldModelViewSet):
 #
 
 class RoleViewSet(ModelViewSet):
-    queryset = Role.objects.all()
+    prefix_count = Prefix.objects.filter(
+        role=OuterRef('pk')
+    ).order_by().values('role').annotate(c=Count('*')).values('c')
+    vlan_count = VLAN.objects.filter(
+        role=OuterRef('pk')
+    ).order_by().values('role').annotate(c=Count('*')).values('c')
+    queryset = Role.objects.annotate(
+        prefix_count=Subquery(prefix_count),
+        vlan_count=Subquery(vlan_count)
+    )
     serializer_class = serializers.RoleSerializer
     filterset_class = filters.RoleFilter
 
@@ -71,7 +85,11 @@ class RoleViewSet(ModelViewSet):
 #
 
 class PrefixViewSet(CustomFieldModelViewSet):
-    queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role').prefetch_related('tags')
+    queryset = Prefix.objects.select_related(
+        'site', 'vrf__tenant', 'tenant', 'vlan', 'role'
+    ).prefetch_related(
+        'tags'
+    )
     serializer_class = serializers.PrefixSerializer
     filterset_class = filters.PrefixFilter
 
@@ -263,7 +281,9 @@ class IPAddressViewSet(CustomFieldModelViewSet):
 #
 
 class VLANGroupViewSet(ModelViewSet):
-    queryset = VLANGroup.objects.select_related('site')
+    queryset = VLANGroup.objects.select_related('site').annotate(
+        vlan_count=Count('vlans')
+    )
     serializer_class = serializers.VLANGroupSerializer
     filterset_class = filters.VLANGroupFilter
 

+ 4 - 4
netbox/ipam/tests/test_api.py

@@ -39,7 +39,7 @@ class VRFTest(APITestCase):
 
         self.assertEqual(
             sorted(response.data['results'][0]),
-            ['id', 'name', 'rd', 'url']
+            ['id', 'name', 'prefix_count', 'rd', 'url']
         )
 
     def test_create_vrf(self):
@@ -147,7 +147,7 @@ class RIRTest(APITestCase):
 
         self.assertEqual(
             sorted(response.data['results'][0]),
-            ['id', 'name', 'slug', 'url']
+            ['aggregate_count', 'id', 'name', 'slug', 'url']
         )
 
     def test_create_rir(self):
@@ -351,7 +351,7 @@ class RoleTest(APITestCase):
 
         self.assertEqual(
             sorted(response.data['results'][0]),
-            ['id', 'name', 'slug', 'url']
+            ['id', 'name', 'prefix_count', 'slug', 'url', 'vlan_count']
         )
 
     def test_create_role(self):
@@ -790,7 +790,7 @@ class VLANGroupTest(APITestCase):
 
         self.assertEqual(
             sorted(response.data['results'][0]),
-            ['id', 'name', 'slug', 'url']
+            ['id', 'name', 'slug', 'url', 'vlan_count']
         )
 
     def test_create_vlangroup(self):

+ 3 - 7
netbox/netbox/api.py

@@ -1,4 +1,5 @@
 from django.conf import settings
+from django.db.models import QuerySet
 from rest_framework import authentication, exceptions
 from rest_framework.pagination import LimitOffsetPagination
 from rest_framework.permissions import DjangoModelPermissions, SAFE_METHODS
@@ -96,13 +97,8 @@ class OptionalLimitOffsetPagination(LimitOffsetPagination):
 
     def paginate_queryset(self, queryset, request, view=None):
 
-        if hasattr(queryset, 'all'):
-            # TODO: This breaks filtering by annotated values
-            # Make a clone of the queryset with any annotations stripped (performance hack)
-            qs = queryset.all()
-            qs.query.annotations.clear()
-            self.count = qs.count()
-
+        if type(queryset) is QuerySet:
+            self.count = queryset.count()
         else:
             # We're dealing with an iterable, not a QuerySet
             self.count = len(queryset)

+ 2 - 1
netbox/secrets/api/nested_serializers.py

@@ -10,7 +10,8 @@ __all__ = [
 
 class NestedSecretRoleSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='secrets-api:secretrole-detail')
+    secret_count = serializers.IntegerField(read_only=True)
 
     class Meta:
         model = SecretRole
-        fields = ['id', 'url', 'name', 'slug']
+        fields = ['id', 'url', 'name', 'slug', 'secret_count']

+ 2 - 1
netbox/secrets/api/serializers.py

@@ -14,10 +14,11 @@ from .nested_serializers import *
 #
 
 class SecretRoleSerializer(ValidatedModelSerializer):
+    secret_count = serializers.IntegerField(read_only=True)
 
     class Meta:
         model = SecretRole
-        fields = ['id', 'name', 'slug']
+        fields = ['id', 'name', 'slug', 'secret_count']
 
 
 class SecretSerializer(TaggitSerializer, CustomFieldModelSerializer):

+ 4 - 1
netbox/secrets/api/views.py

@@ -1,6 +1,7 @@
 import base64
 
 from Crypto.PublicKey import RSA
+from django.db.models import Count
 from django.http import HttpResponseBadRequest
 from rest_framework.exceptions import ValidationError
 from rest_framework.permissions import IsAuthenticated
@@ -32,7 +33,9 @@ class SecretsFieldChoicesViewSet(FieldChoicesViewSet):
 #
 
 class SecretRoleViewSet(ModelViewSet):
-    queryset = SecretRole.objects.all()
+    queryset = SecretRole.objects.annotate(
+        secret_count=Count('secrets')
+    )
     serializer_class = serializers.SecretRoleSerializer
     permission_classes = [IsAuthenticated]
     filterset_class = filters.SecretRoleFilter

+ 1 - 1
netbox/secrets/tests/test_api.py

@@ -78,7 +78,7 @@ class SecretRoleTest(APITestCase):
 
         self.assertEqual(
             sorted(response.data['results'][0]),
-            ['id', 'name', 'slug', 'url']
+            ['id', 'name', 'secret_count', 'slug', 'url']
         )
 
     def test_create_secretrole(self):

+ 2 - 1
netbox/tenancy/api/nested_serializers.py

@@ -15,10 +15,11 @@ __all__ = [
 
 class NestedTenantGroupSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenantgroup-detail')
+    tenant_count = serializers.IntegerField(read_only=True)
 
     class Meta:
         model = TenantGroup
-        fields = ['id', 'url', 'name', 'slug']
+        fields = ['id', 'url', 'name', 'slug', 'tenant_count']
 
 
 class NestedTenantSerializer(WritableNestedSerializer):

+ 3 - 1
netbox/tenancy/api/serializers.py

@@ -1,3 +1,4 @@
+from rest_framework import serializers
 from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
 
 from extras.api.customfields import CustomFieldModelSerializer
@@ -11,10 +12,11 @@ from .nested_serializers import *
 #
 
 class TenantGroupSerializer(ValidatedModelSerializer):
+    tenant_count = serializers.IntegerField(read_only=True)
 
     class Meta:
         model = TenantGroup
-        fields = ['id', 'name', 'slug']
+        fields = ['id', 'name', 'slug', 'tenant_count']
 
 
 class TenantSerializer(TaggitSerializer, CustomFieldModelSerializer):

+ 5 - 1
netbox/tenancy/api/views.py

@@ -1,3 +1,5 @@
+from django.db.models import Count
+
 from extras.api.views import CustomFieldModelViewSet
 from tenancy import filters
 from tenancy.models import Tenant, TenantGroup
@@ -18,7 +20,9 @@ class TenancyFieldChoicesViewSet(FieldChoicesViewSet):
 #
 
 class TenantGroupViewSet(ModelViewSet):
-    queryset = TenantGroup.objects.all()
+    queryset = TenantGroup.objects.annotate(
+        tenant_count=Count('tenants')
+    )
     serializer_class = serializers.TenantGroupSerializer
     filterset_class = filters.TenantGroupFilter
 

+ 1 - 1
netbox/tenancy/tests/test_api.py

@@ -36,7 +36,7 @@ class TenantGroupTest(APITestCase):
 
         self.assertEqual(
             sorted(response.data['results'][0]),
-            ['id', 'name', 'slug', 'url']
+            ['id', 'name', 'slug', 'tenant_count', 'url']
         )
 
     def test_create_tenantgroup(self):

+ 6 - 3
netbox/virtualization/api/nested_serializers.py

@@ -19,26 +19,29 @@ __all__ = [
 
 class NestedClusterTypeSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustertype-detail')
+    cluster_count = serializers.IntegerField(read_only=True)
 
     class Meta:
         model = ClusterType
-        fields = ['id', 'url', 'name', 'slug']
+        fields = ['id', 'url', 'name', 'slug', 'cluster_count']
 
 
 class NestedClusterGroupSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustergroup-detail')
+    cluster_count = serializers.IntegerField(read_only=True)
 
     class Meta:
         model = ClusterGroup
-        fields = ['id', 'url', 'name', 'slug']
+        fields = ['id', 'url', 'name', 'slug', 'cluster_count']
 
 
 class NestedClusterSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:cluster-detail')
+    virtualmachine_count = serializers.IntegerField(read_only=True)
 
     class Meta:
         model = Cluster
-        fields = ['id', 'url', 'name']
+        fields = ['id', 'url', 'name', 'virtualmachine_count']
 
 
 #

+ 6 - 2
netbox/virtualization/api/serializers.py

@@ -20,17 +20,19 @@ from .nested_serializers import *
 #
 
 class ClusterTypeSerializer(ValidatedModelSerializer):
+    cluster_count = serializers.IntegerField(read_only=True)
 
     class Meta:
         model = ClusterType
-        fields = ['id', 'name', 'slug']
+        fields = ['id', 'name', 'slug', 'cluster_count']
 
 
 class ClusterGroupSerializer(ValidatedModelSerializer):
+    cluster_count = serializers.IntegerField(read_only=True)
 
     class Meta:
         model = ClusterGroup
-        fields = ['id', 'name', 'slug']
+        fields = ['id', 'name', 'slug', 'cluster_count']
 
 
 class ClusterSerializer(TaggitSerializer, CustomFieldModelSerializer):
@@ -38,11 +40,13 @@ class ClusterSerializer(TaggitSerializer, CustomFieldModelSerializer):
     group = NestedClusterGroupSerializer(required=False, allow_null=True)
     site = NestedSiteSerializer(required=False, allow_null=True)
     tags = TagListSerializerField(required=False)
+    virtualmachine_count = serializers.IntegerField(read_only=True)
 
     class Meta:
         model = Cluster
         fields = [
             'id', 'name', 'type', 'group', 'site', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
+            'virtualmachine_count',
         ]
 
 

+ 11 - 3
netbox/virtualization/api/views.py

@@ -1,3 +1,5 @@
+from django.db.models import Count
+
 from dcim.models import Interface
 from extras.api.views import CustomFieldModelViewSet
 from utilities.api import FieldChoicesViewSet, ModelViewSet
@@ -21,19 +23,25 @@ class VirtualizationFieldChoicesViewSet(FieldChoicesViewSet):
 #
 
 class ClusterTypeViewSet(ModelViewSet):
-    queryset = ClusterType.objects.all()
+    queryset = ClusterType.objects.annotate(
+        cluster_count=Count('clusters')
+    )
     serializer_class = serializers.ClusterTypeSerializer
     filterset_class = filters.ClusterTypeFilter
 
 
 class ClusterGroupViewSet(ModelViewSet):
-    queryset = ClusterGroup.objects.all()
+    queryset = ClusterGroup.objects.annotate(
+        cluster_count=Count('clusters')
+    )
     serializer_class = serializers.ClusterGroupSerializer
     filterset_class = filters.ClusterGroupFilter
 
 
 class ClusterViewSet(CustomFieldModelViewSet):
-    queryset = Cluster.objects.select_related('type', 'group').prefetch_related('tags')
+    queryset = Cluster.objects.select_related('type', 'group').prefetch_related('tags').annotate(
+        virtualmachine_count=Count('virtual_machines')
+    )
     serializer_class = serializers.ClusterSerializer
     filterset_class = filters.ClusterFilter
 

+ 3 - 3
netbox/virtualization/tests/test_api.py

@@ -40,7 +40,7 @@ class ClusterTypeTest(APITestCase):
 
         self.assertEqual(
             sorted(response.data['results'][0]),
-            ['id', 'name', 'slug', 'url']
+            ['cluster_count', 'id', 'name', 'slug', 'url']
         )
 
     def test_create_clustertype(self):
@@ -141,7 +141,7 @@ class ClusterGroupTest(APITestCase):
 
         self.assertEqual(
             sorted(response.data['results'][0]),
-            ['id', 'name', 'slug', 'url']
+            ['cluster_count', 'id', 'name', 'slug', 'url']
         )
 
     def test_create_clustergroup(self):
@@ -245,7 +245,7 @@ class ClusterTest(APITestCase):
 
         self.assertEqual(
             sorted(response.data['results'][0]),
-            ['id', 'name', 'url']
+            ['id', 'name', 'url', 'virtualmachine_count']
         )
 
     def test_create_cluster(self):