From 7abb2b2ab55e9377eeee9e4234c85cdf02440705 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 15 Feb 2024 14:49:27 -0500 Subject: [PATCH] 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 --- netbox/circuits/api/nested_serializers.py | 8 +-- netbox/circuits/api/serializers.py | 12 ++-- netbox/circuits/api/views.py | 9 +-- netbox/core/api/serializers.py | 6 +- netbox/core/api/views.py | 5 +- netbox/dcim/api/nested_serializers.py | 24 ++++---- netbox/dcim/api/serializers.py | 58 ++++++++++++------- netbox/dcim/api/views.py | 52 ++++------------- netbox/dcim/migrations/0002_squashed.py | 2 +- netbox/dcim/models/power.py | 1 + netbox/extras/api/serializers.py | 7 ++- netbox/extras/api/views.py | 6 +- netbox/ipam/api/nested_serializers.py | 11 ++-- netbox/ipam/api/serializers.py | 32 ++++++---- netbox/ipam/api/views.py | 24 ++------ netbox/netbox/api/fields.py | 16 ++++- netbox/netbox/api/viewsets/__init__.py | 13 +++-- netbox/netbox/api/viewsets/mixins.py | 13 ----- netbox/tenancy/api/serializers.py | 24 ++++---- netbox/tenancy/api/views.py | 18 +----- netbox/utilities/api.py | 23 ++++++++ .../virtualization/api/nested_serializers.py | 7 ++- netbox/virtualization/api/serializers.py | 16 +++-- netbox/virtualization/api/views.py | 15 +---- netbox/vpn/api/nested_serializers.py | 3 +- netbox/vpn/api/serializers.py | 11 +++- netbox/vpn/api/views.py | 9 +-- 27 files changed, 204 insertions(+), 221 deletions(-) diff --git a/netbox/circuits/api/nested_serializers.py b/netbox/circuits/api/nested_serializers.py index c7aa583fa..36254dc8b 100644 --- a/netbox/circuits/api/nested_serializers.py +++ b/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 circuits.models import * +from netbox.api.fields import RelatedObjectCountField from netbox.api.serializers import WritableNestedSerializer __all__ = [ @@ -36,7 +36,7 @@ class NestedProviderNetworkSerializer(WritableNestedSerializer): ) class NestedProviderSerializer(WritableNestedSerializer): url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail') - circuit_count = serializers.IntegerField(read_only=True) + circuit_count = RelatedObjectCountField('circuits') class Meta: model = Provider @@ -64,7 +64,7 @@ class NestedProviderAccountSerializer(WritableNestedSerializer): ) class NestedCircuitTypeSerializer(WritableNestedSerializer): url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail') - circuit_count = serializers.IntegerField(read_only=True) + circuit_count = RelatedObjectCountField('circuits') class Meta: model = CircuitType diff --git a/netbox/circuits/api/serializers.py b/netbox/circuits/api/serializers.py index 5223de339..4f88b9b28 100644 --- a/netbox/circuits/api/serializers.py +++ b/netbox/circuits/api/serializers.py @@ -6,7 +6,7 @@ from dcim.api.nested_serializers import NestedSiteSerializer from dcim.api.serializers import CabledObjectSerializer from ipam.models import ASN 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 tenancy.api.nested_serializers import NestedTenantSerializer from .nested_serializers import * @@ -32,7 +32,7 @@ class ProviderSerializer(NetBoxModelSerializer): ) # Related object counts - circuit_count = serializers.IntegerField(read_only=True) + circuit_count = RelatedObjectCountField('circuits') class Meta: model = Provider @@ -80,13 +80,15 @@ class ProviderNetworkSerializer(NetBoxModelSerializer): class CircuitTypeSerializer(NetBoxModelSerializer): 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: model = CircuitType 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', ] diff --git a/netbox/circuits/api/views.py b/netbox/circuits/api/views.py index 71db8f8f2..fffb59a57 100644 --- a/netbox/circuits/api/views.py +++ b/netbox/circuits/api/views.py @@ -4,7 +4,6 @@ from circuits import filtersets from circuits.models import * from dcim.api.views import PassThroughPortMixin from netbox.api.viewsets import NetBoxModelViewSet -from utilities.utils import count_related from . import serializers @@ -21,9 +20,7 @@ class CircuitsRootView(APIRootView): # class ProviderViewSet(NetBoxModelViewSet): - queryset = Provider.objects.annotate( - circuit_count=count_related(Circuit, 'provider') - ) + queryset = Provider.objects.all() serializer_class = serializers.ProviderSerializer filterset_class = filtersets.ProviderFilterSet @@ -33,9 +30,7 @@ class ProviderViewSet(NetBoxModelViewSet): # class CircuitTypeViewSet(NetBoxModelViewSet): - queryset = CircuitType.objects.annotate( - circuit_count=count_related(Circuit, 'type') - ) + queryset = CircuitType.objects.all() serializer_class = serializers.CircuitTypeSerializer filterset_class = filtersets.CircuitTypeFilterSet diff --git a/netbox/core/api/serializers.py b/netbox/core/api/serializers.py index a16a06d62..d685ba43d 100644 --- a/netbox/core/api/serializers.py +++ b/netbox/core/api/serializers.py @@ -2,7 +2,7 @@ from rest_framework import serializers from core.choices 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.utils import get_data_backend_choices from users.api.nested_serializers import NestedUserSerializer @@ -28,9 +28,7 @@ class DataSourceSerializer(NetBoxModelSerializer): ) # Related object counts - file_count = serializers.IntegerField( - read_only=True - ) + file_count = RelatedObjectCountField('datafiles') class Meta: model = DataSource diff --git a/netbox/core/api/views.py b/netbox/core/api/views.py index 9c2e23f2e..3fddfd691 100644 --- a/netbox/core/api/views.py +++ b/netbox/core/api/views.py @@ -9,7 +9,6 @@ from rest_framework.viewsets import ReadOnlyModelViewSet from core import filtersets from core.models import * from netbox.api.viewsets import NetBoxModelViewSet, NetBoxReadOnlyModelViewSet -from utilities.utils import count_related from . import serializers @@ -22,9 +21,7 @@ class CoreRootView(APIRootView): class DataSourceViewSet(NetBoxModelViewSet): - queryset = DataSource.objects.annotate( - file_count=count_related(DataFile, 'source') - ) + queryset = DataSource.objects.all() serializer_class = serializers.DataSourceSerializer filterset_class = filtersets.DataSourceFilterSet diff --git a/netbox/dcim/api/nested_serializers.py b/netbox/dcim/api/nested_serializers.py index c8440612d..9a59af8e2 100644 --- a/netbox/dcim/api/nested_serializers.py +++ b/netbox/dcim/api/nested_serializers.py @@ -2,7 +2,8 @@ from drf_spectacular.utils import extend_schema_serializer from rest_framework import serializers from dcim import models -from netbox.api.serializers import BaseModelSerializer, WritableNestedSerializer +from netbox.api.fields import RelatedObjectCountField +from netbox.api.serializers import WritableNestedSerializer __all__ = [ 'ComponentNestedModuleSerializer', @@ -110,7 +111,7 @@ class NestedLocationSerializer(WritableNestedSerializer): ) class NestedRackRoleSerializer(WritableNestedSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail') - rack_count = serializers.IntegerField(read_only=True) + rack_count = RelatedObjectCountField('racks') class Meta: model = models.RackRole @@ -122,7 +123,7 @@ class NestedRackRoleSerializer(WritableNestedSerializer): ) class NestedRackSerializer(WritableNestedSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail') - device_count = serializers.IntegerField(read_only=True) + device_count = RelatedObjectCountField('devices') class Meta: model = models.Rack @@ -150,7 +151,7 @@ class NestedRackReservationSerializer(WritableNestedSerializer): ) class NestedManufacturerSerializer(WritableNestedSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail') - devicetype_count = serializers.IntegerField(read_only=True) + devicetype_count = RelatedObjectCountField('device_types') class Meta: model = models.Manufacturer @@ -163,7 +164,7 @@ class NestedManufacturerSerializer(WritableNestedSerializer): class NestedDeviceTypeSerializer(WritableNestedSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail') manufacturer = NestedManufacturerSerializer(read_only=True) - device_count = serializers.IntegerField(read_only=True) + device_count = RelatedObjectCountField('instances') class Meta: model = models.DeviceType @@ -173,7 +174,6 @@ class NestedDeviceTypeSerializer(WritableNestedSerializer): class NestedModuleTypeSerializer(WritableNestedSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:moduletype-detail') manufacturer = NestedManufacturerSerializer(read_only=True) - # module_count = serializers.IntegerField(read_only=True) class Meta: model = models.ModuleType @@ -274,8 +274,8 @@ class NestedInventoryItemTemplateSerializer(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) + device_count = RelatedObjectCountField('devices') + virtualmachine_count = RelatedObjectCountField('virtual_machines') class Meta: model = models.DeviceRole @@ -287,8 +287,8 @@ class NestedDeviceRoleSerializer(WritableNestedSerializer): ) 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) + device_count = RelatedObjectCountField('devices') + virtualmachine_count = RelatedObjectCountField('virtual_machines') class Meta: model = models.Platform @@ -445,7 +445,7 @@ class NestedInventoryItemSerializer(WritableNestedSerializer): ) class NestedInventoryItemRoleSerializer(WritableNestedSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemrole-detail') - inventoryitem_count = serializers.IntegerField(read_only=True) + inventoryitem_count = RelatedObjectCountField('inventory_items') class Meta: model = models.InventoryItemRole @@ -490,7 +490,7 @@ class NestedVirtualChassisSerializer(WritableNestedSerializer): ) class NestedPowerPanelSerializer(WritableNestedSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerpanel-detail') - powerfeed_count = serializers.IntegerField(read_only=True) + powerfeed_count = RelatedObjectCountField('powerfeeds') class Meta: model = models.PowerPanel diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 09933f2de..e4c51a476 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -15,7 +15,7 @@ from ipam.api.nested_serializers import ( NestedASNSerializer, NestedIPAddressSerializer, NestedVLANSerializer, NestedVRFSerializer, ) 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 ( GenericObjectSerializer, NestedGroupModelSerializer, NetBoxModelSerializer, ValidatedModelSerializer, WritableNestedSerializer, @@ -144,12 +144,12 @@ class SiteSerializer(NetBoxModelSerializer): ) # 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: model = Site @@ -184,7 +184,9 @@ class LocationSerializer(NestedGroupModelSerializer): class RackRoleSerializer(NetBoxModelSerializer): 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: model = RackRole @@ -207,8 +209,10 @@ class RackSerializer(NetBoxModelSerializer): width = ChoiceField(choices=RackWidthChoices, required=False) 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) - 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: model = Rack @@ -299,9 +303,11 @@ class RackElevationDetailFilterSerializer(serializers.Serializer): class ManufacturerSerializer(NetBoxModelSerializer): 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: model = Manufacturer @@ -325,7 +331,6 @@ class DeviceTypeSerializer(NetBoxModelSerializer): 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) weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False, allow_null=True) - device_count = serializers.IntegerField(read_only=True) # Counter fields 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) inventory_item_template_count = serializers.IntegerField(read_only=True) + # Related object counts + device_count = RelatedObjectCountField('instances') + class Meta: model = DeviceType fields = [ @@ -636,8 +644,10 @@ class InventoryItemTemplateSerializer(ValidatedModelSerializer): class DeviceRoleSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail') 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: model = DeviceRole @@ -651,8 +661,10 @@ class PlatformSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail') manufacturer = NestedManufacturerSerializer(required=False, allow_null=True) 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: model = Platform @@ -761,7 +773,7 @@ class VirtualDeviceContextSerializer(NetBoxModelSerializer): status = ChoiceField(choices=VirtualDeviceContextStatusChoices) # Related object counts - interface_count = serializers.IntegerField(read_only=True) + interface_count = RelatedObjectCountField('interfaces') class Meta: model = VirtualDeviceContext @@ -1092,7 +1104,9 @@ class InventoryItemSerializer(NetBoxModelSerializer): class InventoryItemRoleSerializer(NetBoxModelSerializer): 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: model = InventoryItemRole @@ -1204,7 +1218,9 @@ class PowerPanelSerializer(NetBoxModelSerializer): allow_null=True, default=None ) - powerfeed_count = serializers.IntegerField(read_only=True) + + # Related object counts + powerfeed_count = RelatedObjectCountField('powerfeeds') class Meta: model = PowerPanel diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index c429980c4..ea1a2e3d1 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -13,7 +13,6 @@ from dcim.constants import CABLE_TRACE_SVG_DEFAULT_WIDTH from dcim.models import * from dcim.svg import CableTraceSVG from extras.api.mixins import ConfigContextQuerySetMixin, RenderConfigMixin -from ipam.models import Prefix, VLAN from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired from netbox.api.metadata import ContentTypeMetadata 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.query_functions import CollateAsChar from utilities.utils import count_related -from virtualization.models import VirtualMachine from . import serializers from .exceptions import MissingFilterException @@ -129,14 +127,7 @@ class SiteGroupViewSet(MPTTLockedMixin, 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 filterset_class = filtersets.SiteFilterSet @@ -168,9 +159,7 @@ class LocationViewSet(MPTTLockedMixin, NetBoxModelViewSet): # class RackRoleViewSet(NetBoxModelViewSet): - queryset = RackRole.objects.annotate( - rack_count=count_related(Rack, 'role') - ) + queryset = RackRole.objects.all() serializer_class = serializers.RackRoleSerializer filterset_class = filtersets.RackRoleFilterSet @@ -180,10 +169,7 @@ class RackRoleViewSet(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 filterset_class = filtersets.RackFilterSet @@ -255,11 +241,7 @@ class RackReservationViewSet(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 filterset_class = filtersets.ManufacturerFilterSet @@ -269,9 +251,7 @@ class ManufacturerViewSet(NetBoxModelViewSet): # class DeviceTypeViewSet(NetBoxModelViewSet): - queryset = DeviceType.objects.annotate( - device_count=count_related(Device, 'device_type') - ) + queryset = DeviceType.objects.all() serializer_class = serializers.DeviceTypeSerializer filterset_class = filtersets.DeviceTypeFilterSet @@ -351,10 +331,7 @@ class InventoryItemTemplateViewSet(MPTTLockedMixin, 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 filterset_class = filtersets.DeviceRoleFilterSet @@ -364,10 +341,7 @@ class DeviceRoleViewSet(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 filterset_class = filtersets.PlatformFilterSet @@ -410,9 +384,7 @@ class DeviceViewSet( class VirtualDeviceContextViewSet(NetBoxModelViewSet): - queryset = VirtualDeviceContext.objects.annotate( - interface_count=count_related(Interface, 'vdcs'), - ) + queryset = VirtualDeviceContext.objects.all() serializer_class = serializers.VirtualDeviceContextSerializer filterset_class = filtersets.VirtualDeviceContextFilterSet @@ -513,9 +485,7 @@ class InventoryItemViewSet(MPTTLockedMixin, NetBoxModelViewSet): # class InventoryItemRoleViewSet(NetBoxModelViewSet): - queryset = InventoryItemRole.objects.annotate( - inventoryitem_count=count_related(InventoryItem, 'role') - ) + queryset = InventoryItemRole.objects.all() serializer_class = serializers.InventoryItemRoleSerializer filterset_class = filtersets.InventoryItemRoleFilterSet @@ -552,9 +522,7 @@ class VirtualChassisViewSet(NetBoxModelViewSet): # class PowerPanelViewSet(NetBoxModelViewSet): - queryset = PowerPanel.objects.annotate( - powerfeed_count=count_related(PowerFeed, 'power_panel') - ) + queryset = PowerPanel.objects.all() serializer_class = serializers.PowerPanelSerializer filterset_class = filtersets.PowerPanelFilterSet diff --git a/netbox/dcim/migrations/0002_squashed.py b/netbox/dcim/migrations/0002_squashed.py index c7325210e..786167680 100644 --- a/netbox/dcim/migrations/0002_squashed.py +++ b/netbox/dcim/migrations/0002_squashed.py @@ -233,7 +233,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='powerfeed', 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( model_name='powerfeed', diff --git a/netbox/dcim/models/power.py b/netbox/dcim/models/power.py index 62578d6c4..826eaae9c 100644 --- a/netbox/dcim/models/power.py +++ b/netbox/dcim/models/power.py @@ -84,6 +84,7 @@ class PowerFeed(PrimaryModel, PathEndpoint, CabledObjectModel): rack = models.ForeignKey( to='Rack', on_delete=models.PROTECT, + related_name='powerfeeds', blank=True, null=True ) diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index e2f30e8b0..40b73f902 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -3,7 +3,6 @@ from django.core.exceptions import ObjectDoesNotExist from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import extend_schema_field from rest_framework import serializers -from rest_framework.fields import ListField from core.api.nested_serializers import NestedDataSourceSerializer, NestedDataFileSerializer, NestedJobSerializer 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.models import * 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.features import TaggableModelSerializer from netbox.constants import NESTED_SERIALIZER_PREFIX @@ -288,7 +287,9 @@ class TagSerializer(ValidatedModelSerializer): many=True, required=False ) - tagged_items = serializers.IntegerField(read_only=True) + + # Related object counts + tagged_items = RelatedObjectCountField('extras_taggeditem_items') class Meta: model = Tag diff --git a/netbox/extras/api/views.py b/netbox/extras/api/views.py index 32a569d52..db26a6f6c 100644 --- a/netbox/extras/api/views.py +++ b/netbox/extras/api/views.py @@ -23,7 +23,7 @@ from netbox.api.metadata import ContentTypeMetadata from netbox.api.renderers import TextRenderer from netbox.api.viewsets import NetBoxModelViewSet 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 .mixins import ConfigTemplateRenderMixin @@ -147,9 +147,7 @@ class BookmarkViewSet(NetBoxModelViewSet): # class TagViewSet(NetBoxModelViewSet): - queryset = Tag.objects.annotate( - tagged_items=count_related(TaggedItem, 'tag') - ) + queryset = Tag.objects.all() serializer_class = serializers.TagSerializer filterset_class = filtersets.TagFilterSet diff --git a/netbox/ipam/api/nested_serializers.py b/netbox/ipam/api/nested_serializers.py index c012eca6d..4d4a77bef 100644 --- a/netbox/ipam/api/nested_serializers.py +++ b/netbox/ipam/api/nested_serializers.py @@ -2,6 +2,7 @@ from drf_spectacular.utils import extend_schema_serializer from rest_framework import serializers from ipam import models +from netbox.api.fields import RelatedObjectCountField from netbox.api.serializers import WritableNestedSerializer from .field_serializers import IPAddressField @@ -58,7 +59,7 @@ class NestedASNSerializer(WritableNestedSerializer): ) class NestedVRFSerializer(WritableNestedSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vrf-detail') - prefix_count = serializers.IntegerField(read_only=True) + prefix_count = RelatedObjectCountField('prefixes') class Meta: model = models.VRF @@ -86,7 +87,7 @@ class NestedRouteTargetSerializer(WritableNestedSerializer): ) class NestedRIRSerializer(WritableNestedSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail') - aggregate_count = serializers.IntegerField(read_only=True) + aggregate_count = RelatedObjectCountField('aggregates') class Meta: model = models.RIR @@ -132,8 +133,8 @@ class NestedFHRPGroupAssignmentSerializer(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) + prefix_count = RelatedObjectCountField('prefixes') + vlan_count = RelatedObjectCountField('vlans') class Meta: model = models.Role @@ -145,7 +146,7 @@ class NestedRoleSerializer(WritableNestedSerializer): ) class NestedVLANGroupSerializer(WritableNestedSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail') - vlan_count = serializers.IntegerField(read_only=True) + vlan_count = RelatedObjectCountField('vlans') class Meta: model = models.VLANGroup diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index 33aa55a93..eedb19915 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -6,7 +6,7 @@ from dcim.api.nested_serializers import NestedDeviceSerializer, NestedSiteSerial from ipam.choices import * from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS, VLANGROUP_SCOPE_TYPES 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.constants import NESTED_SERIALIZER_PREFIX from tenancy.api.nested_serializers import NestedTenantSerializer @@ -43,8 +43,10 @@ class ASNSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:asn-detail') rir = NestedRIRSerializer(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: model = ASN @@ -90,8 +92,10 @@ class VRFSerializer(NetBoxModelSerializer): required=False, 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: model = VRF @@ -124,7 +128,9 @@ class RouteTargetSerializer(NetBoxModelSerializer): class RIRSerializer(NetBoxModelSerializer): 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: model = RIR @@ -195,8 +201,10 @@ class FHRPGroupAssignmentSerializer(NetBoxModelSerializer): class RoleSerializer(NetBoxModelSerializer): 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: model = Role @@ -218,9 +226,11 @@ class VLANGroupSerializer(NetBoxModelSerializer): ) scope_id = serializers.IntegerField(allow_null=True, required=False, default=None) scope = serializers.SerializerMethodField(read_only=True) - vlan_count = serializers.IntegerField(read_only=True) utilization = serializers.CharField(read_only=True) + # Related object counts + vlan_count = RelatedObjectCountField('vlans') + class Meta: model = VLANGroup fields = [ @@ -247,7 +257,9 @@ class VLANSerializer(NetBoxModelSerializer): status = ChoiceField(choices=VLANStatusChoices, required=False) role = NestedRoleSerializer(required=False, 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: model = VLAN diff --git a/netbox/ipam/api/views.py b/netbox/ipam/api/views.py index c3594bdcf..d928e6a26 100644 --- a/netbox/ipam/api/views.py +++ b/netbox/ipam/api/views.py @@ -12,8 +12,6 @@ from rest_framework.response import Response from rest_framework.routers import APIRootView from rest_framework.views import APIView -from circuits.models import Provider -from dcim.models import Site from ipam import filtersets from ipam.models import * 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.constants import ADVISORY_LOCK_KEYS from utilities.api import get_serializer_for_model -from utilities.utils import count_related from . import serializers @@ -45,19 +42,13 @@ class ASNRangeViewSet(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 filterset_class = filtersets.ASNFilterSet 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 filterset_class = filtersets.VRFFilterSet @@ -69,9 +60,7 @@ class RouteTargetViewSet(NetBoxModelViewSet): class RIRViewSet(NetBoxModelViewSet): - queryset = RIR.objects.annotate( - aggregate_count=count_related(Aggregate, 'rir') - ) + queryset = RIR.objects.all() serializer_class = serializers.RIRSerializer filterset_class = filtersets.RIRFilterSet @@ -83,10 +72,7 @@ class AggregateViewSet(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 filterset_class = filtersets.RoleFilterSet @@ -151,8 +137,6 @@ class VLANGroupViewSet(NetBoxModelViewSet): class VLANViewSet(NetBoxModelViewSet): queryset = VLAN.objects.prefetch_related( 'l2vpn_terminations', # Referenced by VLANSerializer.l2vpn_termination - ).annotate( - prefix_count=count_related(Prefix, 'vlan') ) serializer_class = serializers.VLANSerializer filterset_class = filtersets.VLANFilterSet diff --git a/netbox/netbox/api/fields.py b/netbox/netbox/api/fields.py index d6e43ea75..ae0621c46 100644 --- a/netbox/netbox/api/fields.py +++ b/netbox/netbox/api/fields.py @@ -1,6 +1,6 @@ from django.core.exceptions import ObjectDoesNotExist -from drf_spectacular.utils import extend_schema_field from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import extend_schema_field from netaddr import IPNetwork from rest_framework import serializers from rest_framework.exceptions import ValidationError @@ -10,6 +10,7 @@ __all__ = ( 'ChoiceField', 'ContentTypeField', 'IPNetworkSerializer', + 'RelatedObjectCountField', 'SerializedPKRelatedField', ) @@ -135,3 +136,16 @@ class SerializedPKRelatedField(PrimaryKeyRelatedField): def to_representation(self, value): 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) diff --git a/netbox/netbox/api/viewsets/__init__.py b/netbox/netbox/api/viewsets/__init__.py index e9edf9311..2ff764bed 100644 --- a/netbox/netbox/api/viewsets/__init__.py +++ b/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.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 . import mixins @@ -44,15 +44,16 @@ class BaseViewSet(GenericViewSet): def get_queryset(self): qs = super().get_queryset() + serializer_class = self.get_serializer_class() # 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) + # 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 def get_serializer(self, *args, **kwargs): diff --git a/netbox/netbox/api/viewsets/mixins.py b/netbox/netbox/api/viewsets/mixins.py index e6790e21c..b2c6bdda6 100644 --- a/netbox/netbox/api/viewsets/mixins.py +++ b/netbox/netbox/api/viewsets/mixins.py @@ -52,19 +52,6 @@ class BriefModeMixin: 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: """ diff --git a/netbox/tenancy/api/serializers.py b/netbox/tenancy/api/serializers.py index 118cafd81..ce16f2523 100644 --- a/netbox/tenancy/api/serializers.py +++ b/netbox/tenancy/api/serializers.py @@ -3,7 +3,7 @@ from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import extend_schema_field 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.constants import NESTED_SERIALIZER_PREFIX from tenancy.choices import ContactPriorityChoices @@ -32,16 +32,18 @@ class TenantGroupSerializer(NestedGroupModelSerializer): class TenantSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenant-detail') 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: model = Tenant diff --git a/netbox/tenancy/api/views.py b/netbox/tenancy/api/views.py index b922935ae..70330ddb8 100644 --- a/netbox/tenancy/api/views.py +++ b/netbox/tenancy/api/views.py @@ -1,13 +1,8 @@ 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 tenancy import filtersets from tenancy.models import * -from utilities.utils import count_related -from virtualization.models import VirtualMachine, Cluster from . import serializers @@ -36,18 +31,7 @@ class TenantGroupViewSet(MPTTLockedMixin, 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 filterset_class = filtersets.TenantFilterSet diff --git a/netbox/utilities/api.py b/netbox/utilities/api.py index 320d175c3..a13e62bfd 100644 --- a/netbox/utilities/api.py +++ b/netbox/utilities/api.py @@ -11,10 +11,13 @@ from rest_framework import status from rest_framework.serializers import Serializer from rest_framework.utils import formatting +from netbox.api.fields import RelatedObjectCountField from netbox.api.exceptions import GraphQLTypeNotFound, SerializerNotFound +from utilities.utils import count_related from .utils import dynamic_import __all__ = ( + 'get_annotations_for_serializer', 'get_graphql_type_for_model', 'get_prefetches_for_serializer', 'get_serializer_for_model', @@ -131,6 +134,26 @@ def get_prefetches_for_serializer(serializer_class, fields_to_include=None): 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): """ Handle exceptions and return a useful error message for REST API requests. diff --git a/netbox/virtualization/api/nested_serializers.py b/netbox/virtualization/api/nested_serializers.py index afb7e39a1..0c7a10918 100644 --- a/netbox/virtualization/api/nested_serializers.py +++ b/netbox/virtualization/api/nested_serializers.py @@ -1,6 +1,7 @@ from drf_spectacular.utils import extend_schema_serializer from rest_framework import serializers +from netbox.api.fields import RelatedObjectCountField from netbox.api.serializers import WritableNestedSerializer from virtualization.models import * @@ -23,7 +24,7 @@ __all__ = [ ) class NestedClusterTypeSerializer(WritableNestedSerializer): url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustertype-detail') - cluster_count = serializers.IntegerField(read_only=True) + cluster_count = RelatedObjectCountField('clusters') class Meta: model = ClusterType @@ -35,7 +36,7 @@ class NestedClusterTypeSerializer(WritableNestedSerializer): ) class NestedClusterGroupSerializer(WritableNestedSerializer): url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustergroup-detail') - cluster_count = serializers.IntegerField(read_only=True) + cluster_count = RelatedObjectCountField('clusters') class Meta: model = ClusterGroup @@ -47,7 +48,7 @@ class NestedClusterGroupSerializer(WritableNestedSerializer): ) class NestedClusterSerializer(WritableNestedSerializer): url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:cluster-detail') - virtualmachine_count = serializers.IntegerField(read_only=True) + virtualmachine_count = RelatedObjectCountField('virtual_machines') class Meta: model = Cluster diff --git a/netbox/virtualization/api/serializers.py b/netbox/virtualization/api/serializers.py index 7ed36388b..5bfec1dd6 100644 --- a/netbox/virtualization/api/serializers.py +++ b/netbox/virtualization/api/serializers.py @@ -8,7 +8,7 @@ from dcim.choices import InterfaceModeChoices from extras.api.nested_serializers import NestedConfigTemplateSerializer from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer, NestedVRFSerializer 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 tenancy.api.nested_serializers import NestedTenantSerializer from virtualization.choices import * @@ -23,7 +23,9 @@ from .nested_serializers import * class ClusterTypeSerializer(NetBoxModelSerializer): 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: model = ClusterType @@ -35,7 +37,9 @@ class ClusterTypeSerializer(NetBoxModelSerializer): class ClusterGroupSerializer(NetBoxModelSerializer): 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: model = ClusterGroup @@ -52,8 +56,10 @@ class ClusterSerializer(NetBoxModelSerializer): status = ChoiceField(choices=ClusterStatusChoices, required=False) tenant = NestedTenantSerializer(required=False, allow_null=True) 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: model = Cluster diff --git a/netbox/virtualization/api/views.py b/netbox/virtualization/api/views.py index ff894c6dc..c9ed0f970 100644 --- a/netbox/virtualization/api/views.py +++ b/netbox/virtualization/api/views.py @@ -1,10 +1,8 @@ from rest_framework.routers import APIRootView -from dcim.models import Device from extras.api.mixins import ConfigContextQuerySetMixin, RenderConfigMixin from netbox.api.viewsets import NetBoxModelViewSet from utilities.query_functions import CollateAsChar -from utilities.utils import count_related from virtualization import filtersets from virtualization.models import * from . import serializers @@ -23,26 +21,19 @@ class VirtualizationRootView(APIRootView): # class ClusterTypeViewSet(NetBoxModelViewSet): - queryset = ClusterType.objects.annotate( - cluster_count=count_related(Cluster, 'type') - ) + queryset = ClusterType.objects.all() serializer_class = serializers.ClusterTypeSerializer filterset_class = filtersets.ClusterTypeFilterSet class ClusterGroupViewSet(NetBoxModelViewSet): - queryset = ClusterGroup.objects.annotate( - cluster_count=count_related(Cluster, 'group') - ) + queryset = ClusterGroup.objects.all() serializer_class = serializers.ClusterGroupSerializer filterset_class = filtersets.ClusterGroupFilterSet 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 filterset_class = filtersets.ClusterFilterSet diff --git a/netbox/vpn/api/nested_serializers.py b/netbox/vpn/api/nested_serializers.py index 1042b375e..c91dcb40a 100644 --- a/netbox/vpn/api/nested_serializers.py +++ b/netbox/vpn/api/nested_serializers.py @@ -1,6 +1,7 @@ from drf_spectacular.utils import extend_schema_serializer from rest_framework import serializers +from netbox.api.fields import RelatedObjectCountField from netbox.api.serializers import WritableNestedSerializer from vpn import models @@ -23,7 +24,7 @@ __all__ = ( ) class NestedTunnelGroupSerializer(WritableNestedSerializer): url = serializers.HyperlinkedIdentityField(view_name='vpn-api:tunnelgroup-detail') - tunnel_count = serializers.IntegerField(read_only=True) + tunnel_count = RelatedObjectCountField('tunnels') class Meta: model = models.TunnelGroup diff --git a/netbox/vpn/api/serializers.py b/netbox/vpn/api/serializers.py index dedcbfbf5..a1f5995a4 100644 --- a/netbox/vpn/api/serializers.py +++ b/netbox/vpn/api/serializers.py @@ -4,7 +4,7 @@ from rest_framework import serializers from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedRouteTargetSerializer 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.constants import NESTED_SERIALIZER_PREFIX from tenancy.api.nested_serializers import NestedTenantSerializer @@ -29,7 +29,9 @@ __all__ = ( class TunnelGroupSerializer(NetBoxModelSerializer): 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: model = TunnelGroup @@ -59,11 +61,14 @@ class TunnelSerializer(NetBoxModelSerializer): allow_null=True ) + # Related object counts + terminations_count = RelatedObjectCountField('terminations') + class Meta: model = Tunnel fields = ( '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', ) diff --git a/netbox/vpn/api/views.py b/netbox/vpn/api/views.py index 5015f0618..4119d396a 100644 --- a/netbox/vpn/api/views.py +++ b/netbox/vpn/api/views.py @@ -1,7 +1,6 @@ from rest_framework.routers import APIRootView from netbox.api.viewsets import NetBoxModelViewSet -from utilities.utils import count_related from vpn import filtersets from vpn.models import * from . import serializers @@ -34,17 +33,13 @@ class VPNRootView(APIRootView): # class TunnelGroupViewSet(NetBoxModelViewSet): - queryset = TunnelGroup.objects.annotate( - tunnel_count=count_related(Tunnel, 'group') - ) + queryset = TunnelGroup.objects.all() serializer_class = serializers.TunnelGroupSerializer filterset_class = filtersets.TunnelGroupFilterSet class TunnelViewSet(NetBoxModelViewSet): - queryset = Tunnel.objects.annotate( - terminations_count=count_related(TunnelTermination, 'tunnel') - ) + queryset = Tunnel.objects.all() serializer_class = serializers.TunnelSerializer filterset_class = filtersets.TunnelFilterSet