diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cc743f4c..bd9aa35cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,6 +63,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`. + ## Bug Fixes diff --git a/netbox/circuits/api/nested_serializers.py b/netbox/circuits/api/nested_serializers.py index 211dc4007..067b82282 100644 --- a/netbox/circuits/api/nested_serializers.py +++ b/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): diff --git a/netbox/circuits/api/serializers.py b/netbox/circuits/api/serializers.py index e94875c21..39a0b6b26 100644 --- a/netbox/circuits/api/serializers.py +++ b/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): diff --git a/netbox/circuits/api/views.py b/netbox/circuits/api/views.py index 877d85f85..ad48174e6 100644 --- a/netbox/circuits/api/views.py +++ b/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 diff --git a/netbox/circuits/tests/test_api.py b/netbox/circuits/tests/test_api.py index 0810f0ff9..e53c2c402 100644 --- a/netbox/circuits/tests/test_api.py +++ b/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): diff --git a/netbox/dcim/api/nested_serializers.py b/netbox/dcim/api/nested_serializers.py index 16d77bd69..cf22916ad 100644 --- a/netbox/dcim/api/nested_serializers.py +++ b/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): diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index ab1be0b01..46d46b0a1 100644 --- a/netbox/dcim/api/serializers.py +++ b/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): diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index f37c4d452..dc7e1f34d 100644 --- a/netbox/dcim/api/views.py +++ b/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 from django.http import HttpResponseForbidden from django.shortcuts import get_object_or_404 from drf_yasg import openapi @@ -93,7 +93,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 +125,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 +137,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 +149,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 +204,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 +216,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 +280,10 @@ class DeviceBayTemplateViewSet(ModelViewSet): # class DeviceRoleViewSet(ModelViewSet): - queryset = DeviceRole.objects.all() + queryset = DeviceRole.objects.annotate( + device_count=Count('devices', distinct=True), + virtualmachine_count=Count('virtual_machines', distinct=True) + ) serializer_class = serializers.DeviceRoleSerializer filterset_class = filters.DeviceRoleFilter @@ -274,7 +293,10 @@ class DeviceRoleViewSet(ModelViewSet): # class PlatformViewSet(ModelViewSet): - queryset = Platform.objects.all() + queryset = Platform.objects.annotate( + device_count=Count('devices', distinct=True), + virtualmachine_count=Count('virtual_machines', distinct=True) + ) serializer_class = serializers.PlatformSerializer filterset_class = filters.PlatformFilter @@ -535,7 +557,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 +568,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 diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index ea06c2ae6..9c873c886 100644 --- a/netbox/dcim/tests/test_api.py +++ b/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): diff --git a/netbox/ipam/api/nested_serializers.py b/netbox/ipam/api/nested_serializers.py index 2ffaa0ae2..aa7c95f1c 100644 --- a/netbox/ipam/api/nested_serializers.py +++ b/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): diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index 9b2c45371..8587c086b 100644 --- a/netbox/ipam/api/serializers.py +++ b/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): diff --git a/netbox/ipam/api/views.py b/netbox/ipam/api/views.py index e846f0489..a1578eb97 100644 --- a/netbox/ipam/api/views.py +++ b/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,10 @@ class AggregateViewSet(CustomFieldModelViewSet): # class RoleViewSet(ModelViewSet): - queryset = Role.objects.all() + queryset = Role.objects.annotate( + prefix_count=Count('prefixes', distinct=True), + vlan_count=Count('vlans', distinct=True) + ) serializer_class = serializers.RoleSerializer filterset_class = filters.RoleFilter @@ -71,7 +79,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 +275,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 diff --git a/netbox/ipam/tests/test_api.py b/netbox/ipam/tests/test_api.py index 3f4555b55..391ba7310 100644 --- a/netbox/ipam/tests/test_api.py +++ b/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): diff --git a/netbox/netbox/api.py b/netbox/netbox/api.py index 5e4a7a028..50d66c543 100644 --- a/netbox/netbox/api.py +++ b/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) diff --git a/netbox/secrets/api/nested_serializers.py b/netbox/secrets/api/nested_serializers.py index 819546c63..7aa8087da 100644 --- a/netbox/secrets/api/nested_serializers.py +++ b/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'] diff --git a/netbox/secrets/api/serializers.py b/netbox/secrets/api/serializers.py index 1faf85dcf..7a0447a39 100644 --- a/netbox/secrets/api/serializers.py +++ b/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): diff --git a/netbox/secrets/api/views.py b/netbox/secrets/api/views.py index 0c164de07..88537b649 100644 --- a/netbox/secrets/api/views.py +++ b/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 diff --git a/netbox/secrets/tests/test_api.py b/netbox/secrets/tests/test_api.py index c260f1a48..ce0295d83 100644 --- a/netbox/secrets/tests/test_api.py +++ b/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): diff --git a/netbox/tenancy/api/nested_serializers.py b/netbox/tenancy/api/nested_serializers.py index d26ac4675..80780dba3 100644 --- a/netbox/tenancy/api/nested_serializers.py +++ b/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): diff --git a/netbox/tenancy/api/serializers.py b/netbox/tenancy/api/serializers.py index 80f3b948d..b00c6cf3d 100644 --- a/netbox/tenancy/api/serializers.py +++ b/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): diff --git a/netbox/tenancy/api/views.py b/netbox/tenancy/api/views.py index af3e318fc..32015e316 100644 --- a/netbox/tenancy/api/views.py +++ b/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 diff --git a/netbox/tenancy/tests/test_api.py b/netbox/tenancy/tests/test_api.py index 69db73ac6..121898019 100644 --- a/netbox/tenancy/tests/test_api.py +++ b/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): diff --git a/netbox/virtualization/api/nested_serializers.py b/netbox/virtualization/api/nested_serializers.py index fb6e2b0be..47b7e6442 100644 --- a/netbox/virtualization/api/nested_serializers.py +++ b/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'] # diff --git a/netbox/virtualization/api/serializers.py b/netbox/virtualization/api/serializers.py index 23ab67741..18b8e878e 100644 --- a/netbox/virtualization/api/serializers.py +++ b/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', ] diff --git a/netbox/virtualization/api/views.py b/netbox/virtualization/api/views.py index ce7ee4934..9ce505310 100644 --- a/netbox/virtualization/api/views.py +++ b/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 diff --git a/netbox/virtualization/tests/test_api.py b/netbox/virtualization/tests/test_api.py index 49ae849a2..814e05854 100644 --- a/netbox/virtualization/tests/test_api.py +++ b/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):