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

Closes #15131: Dynamic queryset annotations for REST API endpoints (#15152)

* Introduce RelatedObjectCountField

* Introduce get_annotations_for_serializer() and enable dynamic annotations

* Add RelatedObjectCountFields to serializers; remove static annotations from querysets

* Remove annotations cleanup logic from BriefModeMixin

* Annotate type for RelatedObjectCountField

* Remove redundant field on TagSerializer

* Add missing reverse relationship for power feeds to rack

* Refactor RelatedObjectCountField to take a single relationship name
Jeremy Stretch 2 лет назад
Родитель
Сommit
7abb2b2ab5

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

@@ -1,8 +1,8 @@
-from drf_spectacular.utils import extend_schema_field, extend_schema_serializer
-from drf_spectacular.types import OpenApiTypes
+from drf_spectacular.utils import extend_schema_serializer
 from rest_framework import serializers
 from rest_framework import serializers
 
 
 from circuits.models import *
 from circuits.models import *
+from netbox.api.fields import RelatedObjectCountField
 from netbox.api.serializers import WritableNestedSerializer
 from netbox.api.serializers import WritableNestedSerializer
 
 
 __all__ = [
 __all__ = [
@@ -36,7 +36,7 @@ class NestedProviderNetworkSerializer(WritableNestedSerializer):
 )
 )
 class NestedProviderSerializer(WritableNestedSerializer):
 class NestedProviderSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
     url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
-    circuit_count = serializers.IntegerField(read_only=True)
+    circuit_count = RelatedObjectCountField('circuits')
 
 
     class Meta:
     class Meta:
         model = Provider
         model = Provider
@@ -64,7 +64,7 @@ class NestedProviderAccountSerializer(WritableNestedSerializer):
 )
 )
 class NestedCircuitTypeSerializer(WritableNestedSerializer):
 class NestedCircuitTypeSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
     url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
-    circuit_count = serializers.IntegerField(read_only=True)
+    circuit_count = RelatedObjectCountField('circuits')
 
 
     class Meta:
     class Meta:
         model = CircuitType
         model = CircuitType

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

@@ -6,7 +6,7 @@ from dcim.api.nested_serializers import NestedSiteSerializer
 from dcim.api.serializers import CabledObjectSerializer
 from dcim.api.serializers import CabledObjectSerializer
 from ipam.models import ASN
 from ipam.models import ASN
 from ipam.api.nested_serializers import NestedASNSerializer
 from ipam.api.nested_serializers import NestedASNSerializer
-from netbox.api.fields import ChoiceField, SerializedPKRelatedField
+from netbox.api.fields import ChoiceField, RelatedObjectCountField, SerializedPKRelatedField
 from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer
 from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer
 from tenancy.api.nested_serializers import NestedTenantSerializer
 from tenancy.api.nested_serializers import NestedTenantSerializer
 from .nested_serializers import *
 from .nested_serializers import *
@@ -32,7 +32,7 @@ class ProviderSerializer(NetBoxModelSerializer):
     )
     )
 
 
     # Related object counts
     # Related object counts
-    circuit_count = serializers.IntegerField(read_only=True)
+    circuit_count = RelatedObjectCountField('circuits')
 
 
     class Meta:
     class Meta:
         model = Provider
         model = Provider
@@ -80,13 +80,15 @@ class ProviderNetworkSerializer(NetBoxModelSerializer):
 
 
 class CircuitTypeSerializer(NetBoxModelSerializer):
 class CircuitTypeSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
     url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
-    circuit_count = serializers.IntegerField(read_only=True)
+
+    # Related object counts
+    circuit_count = RelatedObjectCountField('circuits')
 
 
     class Meta:
     class Meta:
         model = CircuitType
         model = CircuitType
         fields = [
         fields = [
-            'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', 'created', 'last_updated',
-            'circuit_count',
+            'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', 'created',
+            'last_updated', 'circuit_count',
         ]
         ]
 
 
 
 

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

@@ -4,7 +4,6 @@ from circuits import filtersets
 from circuits.models import *
 from circuits.models import *
 from dcim.api.views import PassThroughPortMixin
 from dcim.api.views import PassThroughPortMixin
 from netbox.api.viewsets import NetBoxModelViewSet
 from netbox.api.viewsets import NetBoxModelViewSet
-from utilities.utils import count_related
 from . import serializers
 from . import serializers
 
 
 
 
@@ -21,9 +20,7 @@ class CircuitsRootView(APIRootView):
 #
 #
 
 
 class ProviderViewSet(NetBoxModelViewSet):
 class ProviderViewSet(NetBoxModelViewSet):
-    queryset = Provider.objects.annotate(
-        circuit_count=count_related(Circuit, 'provider')
-    )
+    queryset = Provider.objects.all()
     serializer_class = serializers.ProviderSerializer
     serializer_class = serializers.ProviderSerializer
     filterset_class = filtersets.ProviderFilterSet
     filterset_class = filtersets.ProviderFilterSet
 
 
@@ -33,9 +30,7 @@ class ProviderViewSet(NetBoxModelViewSet):
 #
 #
 
 
 class CircuitTypeViewSet(NetBoxModelViewSet):
 class CircuitTypeViewSet(NetBoxModelViewSet):
-    queryset = CircuitType.objects.annotate(
-        circuit_count=count_related(Circuit, 'type')
-    )
+    queryset = CircuitType.objects.all()
     serializer_class = serializers.CircuitTypeSerializer
     serializer_class = serializers.CircuitTypeSerializer
     filterset_class = filtersets.CircuitTypeFilterSet
     filterset_class = filtersets.CircuitTypeFilterSet
 
 

+ 2 - 4
netbox/core/api/serializers.py

@@ -2,7 +2,7 @@ from rest_framework import serializers
 
 
 from core.choices import *
 from core.choices import *
 from core.models import *
 from core.models import *
-from netbox.api.fields import ChoiceField, ContentTypeField
+from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField
 from netbox.api.serializers import BaseModelSerializer, NetBoxModelSerializer
 from netbox.api.serializers import BaseModelSerializer, NetBoxModelSerializer
 from netbox.utils import get_data_backend_choices
 from netbox.utils import get_data_backend_choices
 from users.api.nested_serializers import NestedUserSerializer
 from users.api.nested_serializers import NestedUserSerializer
@@ -28,9 +28,7 @@ class DataSourceSerializer(NetBoxModelSerializer):
     )
     )
 
 
     # Related object counts
     # Related object counts
-    file_count = serializers.IntegerField(
-        read_only=True
-    )
+    file_count = RelatedObjectCountField('datafiles')
 
 
     class Meta:
     class Meta:
         model = DataSource
         model = DataSource

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

@@ -9,7 +9,6 @@ from rest_framework.viewsets import ReadOnlyModelViewSet
 from core import filtersets
 from core import filtersets
 from core.models import *
 from core.models import *
 from netbox.api.viewsets import NetBoxModelViewSet, NetBoxReadOnlyModelViewSet
 from netbox.api.viewsets import NetBoxModelViewSet, NetBoxReadOnlyModelViewSet
-from utilities.utils import count_related
 from . import serializers
 from . import serializers
 
 
 
 
@@ -22,9 +21,7 @@ class CoreRootView(APIRootView):
 
 
 
 
 class DataSourceViewSet(NetBoxModelViewSet):
 class DataSourceViewSet(NetBoxModelViewSet):
-    queryset = DataSource.objects.annotate(
-        file_count=count_related(DataFile, 'source')
-    )
+    queryset = DataSource.objects.all()
     serializer_class = serializers.DataSourceSerializer
     serializer_class = serializers.DataSourceSerializer
     filterset_class = filtersets.DataSourceFilterSet
     filterset_class = filtersets.DataSourceFilterSet
 
 

+ 12 - 12
netbox/dcim/api/nested_serializers.py

@@ -2,7 +2,8 @@ from drf_spectacular.utils import extend_schema_serializer
 from rest_framework import serializers
 from rest_framework import serializers
 
 
 from dcim import models
 from dcim import models
-from netbox.api.serializers import BaseModelSerializer, WritableNestedSerializer
+from netbox.api.fields import RelatedObjectCountField
+from netbox.api.serializers import WritableNestedSerializer
 
 
 __all__ = [
 __all__ = [
     'ComponentNestedModuleSerializer',
     'ComponentNestedModuleSerializer',
@@ -110,7 +111,7 @@ class NestedLocationSerializer(WritableNestedSerializer):
 )
 )
 class NestedRackRoleSerializer(WritableNestedSerializer):
 class NestedRackRoleSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
-    rack_count = serializers.IntegerField(read_only=True)
+    rack_count = RelatedObjectCountField('racks')
 
 
     class Meta:
     class Meta:
         model = models.RackRole
         model = models.RackRole
@@ -122,7 +123,7 @@ class NestedRackRoleSerializer(WritableNestedSerializer):
 )
 )
 class NestedRackSerializer(WritableNestedSerializer):
 class NestedRackSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail')
-    device_count = serializers.IntegerField(read_only=True)
+    device_count = RelatedObjectCountField('devices')
 
 
     class Meta:
     class Meta:
         model = models.Rack
         model = models.Rack
@@ -150,7 +151,7 @@ class NestedRackReservationSerializer(WritableNestedSerializer):
 )
 )
 class NestedManufacturerSerializer(WritableNestedSerializer):
 class NestedManufacturerSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
-    devicetype_count = serializers.IntegerField(read_only=True)
+    devicetype_count = RelatedObjectCountField('device_types')
 
 
     class Meta:
     class Meta:
         model = models.Manufacturer
         model = models.Manufacturer
@@ -163,7 +164,7 @@ class NestedManufacturerSerializer(WritableNestedSerializer):
 class NestedDeviceTypeSerializer(WritableNestedSerializer):
 class NestedDeviceTypeSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail')
     manufacturer = NestedManufacturerSerializer(read_only=True)
     manufacturer = NestedManufacturerSerializer(read_only=True)
-    device_count = serializers.IntegerField(read_only=True)
+    device_count = RelatedObjectCountField('instances')
 
 
     class Meta:
     class Meta:
         model = models.DeviceType
         model = models.DeviceType
@@ -173,7 +174,6 @@ class NestedDeviceTypeSerializer(WritableNestedSerializer):
 class NestedModuleTypeSerializer(WritableNestedSerializer):
 class NestedModuleTypeSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:moduletype-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:moduletype-detail')
     manufacturer = NestedManufacturerSerializer(read_only=True)
     manufacturer = NestedManufacturerSerializer(read_only=True)
-    # module_count = serializers.IntegerField(read_only=True)
 
 
     class Meta:
     class Meta:
         model = models.ModuleType
         model = models.ModuleType
@@ -274,8 +274,8 @@ class NestedInventoryItemTemplateSerializer(WritableNestedSerializer):
 )
 )
 class NestedDeviceRoleSerializer(WritableNestedSerializer):
 class NestedDeviceRoleSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
-    device_count = serializers.IntegerField(read_only=True)
-    virtualmachine_count = serializers.IntegerField(read_only=True)
+    device_count = RelatedObjectCountField('devices')
+    virtualmachine_count = RelatedObjectCountField('virtual_machines')
 
 
     class Meta:
     class Meta:
         model = models.DeviceRole
         model = models.DeviceRole
@@ -287,8 +287,8 @@ class NestedDeviceRoleSerializer(WritableNestedSerializer):
 )
 )
 class NestedPlatformSerializer(WritableNestedSerializer):
 class NestedPlatformSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
-    device_count = serializers.IntegerField(read_only=True)
-    virtualmachine_count = serializers.IntegerField(read_only=True)
+    device_count = RelatedObjectCountField('devices')
+    virtualmachine_count = RelatedObjectCountField('virtual_machines')
 
 
     class Meta:
     class Meta:
         model = models.Platform
         model = models.Platform
@@ -445,7 +445,7 @@ class NestedInventoryItemSerializer(WritableNestedSerializer):
 )
 )
 class NestedInventoryItemRoleSerializer(WritableNestedSerializer):
 class NestedInventoryItemRoleSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemrole-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemrole-detail')
-    inventoryitem_count = serializers.IntegerField(read_only=True)
+    inventoryitem_count = RelatedObjectCountField('inventory_items')
 
 
     class Meta:
     class Meta:
         model = models.InventoryItemRole
         model = models.InventoryItemRole
@@ -490,7 +490,7 @@ class NestedVirtualChassisSerializer(WritableNestedSerializer):
 )
 )
 class NestedPowerPanelSerializer(WritableNestedSerializer):
 class NestedPowerPanelSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerpanel-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerpanel-detail')
-    powerfeed_count = serializers.IntegerField(read_only=True)
+    powerfeed_count = RelatedObjectCountField('powerfeeds')
 
 
     class Meta:
     class Meta:
         model = models.PowerPanel
         model = models.PowerPanel

+ 37 - 21
netbox/dcim/api/serializers.py

@@ -15,7 +15,7 @@ from ipam.api.nested_serializers import (
     NestedASNSerializer, NestedIPAddressSerializer, NestedVLANSerializer, NestedVRFSerializer,
     NestedASNSerializer, NestedIPAddressSerializer, NestedVLANSerializer, NestedVRFSerializer,
 )
 )
 from ipam.models import ASN, VLAN
 from ipam.models import ASN, VLAN
-from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
+from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField, SerializedPKRelatedField
 from netbox.api.serializers import (
 from netbox.api.serializers import (
     GenericObjectSerializer, NestedGroupModelSerializer, NetBoxModelSerializer, ValidatedModelSerializer,
     GenericObjectSerializer, NestedGroupModelSerializer, NetBoxModelSerializer, ValidatedModelSerializer,
     WritableNestedSerializer,
     WritableNestedSerializer,
@@ -144,12 +144,12 @@ class SiteSerializer(NetBoxModelSerializer):
     )
     )
 
 
     # Related object counts
     # Related object counts
-    circuit_count = serializers.IntegerField(read_only=True)
-    device_count = serializers.IntegerField(read_only=True)
-    prefix_count = serializers.IntegerField(read_only=True)
-    rack_count = serializers.IntegerField(read_only=True)
-    virtualmachine_count = serializers.IntegerField(read_only=True)
-    vlan_count = serializers.IntegerField(read_only=True)
+    circuit_count = RelatedObjectCountField('circuit_terminations')
+    device_count = RelatedObjectCountField('devices')
+    prefix_count = RelatedObjectCountField('prefixes')
+    rack_count = RelatedObjectCountField('racks')
+    vlan_count = RelatedObjectCountField('vlans')
+    virtualmachine_count = RelatedObjectCountField('virtual_machines')
 
 
     class Meta:
     class Meta:
         model = Site
         model = Site
@@ -184,7 +184,9 @@ class LocationSerializer(NestedGroupModelSerializer):
 
 
 class RackRoleSerializer(NetBoxModelSerializer):
 class RackRoleSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
-    rack_count = serializers.IntegerField(read_only=True)
+
+    # Related object counts
+    rack_count = RelatedObjectCountField('racks')
 
 
     class Meta:
     class Meta:
         model = RackRole
         model = RackRole
@@ -207,8 +209,10 @@ class RackSerializer(NetBoxModelSerializer):
     width = ChoiceField(choices=RackWidthChoices, required=False)
     width = ChoiceField(choices=RackWidthChoices, required=False)
     outer_unit = ChoiceField(choices=RackDimensionUnitChoices, allow_blank=True, required=False, allow_null=True)
     outer_unit = ChoiceField(choices=RackDimensionUnitChoices, allow_blank=True, required=False, allow_null=True)
     weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False, allow_null=True)
     weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False, allow_null=True)
-    device_count = serializers.IntegerField(read_only=True)
-    powerfeed_count = serializers.IntegerField(read_only=True)
+
+    # Related object counts
+    device_count = RelatedObjectCountField('devices')
+    powerfeed_count = RelatedObjectCountField('powerfeeds')
 
 
     class Meta:
     class Meta:
         model = Rack
         model = Rack
@@ -299,9 +303,11 @@ class RackElevationDetailFilterSerializer(serializers.Serializer):
 
 
 class ManufacturerSerializer(NetBoxModelSerializer):
 class ManufacturerSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
-    devicetype_count = serializers.IntegerField(read_only=True)
-    inventoryitem_count = serializers.IntegerField(read_only=True)
-    platform_count = serializers.IntegerField(read_only=True)
+
+    # Related object counts
+    devicetype_count = RelatedObjectCountField('device_types')
+    inventoryitem_count = RelatedObjectCountField('inventory_items')
+    platform_count = RelatedObjectCountField('platforms')
 
 
     class Meta:
     class Meta:
         model = Manufacturer
         model = Manufacturer
@@ -325,7 +331,6 @@ class DeviceTypeSerializer(NetBoxModelSerializer):
     subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, allow_blank=True, required=False, allow_null=True)
     subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, allow_blank=True, required=False, allow_null=True)
     airflow = ChoiceField(choices=DeviceAirflowChoices, allow_blank=True, required=False, allow_null=True)
     airflow = ChoiceField(choices=DeviceAirflowChoices, allow_blank=True, required=False, allow_null=True)
     weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False, allow_null=True)
     weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False, allow_null=True)
-    device_count = serializers.IntegerField(read_only=True)
 
 
     # Counter fields
     # Counter fields
     console_port_template_count = serializers.IntegerField(read_only=True)
     console_port_template_count = serializers.IntegerField(read_only=True)
@@ -339,6 +344,9 @@ class DeviceTypeSerializer(NetBoxModelSerializer):
     module_bay_template_count = serializers.IntegerField(read_only=True)
     module_bay_template_count = serializers.IntegerField(read_only=True)
     inventory_item_template_count = serializers.IntegerField(read_only=True)
     inventory_item_template_count = serializers.IntegerField(read_only=True)
 
 
+    # Related object counts
+    device_count = RelatedObjectCountField('instances')
+
     class Meta:
     class Meta:
         model = DeviceType
         model = DeviceType
         fields = [
         fields = [
@@ -636,8 +644,10 @@ class InventoryItemTemplateSerializer(ValidatedModelSerializer):
 class DeviceRoleSerializer(NetBoxModelSerializer):
 class DeviceRoleSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
     config_template = NestedConfigTemplateSerializer(required=False, allow_null=True, default=None)
     config_template = NestedConfigTemplateSerializer(required=False, allow_null=True, default=None)
-    device_count = serializers.IntegerField(read_only=True)
-    virtualmachine_count = serializers.IntegerField(read_only=True)
+
+    # Related object counts
+    device_count = RelatedObjectCountField('devices')
+    virtualmachine_count = RelatedObjectCountField('virtual_machines')
 
 
     class Meta:
     class Meta:
         model = DeviceRole
         model = DeviceRole
@@ -651,8 +661,10 @@ class PlatformSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
     manufacturer = NestedManufacturerSerializer(required=False, allow_null=True)
     manufacturer = NestedManufacturerSerializer(required=False, allow_null=True)
     config_template = NestedConfigTemplateSerializer(required=False, allow_null=True, default=None)
     config_template = NestedConfigTemplateSerializer(required=False, allow_null=True, default=None)
-    device_count = serializers.IntegerField(read_only=True)
-    virtualmachine_count = serializers.IntegerField(read_only=True)
+
+    # Related object counts
+    device_count = RelatedObjectCountField('devices')
+    virtualmachine_count = RelatedObjectCountField('virtual_machines')
 
 
     class Meta:
     class Meta:
         model = Platform
         model = Platform
@@ -761,7 +773,7 @@ class VirtualDeviceContextSerializer(NetBoxModelSerializer):
     status = ChoiceField(choices=VirtualDeviceContextStatusChoices)
     status = ChoiceField(choices=VirtualDeviceContextStatusChoices)
 
 
     # Related object counts
     # Related object counts
-    interface_count = serializers.IntegerField(read_only=True)
+    interface_count = RelatedObjectCountField('interfaces')
 
 
     class Meta:
     class Meta:
         model = VirtualDeviceContext
         model = VirtualDeviceContext
@@ -1092,7 +1104,9 @@ class InventoryItemSerializer(NetBoxModelSerializer):
 
 
 class InventoryItemRoleSerializer(NetBoxModelSerializer):
 class InventoryItemRoleSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemrole-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemrole-detail')
-    inventoryitem_count = serializers.IntegerField(read_only=True)
+
+    # Related object counts
+    inventoryitem_count = RelatedObjectCountField('inventory_items')
 
 
     class Meta:
     class Meta:
         model = InventoryItemRole
         model = InventoryItemRole
@@ -1204,7 +1218,9 @@ class PowerPanelSerializer(NetBoxModelSerializer):
         allow_null=True,
         allow_null=True,
         default=None
         default=None
     )
     )
-    powerfeed_count = serializers.IntegerField(read_only=True)
+
+    # Related object counts
+    powerfeed_count = RelatedObjectCountField('powerfeeds')
 
 
     class Meta:
     class Meta:
         model = PowerPanel
         model = PowerPanel

+ 10 - 42
netbox/dcim/api/views.py

@@ -13,7 +13,6 @@ from dcim.constants import CABLE_TRACE_SVG_DEFAULT_WIDTH
 from dcim.models import *
 from dcim.models import *
 from dcim.svg import CableTraceSVG
 from dcim.svg import CableTraceSVG
 from extras.api.mixins import ConfigContextQuerySetMixin, RenderConfigMixin
 from extras.api.mixins import ConfigContextQuerySetMixin, RenderConfigMixin
-from ipam.models import Prefix, VLAN
 from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
 from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
 from netbox.api.metadata import ContentTypeMetadata
 from netbox.api.metadata import ContentTypeMetadata
 from netbox.api.pagination import StripCountAnnotationsPaginator
 from netbox.api.pagination import StripCountAnnotationsPaginator
@@ -23,7 +22,6 @@ from netbox.constants import NESTED_SERIALIZER_PREFIX
 from utilities.api import get_serializer_for_model
 from utilities.api import get_serializer_for_model
 from utilities.query_functions import CollateAsChar
 from utilities.query_functions import CollateAsChar
 from utilities.utils import count_related
 from utilities.utils import count_related
-from virtualization.models import VirtualMachine
 from . import serializers
 from . import serializers
 from .exceptions import MissingFilterException
 from .exceptions import MissingFilterException
 
 
@@ -129,14 +127,7 @@ class SiteGroupViewSet(MPTTLockedMixin, NetBoxModelViewSet):
 #
 #
 
 
 class SiteViewSet(NetBoxModelViewSet):
 class SiteViewSet(NetBoxModelViewSet):
-    queryset = Site.objects.annotate(
-        device_count=count_related(Device, 'site'),
-        rack_count=count_related(Rack, 'site'),
-        prefix_count=count_related(Prefix, 'site'),
-        vlan_count=count_related(VLAN, 'site'),
-        circuit_count=count_related(Circuit, 'terminations__site'),
-        virtualmachine_count=count_related(VirtualMachine, 'cluster__site')
-    )
+    queryset = Site.objects.all()
     serializer_class = serializers.SiteSerializer
     serializer_class = serializers.SiteSerializer
     filterset_class = filtersets.SiteFilterSet
     filterset_class = filtersets.SiteFilterSet
 
 
@@ -168,9 +159,7 @@ class LocationViewSet(MPTTLockedMixin, NetBoxModelViewSet):
 #
 #
 
 
 class RackRoleViewSet(NetBoxModelViewSet):
 class RackRoleViewSet(NetBoxModelViewSet):
-    queryset = RackRole.objects.annotate(
-        rack_count=count_related(Rack, 'role')
-    )
+    queryset = RackRole.objects.all()
     serializer_class = serializers.RackRoleSerializer
     serializer_class = serializers.RackRoleSerializer
     filterset_class = filtersets.RackRoleFilterSet
     filterset_class = filtersets.RackRoleFilterSet
 
 
@@ -180,10 +169,7 @@ class RackRoleViewSet(NetBoxModelViewSet):
 #
 #
 
 
 class RackViewSet(NetBoxModelViewSet):
 class RackViewSet(NetBoxModelViewSet):
-    queryset = Rack.objects.annotate(
-        device_count=count_related(Device, 'rack'),
-        powerfeed_count=count_related(PowerFeed, 'rack')
-    )
+    queryset = Rack.objects.all()
     serializer_class = serializers.RackSerializer
     serializer_class = serializers.RackSerializer
     filterset_class = filtersets.RackFilterSet
     filterset_class = filtersets.RackFilterSet
 
 
@@ -255,11 +241,7 @@ class RackReservationViewSet(NetBoxModelViewSet):
 #
 #
 
 
 class ManufacturerViewSet(NetBoxModelViewSet):
 class ManufacturerViewSet(NetBoxModelViewSet):
-    queryset = Manufacturer.objects.annotate(
-        devicetype_count=count_related(DeviceType, 'manufacturer'),
-        inventoryitem_count=count_related(InventoryItem, 'manufacturer'),
-        platform_count=count_related(Platform, 'manufacturer')
-    )
+    queryset = Manufacturer.objects.all()
     serializer_class = serializers.ManufacturerSerializer
     serializer_class = serializers.ManufacturerSerializer
     filterset_class = filtersets.ManufacturerFilterSet
     filterset_class = filtersets.ManufacturerFilterSet
 
 
@@ -269,9 +251,7 @@ class ManufacturerViewSet(NetBoxModelViewSet):
 #
 #
 
 
 class DeviceTypeViewSet(NetBoxModelViewSet):
 class DeviceTypeViewSet(NetBoxModelViewSet):
-    queryset = DeviceType.objects.annotate(
-        device_count=count_related(Device, 'device_type')
-    )
+    queryset = DeviceType.objects.all()
     serializer_class = serializers.DeviceTypeSerializer
     serializer_class = serializers.DeviceTypeSerializer
     filterset_class = filtersets.DeviceTypeFilterSet
     filterset_class = filtersets.DeviceTypeFilterSet
 
 
@@ -351,10 +331,7 @@ class InventoryItemTemplateViewSet(MPTTLockedMixin, NetBoxModelViewSet):
 #
 #
 
 
 class DeviceRoleViewSet(NetBoxModelViewSet):
 class DeviceRoleViewSet(NetBoxModelViewSet):
-    queryset = DeviceRole.objects.annotate(
-        device_count=count_related(Device, 'role'),
-        virtualmachine_count=count_related(VirtualMachine, 'role')
-    )
+    queryset = DeviceRole.objects.all()
     serializer_class = serializers.DeviceRoleSerializer
     serializer_class = serializers.DeviceRoleSerializer
     filterset_class = filtersets.DeviceRoleFilterSet
     filterset_class = filtersets.DeviceRoleFilterSet
 
 
@@ -364,10 +341,7 @@ class DeviceRoleViewSet(NetBoxModelViewSet):
 #
 #
 
 
 class PlatformViewSet(NetBoxModelViewSet):
 class PlatformViewSet(NetBoxModelViewSet):
-    queryset = Platform.objects.annotate(
-        device_count=count_related(Device, 'platform'),
-        virtualmachine_count=count_related(VirtualMachine, 'platform')
-    )
+    queryset = Platform.objects.all()
     serializer_class = serializers.PlatformSerializer
     serializer_class = serializers.PlatformSerializer
     filterset_class = filtersets.PlatformFilterSet
     filterset_class = filtersets.PlatformFilterSet
 
 
@@ -410,9 +384,7 @@ class DeviceViewSet(
 
 
 
 
 class VirtualDeviceContextViewSet(NetBoxModelViewSet):
 class VirtualDeviceContextViewSet(NetBoxModelViewSet):
-    queryset = VirtualDeviceContext.objects.annotate(
-        interface_count=count_related(Interface, 'vdcs'),
-    )
+    queryset = VirtualDeviceContext.objects.all()
     serializer_class = serializers.VirtualDeviceContextSerializer
     serializer_class = serializers.VirtualDeviceContextSerializer
     filterset_class = filtersets.VirtualDeviceContextFilterSet
     filterset_class = filtersets.VirtualDeviceContextFilterSet
 
 
@@ -513,9 +485,7 @@ class InventoryItemViewSet(MPTTLockedMixin, NetBoxModelViewSet):
 #
 #
 
 
 class InventoryItemRoleViewSet(NetBoxModelViewSet):
 class InventoryItemRoleViewSet(NetBoxModelViewSet):
-    queryset = InventoryItemRole.objects.annotate(
-        inventoryitem_count=count_related(InventoryItem, 'role')
-    )
+    queryset = InventoryItemRole.objects.all()
     serializer_class = serializers.InventoryItemRoleSerializer
     serializer_class = serializers.InventoryItemRoleSerializer
     filterset_class = filtersets.InventoryItemRoleFilterSet
     filterset_class = filtersets.InventoryItemRoleFilterSet
 
 
@@ -552,9 +522,7 @@ class VirtualChassisViewSet(NetBoxModelViewSet):
 #
 #
 
 
 class PowerPanelViewSet(NetBoxModelViewSet):
 class PowerPanelViewSet(NetBoxModelViewSet):
-    queryset = PowerPanel.objects.annotate(
-        powerfeed_count=count_related(PowerFeed, 'power_panel')
-    )
+    queryset = PowerPanel.objects.all()
     serializer_class = serializers.PowerPanelSerializer
     serializer_class = serializers.PowerPanelSerializer
     filterset_class = filtersets.PowerPanelFilterSet
     filterset_class = filtersets.PowerPanelFilterSet
 
 

+ 1 - 1
netbox/dcim/migrations/0002_squashed.py

@@ -233,7 +233,7 @@ class Migration(migrations.Migration):
         migrations.AddField(
         migrations.AddField(
             model_name='powerfeed',
             model_name='powerfeed',
             name='rack',
             name='rack',
-            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='dcim.rack'),
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='powerfeeds', to='dcim.rack'),
         ),
         ),
         migrations.AddField(
         migrations.AddField(
             model_name='powerfeed',
             model_name='powerfeed',

+ 1 - 0
netbox/dcim/models/power.py

@@ -84,6 +84,7 @@ class PowerFeed(PrimaryModel, PathEndpoint, CabledObjectModel):
     rack = models.ForeignKey(
     rack = models.ForeignKey(
         to='Rack',
         to='Rack',
         on_delete=models.PROTECT,
         on_delete=models.PROTECT,
+        related_name='powerfeeds',
         blank=True,
         blank=True,
         null=True
         null=True
     )
     )

+ 4 - 3
netbox/extras/api/serializers.py

@@ -3,7 +3,6 @@ from django.core.exceptions import ObjectDoesNotExist
 from drf_spectacular.types import OpenApiTypes
 from drf_spectacular.types import OpenApiTypes
 from drf_spectacular.utils import extend_schema_field
 from drf_spectacular.utils import extend_schema_field
 from rest_framework import serializers
 from rest_framework import serializers
-from rest_framework.fields import ListField
 
 
 from core.api.nested_serializers import NestedDataSourceSerializer, NestedDataFileSerializer, NestedJobSerializer
 from core.api.nested_serializers import NestedDataSourceSerializer, NestedDataFileSerializer, NestedJobSerializer
 from core.api.serializers import JobSerializer
 from core.api.serializers import JobSerializer
@@ -16,7 +15,7 @@ from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site
 from extras.choices import *
 from extras.choices import *
 from extras.models import *
 from extras.models import *
 from netbox.api.exceptions import SerializerNotFound
 from netbox.api.exceptions import SerializerNotFound
-from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
+from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField, SerializedPKRelatedField
 from netbox.api.serializers import BaseModelSerializer, NetBoxModelSerializer, ValidatedModelSerializer
 from netbox.api.serializers import BaseModelSerializer, NetBoxModelSerializer, ValidatedModelSerializer
 from netbox.api.serializers.features import TaggableModelSerializer
 from netbox.api.serializers.features import TaggableModelSerializer
 from netbox.constants import NESTED_SERIALIZER_PREFIX
 from netbox.constants import NESTED_SERIALIZER_PREFIX
@@ -288,7 +287,9 @@ class TagSerializer(ValidatedModelSerializer):
         many=True,
         many=True,
         required=False
         required=False
     )
     )
-    tagged_items = serializers.IntegerField(read_only=True)
+
+    # Related object counts
+    tagged_items = RelatedObjectCountField('extras_taggeditem_items')
 
 
     class Meta:
     class Meta:
         model = Tag
         model = Tag

+ 2 - 4
netbox/extras/api/views.py

@@ -23,7 +23,7 @@ from netbox.api.metadata import ContentTypeMetadata
 from netbox.api.renderers import TextRenderer
 from netbox.api.renderers import TextRenderer
 from netbox.api.viewsets import NetBoxModelViewSet
 from netbox.api.viewsets import NetBoxModelViewSet
 from utilities.exceptions import RQWorkerNotRunningException
 from utilities.exceptions import RQWorkerNotRunningException
-from utilities.utils import copy_safe_request, count_related
+from utilities.utils import copy_safe_request
 from . import serializers
 from . import serializers
 from .mixins import ConfigTemplateRenderMixin
 from .mixins import ConfigTemplateRenderMixin
 
 
@@ -147,9 +147,7 @@ class BookmarkViewSet(NetBoxModelViewSet):
 #
 #
 
 
 class TagViewSet(NetBoxModelViewSet):
 class TagViewSet(NetBoxModelViewSet):
-    queryset = Tag.objects.annotate(
-        tagged_items=count_related(TaggedItem, 'tag')
-    )
+    queryset = Tag.objects.all()
     serializer_class = serializers.TagSerializer
     serializer_class = serializers.TagSerializer
     filterset_class = filtersets.TagFilterSet
     filterset_class = filtersets.TagFilterSet
 
 

+ 6 - 5
netbox/ipam/api/nested_serializers.py

@@ -2,6 +2,7 @@ from drf_spectacular.utils import extend_schema_serializer
 from rest_framework import serializers
 from rest_framework import serializers
 
 
 from ipam import models
 from ipam import models
+from netbox.api.fields import RelatedObjectCountField
 from netbox.api.serializers import WritableNestedSerializer
 from netbox.api.serializers import WritableNestedSerializer
 from .field_serializers import IPAddressField
 from .field_serializers import IPAddressField
 
 
@@ -58,7 +59,7 @@ class NestedASNSerializer(WritableNestedSerializer):
 )
 )
 class NestedVRFSerializer(WritableNestedSerializer):
 class NestedVRFSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vrf-detail')
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vrf-detail')
-    prefix_count = serializers.IntegerField(read_only=True)
+    prefix_count = RelatedObjectCountField('prefixes')
 
 
     class Meta:
     class Meta:
         model = models.VRF
         model = models.VRF
@@ -86,7 +87,7 @@ class NestedRouteTargetSerializer(WritableNestedSerializer):
 )
 )
 class NestedRIRSerializer(WritableNestedSerializer):
 class NestedRIRSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail')
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail')
-    aggregate_count = serializers.IntegerField(read_only=True)
+    aggregate_count = RelatedObjectCountField('aggregates')
 
 
     class Meta:
     class Meta:
         model = models.RIR
         model = models.RIR
@@ -132,8 +133,8 @@ class NestedFHRPGroupAssignmentSerializer(WritableNestedSerializer):
 )
 )
 class NestedRoleSerializer(WritableNestedSerializer):
 class NestedRoleSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:role-detail')
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:role-detail')
-    prefix_count = serializers.IntegerField(read_only=True)
-    vlan_count = serializers.IntegerField(read_only=True)
+    prefix_count = RelatedObjectCountField('prefixes')
+    vlan_count = RelatedObjectCountField('vlans')
 
 
     class Meta:
     class Meta:
         model = models.Role
         model = models.Role
@@ -145,7 +146,7 @@ class NestedRoleSerializer(WritableNestedSerializer):
 )
 )
 class NestedVLANGroupSerializer(WritableNestedSerializer):
 class NestedVLANGroupSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail')
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail')
-    vlan_count = serializers.IntegerField(read_only=True)
+    vlan_count = RelatedObjectCountField('vlans')
 
 
     class Meta:
     class Meta:
         model = models.VLANGroup
         model = models.VLANGroup

+ 22 - 10
netbox/ipam/api/serializers.py

@@ -6,7 +6,7 @@ from dcim.api.nested_serializers import NestedDeviceSerializer, NestedSiteSerial
 from ipam.choices import *
 from ipam.choices import *
 from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS, VLANGROUP_SCOPE_TYPES
 from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS, VLANGROUP_SCOPE_TYPES
 from ipam.models import *
 from ipam.models import *
-from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
+from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField, SerializedPKRelatedField
 from netbox.api.serializers import NetBoxModelSerializer
 from netbox.api.serializers import NetBoxModelSerializer
 from netbox.constants import NESTED_SERIALIZER_PREFIX
 from netbox.constants import NESTED_SERIALIZER_PREFIX
 from tenancy.api.nested_serializers import NestedTenantSerializer
 from tenancy.api.nested_serializers import NestedTenantSerializer
@@ -43,8 +43,10 @@ class ASNSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:asn-detail')
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:asn-detail')
     rir = NestedRIRSerializer(required=False, allow_null=True)
     rir = NestedRIRSerializer(required=False, allow_null=True)
     tenant = NestedTenantSerializer(required=False, allow_null=True)
     tenant = NestedTenantSerializer(required=False, allow_null=True)
-    site_count = serializers.IntegerField(read_only=True)
-    provider_count = serializers.IntegerField(read_only=True)
+
+    # Related object counts
+    site_count = RelatedObjectCountField('sites')
+    provider_count = RelatedObjectCountField('providers')
 
 
     class Meta:
     class Meta:
         model = ASN
         model = ASN
@@ -90,8 +92,10 @@ class VRFSerializer(NetBoxModelSerializer):
         required=False,
         required=False,
         many=True
         many=True
     )
     )
-    ipaddress_count = serializers.IntegerField(read_only=True)
-    prefix_count = serializers.IntegerField(read_only=True)
+
+    # Related object counts
+    ipaddress_count = RelatedObjectCountField('ip_addresses')
+    prefix_count = RelatedObjectCountField('prefixes')
 
 
     class Meta:
     class Meta:
         model = VRF
         model = VRF
@@ -124,7 +128,9 @@ class RouteTargetSerializer(NetBoxModelSerializer):
 
 
 class RIRSerializer(NetBoxModelSerializer):
 class RIRSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail')
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail')
-    aggregate_count = serializers.IntegerField(read_only=True)
+
+    # Related object counts
+    aggregate_count = RelatedObjectCountField('aggregates')
 
 
     class Meta:
     class Meta:
         model = RIR
         model = RIR
@@ -195,8 +201,10 @@ class FHRPGroupAssignmentSerializer(NetBoxModelSerializer):
 
 
 class RoleSerializer(NetBoxModelSerializer):
 class RoleSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:role-detail')
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:role-detail')
-    prefix_count = serializers.IntegerField(read_only=True)
-    vlan_count = serializers.IntegerField(read_only=True)
+
+    # Related object counts
+    prefix_count = RelatedObjectCountField('prefixes')
+    vlan_count = RelatedObjectCountField('vlans')
 
 
     class Meta:
     class Meta:
         model = Role
         model = Role
@@ -218,9 +226,11 @@ class VLANGroupSerializer(NetBoxModelSerializer):
     )
     )
     scope_id = serializers.IntegerField(allow_null=True, required=False, default=None)
     scope_id = serializers.IntegerField(allow_null=True, required=False, default=None)
     scope = serializers.SerializerMethodField(read_only=True)
     scope = serializers.SerializerMethodField(read_only=True)
-    vlan_count = serializers.IntegerField(read_only=True)
     utilization = serializers.CharField(read_only=True)
     utilization = serializers.CharField(read_only=True)
 
 
+    # Related object counts
+    vlan_count = RelatedObjectCountField('vlans')
+
     class Meta:
     class Meta:
         model = VLANGroup
         model = VLANGroup
         fields = [
         fields = [
@@ -247,7 +257,9 @@ class VLANSerializer(NetBoxModelSerializer):
     status = ChoiceField(choices=VLANStatusChoices, required=False)
     status = ChoiceField(choices=VLANStatusChoices, required=False)
     role = NestedRoleSerializer(required=False, allow_null=True)
     role = NestedRoleSerializer(required=False, allow_null=True)
     l2vpn_termination = NestedL2VPNTerminationSerializer(read_only=True, allow_null=True)
     l2vpn_termination = NestedL2VPNTerminationSerializer(read_only=True, allow_null=True)
-    prefix_count = serializers.IntegerField(read_only=True)
+
+    # Related object counts
+    prefix_count = RelatedObjectCountField('prefixes')
 
 
     class Meta:
     class Meta:
         model = VLAN
         model = VLAN

+ 4 - 20
netbox/ipam/api/views.py

@@ -12,8 +12,6 @@ from rest_framework.response import Response
 from rest_framework.routers import APIRootView
 from rest_framework.routers import APIRootView
 from rest_framework.views import APIView
 from rest_framework.views import APIView
 
 
-from circuits.models import Provider
-from dcim.models import Site
 from ipam import filtersets
 from ipam import filtersets
 from ipam.models import *
 from ipam.models import *
 from ipam.utils import get_next_available_prefix
 from ipam.utils import get_next_available_prefix
@@ -22,7 +20,6 @@ from netbox.api.viewsets.mixins import ObjectValidationMixin
 from netbox.config import get_config
 from netbox.config import get_config
 from netbox.constants import ADVISORY_LOCK_KEYS
 from netbox.constants import ADVISORY_LOCK_KEYS
 from utilities.api import get_serializer_for_model
 from utilities.api import get_serializer_for_model
-from utilities.utils import count_related
 from . import serializers
 from . import serializers
 
 
 
 
@@ -45,19 +42,13 @@ class ASNRangeViewSet(NetBoxModelViewSet):
 
 
 
 
 class ASNViewSet(NetBoxModelViewSet):
 class ASNViewSet(NetBoxModelViewSet):
-    queryset = ASN.objects.annotate(
-        site_count=count_related(Site, 'asns'),
-        provider_count=count_related(Provider, 'asns')
-    )
+    queryset = ASN.objects.all()
     serializer_class = serializers.ASNSerializer
     serializer_class = serializers.ASNSerializer
     filterset_class = filtersets.ASNFilterSet
     filterset_class = filtersets.ASNFilterSet
 
 
 
 
 class VRFViewSet(NetBoxModelViewSet):
 class VRFViewSet(NetBoxModelViewSet):
-    queryset = VRF.objects.annotate(
-        ipaddress_count=count_related(IPAddress, 'vrf'),
-        prefix_count=count_related(Prefix, 'vrf')
-    )
+    queryset = VRF.objects.all()
     serializer_class = serializers.VRFSerializer
     serializer_class = serializers.VRFSerializer
     filterset_class = filtersets.VRFFilterSet
     filterset_class = filtersets.VRFFilterSet
 
 
@@ -69,9 +60,7 @@ class RouteTargetViewSet(NetBoxModelViewSet):
 
 
 
 
 class RIRViewSet(NetBoxModelViewSet):
 class RIRViewSet(NetBoxModelViewSet):
-    queryset = RIR.objects.annotate(
-        aggregate_count=count_related(Aggregate, 'rir')
-    )
+    queryset = RIR.objects.all()
     serializer_class = serializers.RIRSerializer
     serializer_class = serializers.RIRSerializer
     filterset_class = filtersets.RIRFilterSet
     filterset_class = filtersets.RIRFilterSet
 
 
@@ -83,10 +72,7 @@ class AggregateViewSet(NetBoxModelViewSet):
 
 
 
 
 class RoleViewSet(NetBoxModelViewSet):
 class RoleViewSet(NetBoxModelViewSet):
-    queryset = Role.objects.annotate(
-        prefix_count=count_related(Prefix, 'role'),
-        vlan_count=count_related(VLAN, 'role')
-    )
+    queryset = Role.objects.all()
     serializer_class = serializers.RoleSerializer
     serializer_class = serializers.RoleSerializer
     filterset_class = filtersets.RoleFilterSet
     filterset_class = filtersets.RoleFilterSet
 
 
@@ -151,8 +137,6 @@ class VLANGroupViewSet(NetBoxModelViewSet):
 class VLANViewSet(NetBoxModelViewSet):
 class VLANViewSet(NetBoxModelViewSet):
     queryset = VLAN.objects.prefetch_related(
     queryset = VLAN.objects.prefetch_related(
         'l2vpn_terminations',  # Referenced by VLANSerializer.l2vpn_termination
         'l2vpn_terminations',  # Referenced by VLANSerializer.l2vpn_termination
-    ).annotate(
-        prefix_count=count_related(Prefix, 'vlan')
     )
     )
     serializer_class = serializers.VLANSerializer
     serializer_class = serializers.VLANSerializer
     filterset_class = filtersets.VLANFilterSet
     filterset_class = filtersets.VLANFilterSet

+ 15 - 1
netbox/netbox/api/fields.py

@@ -1,6 +1,6 @@
 from django.core.exceptions import ObjectDoesNotExist
 from django.core.exceptions import ObjectDoesNotExist
-from drf_spectacular.utils import extend_schema_field
 from drf_spectacular.types import OpenApiTypes
 from drf_spectacular.types import OpenApiTypes
+from drf_spectacular.utils import extend_schema_field
 from netaddr import IPNetwork
 from netaddr import IPNetwork
 from rest_framework import serializers
 from rest_framework import serializers
 from rest_framework.exceptions import ValidationError
 from rest_framework.exceptions import ValidationError
@@ -10,6 +10,7 @@ __all__ = (
     'ChoiceField',
     'ChoiceField',
     'ContentTypeField',
     'ContentTypeField',
     'IPNetworkSerializer',
     'IPNetworkSerializer',
+    'RelatedObjectCountField',
     'SerializedPKRelatedField',
     'SerializedPKRelatedField',
 )
 )
 
 
@@ -135,3 +136,16 @@ class SerializedPKRelatedField(PrimaryKeyRelatedField):
 
 
     def to_representation(self, value):
     def to_representation(self, value):
         return self.serializer(value, context={'request': self.context['request']}).data
         return self.serializer(value, context={'request': self.context['request']}).data
+
+
+@extend_schema_field(OpenApiTypes.INT64)
+class RelatedObjectCountField(serializers.ReadOnlyField):
+    """
+    Represents a read-only integer count of related objects (e.g. the number of racks assigned to a site). This field
+    is detected by get_annotations_for_serializer() when determining the annotations to be added to a queryset
+    depending on the serializer fields selected for inclusion in the response.
+    """
+    def __init__(self, relation, **kwargs):
+        self.relation = relation
+
+        super().__init__(**kwargs)

+ 7 - 6
netbox/netbox/api/viewsets/__init__.py

@@ -10,7 +10,7 @@ from rest_framework import mixins as drf_mixins
 from rest_framework.response import Response
 from rest_framework.response import Response
 from rest_framework.viewsets import GenericViewSet
 from rest_framework.viewsets import GenericViewSet
 
 
-from utilities.api import get_prefetches_for_serializer
+from utilities.api import get_annotations_for_serializer, get_prefetches_for_serializer
 from utilities.exceptions import AbortRequest
 from utilities.exceptions import AbortRequest
 from . import mixins
 from . import mixins
 
 
@@ -44,15 +44,16 @@ class BaseViewSet(GenericViewSet):
 
 
     def get_queryset(self):
     def get_queryset(self):
         qs = super().get_queryset()
         qs = super().get_queryset()
+        serializer_class = self.get_serializer_class()
 
 
         # Dynamically resolve prefetches for included serializer fields and attach them to the queryset
         # Dynamically resolve prefetches for included serializer fields and attach them to the queryset
-        prefetch = get_prefetches_for_serializer(
-            self.get_serializer_class(),
-            fields_to_include=self.requested_fields
-        )
-        if prefetch:
+        if prefetch := get_prefetches_for_serializer(serializer_class, fields_to_include=self.requested_fields):
             qs = qs.prefetch_related(*prefetch)
             qs = qs.prefetch_related(*prefetch)
 
 
+        # Dynamically resolve annotations for RelatedObjectCountFields on the serializer and attach them to the queryset
+        if annotations := get_annotations_for_serializer(serializer_class, fields_to_include=self.requested_fields):
+            qs = qs.annotate(**annotations)
+
         return qs
         return qs
 
 
     def get_serializer(self, *args, **kwargs):
     def get_serializer(self, *args, **kwargs):

+ 0 - 13
netbox/netbox/api/viewsets/mixins.py

@@ -52,19 +52,6 @@ class BriefModeMixin:
 
 
         return self.serializer_class
         return self.serializer_class
 
 
-    def get_queryset(self):
-        qs = super().get_queryset()
-
-        if self.brief:
-            serializer_class = self.get_serializer_class()
-
-            # Clear any annotations for fields not present on the nested serializer
-            for annotation in list(qs.query.annotations.keys()):
-                if annotation not in serializer_class().fields:
-                    qs.query.annotations.pop(annotation)
-
-        return qs
-
 
 
 class CustomFieldsMixin:
 class CustomFieldsMixin:
     """
     """

+ 13 - 11
netbox/tenancy/api/serializers.py

@@ -3,7 +3,7 @@ from drf_spectacular.types import OpenApiTypes
 from drf_spectacular.utils import extend_schema_field
 from drf_spectacular.utils import extend_schema_field
 from rest_framework import serializers
 from rest_framework import serializers
 
 
-from netbox.api.fields import ChoiceField, ContentTypeField
+from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField
 from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer
 from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer
 from netbox.constants import NESTED_SERIALIZER_PREFIX
 from netbox.constants import NESTED_SERIALIZER_PREFIX
 from tenancy.choices import ContactPriorityChoices
 from tenancy.choices import ContactPriorityChoices
@@ -32,16 +32,18 @@ class TenantGroupSerializer(NestedGroupModelSerializer):
 class TenantSerializer(NetBoxModelSerializer):
 class TenantSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenant-detail')
     url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenant-detail')
     group = NestedTenantGroupSerializer(required=False, allow_null=True)
     group = NestedTenantGroupSerializer(required=False, allow_null=True)
-    circuit_count = serializers.IntegerField(read_only=True)
-    device_count = serializers.IntegerField(read_only=True)
-    ipaddress_count = serializers.IntegerField(read_only=True)
-    prefix_count = serializers.IntegerField(read_only=True)
-    rack_count = serializers.IntegerField(read_only=True)
-    site_count = serializers.IntegerField(read_only=True)
-    virtualmachine_count = serializers.IntegerField(read_only=True)
-    vlan_count = serializers.IntegerField(read_only=True)
-    vrf_count = serializers.IntegerField(read_only=True)
-    cluster_count = serializers.IntegerField(read_only=True)
+
+    # Related object counts
+    circuit_count = RelatedObjectCountField('circuits')
+    device_count = RelatedObjectCountField('devices')
+    rack_count = RelatedObjectCountField('racks')
+    site_count = RelatedObjectCountField('sites')
+    ipaddress_count = RelatedObjectCountField('ip_addresses')
+    prefix_count = RelatedObjectCountField('prefixes')
+    vlan_count = RelatedObjectCountField('vlans')
+    vrf_count = RelatedObjectCountField('vrfs')
+    virtualmachine_count = RelatedObjectCountField('virtual_machines')
+    cluster_count = RelatedObjectCountField('clusters')
 
 
     class Meta:
     class Meta:
         model = Tenant
         model = Tenant

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

@@ -1,13 +1,8 @@
 from rest_framework.routers import APIRootView
 from rest_framework.routers import APIRootView
 
 
-from circuits.models import Circuit
-from dcim.models import Device, Rack, Site
-from ipam.models import IPAddress, Prefix, VLAN, VRF
 from netbox.api.viewsets import NetBoxModelViewSet, MPTTLockedMixin
 from netbox.api.viewsets import NetBoxModelViewSet, MPTTLockedMixin
 from tenancy import filtersets
 from tenancy import filtersets
 from tenancy.models import *
 from tenancy.models import *
-from utilities.utils import count_related
-from virtualization.models import VirtualMachine, Cluster
 from . import serializers
 from . import serializers
 
 
 
 
@@ -36,18 +31,7 @@ class TenantGroupViewSet(MPTTLockedMixin, NetBoxModelViewSet):
 
 
 
 
 class TenantViewSet(NetBoxModelViewSet):
 class TenantViewSet(NetBoxModelViewSet):
-    queryset = Tenant.objects.annotate(
-        circuit_count=count_related(Circuit, 'tenant'),
-        device_count=count_related(Device, 'tenant'),
-        ipaddress_count=count_related(IPAddress, 'tenant'),
-        prefix_count=count_related(Prefix, 'tenant'),
-        rack_count=count_related(Rack, 'tenant'),
-        site_count=count_related(Site, 'tenant'),
-        virtualmachine_count=count_related(VirtualMachine, 'tenant'),
-        vlan_count=count_related(VLAN, 'tenant'),
-        vrf_count=count_related(VRF, 'tenant'),
-        cluster_count=count_related(Cluster, 'tenant')
-    )
+    queryset = Tenant.objects.all()
     serializer_class = serializers.TenantSerializer
     serializer_class = serializers.TenantSerializer
     filterset_class = filtersets.TenantFilterSet
     filterset_class = filtersets.TenantFilterSet
 
 

+ 23 - 0
netbox/utilities/api.py

@@ -11,10 +11,13 @@ from rest_framework import status
 from rest_framework.serializers import Serializer
 from rest_framework.serializers import Serializer
 from rest_framework.utils import formatting
 from rest_framework.utils import formatting
 
 
+from netbox.api.fields import RelatedObjectCountField
 from netbox.api.exceptions import GraphQLTypeNotFound, SerializerNotFound
 from netbox.api.exceptions import GraphQLTypeNotFound, SerializerNotFound
+from utilities.utils import count_related
 from .utils import dynamic_import
 from .utils import dynamic_import
 
 
 __all__ = (
 __all__ = (
+    'get_annotations_for_serializer',
     'get_graphql_type_for_model',
     'get_graphql_type_for_model',
     'get_prefetches_for_serializer',
     'get_prefetches_for_serializer',
     'get_serializer_for_model',
     'get_serializer_for_model',
@@ -131,6 +134,26 @@ def get_prefetches_for_serializer(serializer_class, fields_to_include=None):
     return prefetch_fields
     return prefetch_fields
 
 
 
 
+def get_annotations_for_serializer(serializer_class, fields_to_include=None):
+    """
+    Return a mapping of field names to annotations to be applied to the queryset for a serializer.
+    """
+    annotations = {}
+
+    # If specific fields are not specified, default to all
+    if not fields_to_include:
+        fields_to_include = serializer_class.Meta.fields
+
+    model = serializer_class.Meta.model
+
+    for field_name, field in serializer_class._declared_fields.items():
+        if field_name in fields_to_include and type(field) is RelatedObjectCountField:
+            related_field = model._meta.get_field(field.relation).field
+            annotations[field_name] = count_related(related_field.model, related_field.name)
+
+    return annotations
+
+
 def rest_api_server_error(request, *args, **kwargs):
 def rest_api_server_error(request, *args, **kwargs):
     """
     """
     Handle exceptions and return a useful error message for REST API requests.
     Handle exceptions and return a useful error message for REST API requests.

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

@@ -1,6 +1,7 @@
 from drf_spectacular.utils import extend_schema_serializer
 from drf_spectacular.utils import extend_schema_serializer
 from rest_framework import serializers
 from rest_framework import serializers
 
 
+from netbox.api.fields import RelatedObjectCountField
 from netbox.api.serializers import WritableNestedSerializer
 from netbox.api.serializers import WritableNestedSerializer
 from virtualization.models import *
 from virtualization.models import *
 
 
@@ -23,7 +24,7 @@ __all__ = [
 )
 )
 class NestedClusterTypeSerializer(WritableNestedSerializer):
 class NestedClusterTypeSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustertype-detail')
     url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustertype-detail')
-    cluster_count = serializers.IntegerField(read_only=True)
+    cluster_count = RelatedObjectCountField('clusters')
 
 
     class Meta:
     class Meta:
         model = ClusterType
         model = ClusterType
@@ -35,7 +36,7 @@ class NestedClusterTypeSerializer(WritableNestedSerializer):
 )
 )
 class NestedClusterGroupSerializer(WritableNestedSerializer):
 class NestedClusterGroupSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustergroup-detail')
     url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustergroup-detail')
-    cluster_count = serializers.IntegerField(read_only=True)
+    cluster_count = RelatedObjectCountField('clusters')
 
 
     class Meta:
     class Meta:
         model = ClusterGroup
         model = ClusterGroup
@@ -47,7 +48,7 @@ class NestedClusterGroupSerializer(WritableNestedSerializer):
 )
 )
 class NestedClusterSerializer(WritableNestedSerializer):
 class NestedClusterSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:cluster-detail')
     url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:cluster-detail')
-    virtualmachine_count = serializers.IntegerField(read_only=True)
+    virtualmachine_count = RelatedObjectCountField('virtual_machines')
 
 
     class Meta:
     class Meta:
         model = Cluster
         model = Cluster

+ 11 - 5
netbox/virtualization/api/serializers.py

@@ -8,7 +8,7 @@ from dcim.choices import InterfaceModeChoices
 from extras.api.nested_serializers import NestedConfigTemplateSerializer
 from extras.api.nested_serializers import NestedConfigTemplateSerializer
 from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer, NestedVRFSerializer
 from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer, NestedVRFSerializer
 from ipam.models import VLAN
 from ipam.models import VLAN
-from netbox.api.fields import ChoiceField, SerializedPKRelatedField
+from netbox.api.fields import ChoiceField, RelatedObjectCountField, SerializedPKRelatedField
 from netbox.api.serializers import NetBoxModelSerializer
 from netbox.api.serializers import NetBoxModelSerializer
 from tenancy.api.nested_serializers import NestedTenantSerializer
 from tenancy.api.nested_serializers import NestedTenantSerializer
 from virtualization.choices import *
 from virtualization.choices import *
@@ -23,7 +23,9 @@ from .nested_serializers import *
 
 
 class ClusterTypeSerializer(NetBoxModelSerializer):
 class ClusterTypeSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustertype-detail')
     url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustertype-detail')
-    cluster_count = serializers.IntegerField(read_only=True)
+
+    # Related object counts
+    cluster_count = RelatedObjectCountField('clusters')
 
 
     class Meta:
     class Meta:
         model = ClusterType
         model = ClusterType
@@ -35,7 +37,9 @@ class ClusterTypeSerializer(NetBoxModelSerializer):
 
 
 class ClusterGroupSerializer(NetBoxModelSerializer):
 class ClusterGroupSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustergroup-detail')
     url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustergroup-detail')
-    cluster_count = serializers.IntegerField(read_only=True)
+
+    # Related object counts
+    cluster_count = RelatedObjectCountField('clusters')
 
 
     class Meta:
     class Meta:
         model = ClusterGroup
         model = ClusterGroup
@@ -52,8 +56,10 @@ class ClusterSerializer(NetBoxModelSerializer):
     status = ChoiceField(choices=ClusterStatusChoices, required=False)
     status = ChoiceField(choices=ClusterStatusChoices, required=False)
     tenant = NestedTenantSerializer(required=False, allow_null=True)
     tenant = NestedTenantSerializer(required=False, allow_null=True)
     site = NestedSiteSerializer(required=False, allow_null=True, default=None)
     site = NestedSiteSerializer(required=False, allow_null=True, default=None)
-    device_count = serializers.IntegerField(read_only=True)
-    virtualmachine_count = serializers.IntegerField(read_only=True)
+
+    # Related object counts
+    device_count = RelatedObjectCountField('devices')
+    virtualmachine_count = RelatedObjectCountField('virtual_machines')
 
 
     class Meta:
     class Meta:
         model = Cluster
         model = Cluster

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

@@ -1,10 +1,8 @@
 from rest_framework.routers import APIRootView
 from rest_framework.routers import APIRootView
 
 
-from dcim.models import Device
 from extras.api.mixins import ConfigContextQuerySetMixin, RenderConfigMixin
 from extras.api.mixins import ConfigContextQuerySetMixin, RenderConfigMixin
 from netbox.api.viewsets import NetBoxModelViewSet
 from netbox.api.viewsets import NetBoxModelViewSet
 from utilities.query_functions import CollateAsChar
 from utilities.query_functions import CollateAsChar
-from utilities.utils import count_related
 from virtualization import filtersets
 from virtualization import filtersets
 from virtualization.models import *
 from virtualization.models import *
 from . import serializers
 from . import serializers
@@ -23,26 +21,19 @@ class VirtualizationRootView(APIRootView):
 #
 #
 
 
 class ClusterTypeViewSet(NetBoxModelViewSet):
 class ClusterTypeViewSet(NetBoxModelViewSet):
-    queryset = ClusterType.objects.annotate(
-        cluster_count=count_related(Cluster, 'type')
-    )
+    queryset = ClusterType.objects.all()
     serializer_class = serializers.ClusterTypeSerializer
     serializer_class = serializers.ClusterTypeSerializer
     filterset_class = filtersets.ClusterTypeFilterSet
     filterset_class = filtersets.ClusterTypeFilterSet
 
 
 
 
 class ClusterGroupViewSet(NetBoxModelViewSet):
 class ClusterGroupViewSet(NetBoxModelViewSet):
-    queryset = ClusterGroup.objects.annotate(
-        cluster_count=count_related(Cluster, 'group')
-    )
+    queryset = ClusterGroup.objects.all()
     serializer_class = serializers.ClusterGroupSerializer
     serializer_class = serializers.ClusterGroupSerializer
     filterset_class = filtersets.ClusterGroupFilterSet
     filterset_class = filtersets.ClusterGroupFilterSet
 
 
 
 
 class ClusterViewSet(NetBoxModelViewSet):
 class ClusterViewSet(NetBoxModelViewSet):
-    queryset = Cluster.objects.annotate(
-        device_count=count_related(Device, 'cluster'),
-        virtualmachine_count=count_related(VirtualMachine, 'cluster')
-    )
+    queryset = Cluster.objects.all()
     serializer_class = serializers.ClusterSerializer
     serializer_class = serializers.ClusterSerializer
     filterset_class = filtersets.ClusterFilterSet
     filterset_class = filtersets.ClusterFilterSet
 
 

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

@@ -1,6 +1,7 @@
 from drf_spectacular.utils import extend_schema_serializer
 from drf_spectacular.utils import extend_schema_serializer
 from rest_framework import serializers
 from rest_framework import serializers
 
 
+from netbox.api.fields import RelatedObjectCountField
 from netbox.api.serializers import WritableNestedSerializer
 from netbox.api.serializers import WritableNestedSerializer
 from vpn import models
 from vpn import models
 
 
@@ -23,7 +24,7 @@ __all__ = (
 )
 )
 class NestedTunnelGroupSerializer(WritableNestedSerializer):
 class NestedTunnelGroupSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='vpn-api:tunnelgroup-detail')
     url = serializers.HyperlinkedIdentityField(view_name='vpn-api:tunnelgroup-detail')
-    tunnel_count = serializers.IntegerField(read_only=True)
+    tunnel_count = RelatedObjectCountField('tunnels')
 
 
     class Meta:
     class Meta:
         model = models.TunnelGroup
         model = models.TunnelGroup

+ 8 - 3
netbox/vpn/api/serializers.py

@@ -4,7 +4,7 @@ from rest_framework import serializers
 
 
 from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedRouteTargetSerializer
 from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedRouteTargetSerializer
 from ipam.models import RouteTarget
 from ipam.models import RouteTarget
-from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
+from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField, SerializedPKRelatedField
 from netbox.api.serializers import NetBoxModelSerializer
 from netbox.api.serializers import NetBoxModelSerializer
 from netbox.constants import NESTED_SERIALIZER_PREFIX
 from netbox.constants import NESTED_SERIALIZER_PREFIX
 from tenancy.api.nested_serializers import NestedTenantSerializer
 from tenancy.api.nested_serializers import NestedTenantSerializer
@@ -29,7 +29,9 @@ __all__ = (
 
 
 class TunnelGroupSerializer(NetBoxModelSerializer):
 class TunnelGroupSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='vpn-api:tunnelgroup-detail')
     url = serializers.HyperlinkedIdentityField(view_name='vpn-api:tunnelgroup-detail')
-    tunnel_count = serializers.IntegerField(read_only=True)
+
+    # Related object counts
+    tunnel_count = RelatedObjectCountField('tunnels')
 
 
     class Meta:
     class Meta:
         model = TunnelGroup
         model = TunnelGroup
@@ -59,11 +61,14 @@ class TunnelSerializer(NetBoxModelSerializer):
         allow_null=True
         allow_null=True
     )
     )
 
 
+    # Related object counts
+    terminations_count = RelatedObjectCountField('terminations')
+
     class Meta:
     class Meta:
         model = Tunnel
         model = Tunnel
         fields = (
         fields = (
             'id', 'url', 'display', 'name', 'status', 'group', 'encapsulation', 'ipsec_profile', 'tenant', 'tunnel_id',
             'id', 'url', 'display', 'name', 'status', 'group', 'encapsulation', 'ipsec_profile', 'tenant', 'tunnel_id',
-            'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
+            'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'terminations_count',
         )
         )
 
 
 
 

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

@@ -1,7 +1,6 @@
 from rest_framework.routers import APIRootView
 from rest_framework.routers import APIRootView
 
 
 from netbox.api.viewsets import NetBoxModelViewSet
 from netbox.api.viewsets import NetBoxModelViewSet
-from utilities.utils import count_related
 from vpn import filtersets
 from vpn import filtersets
 from vpn.models import *
 from vpn.models import *
 from . import serializers
 from . import serializers
@@ -34,17 +33,13 @@ class VPNRootView(APIRootView):
 #
 #
 
 
 class TunnelGroupViewSet(NetBoxModelViewSet):
 class TunnelGroupViewSet(NetBoxModelViewSet):
-    queryset = TunnelGroup.objects.annotate(
-        tunnel_count=count_related(Tunnel, 'group')
-    )
+    queryset = TunnelGroup.objects.all()
     serializer_class = serializers.TunnelGroupSerializer
     serializer_class = serializers.TunnelGroupSerializer
     filterset_class = filtersets.TunnelGroupFilterSet
     filterset_class = filtersets.TunnelGroupFilterSet
 
 
 
 
 class TunnelViewSet(NetBoxModelViewSet):
 class TunnelViewSet(NetBoxModelViewSet):
-    queryset = Tunnel.objects.annotate(
-        terminations_count=count_related(TunnelTermination, 'tunnel')
-    )
+    queryset = Tunnel.objects.all()
     serializer_class = serializers.TunnelSerializer
     serializer_class = serializers.TunnelSerializer
     filterset_class = filtersets.TunnelFilterSet
     filterset_class = filtersets.TunnelFilterSet