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

Replace nested serializers with primary serializers where possible

Jeremy Stretch 2 лет назад
Родитель
Сommit
c146f5e1b5

+ 13 - 14
netbox/circuits/api/serializers.py

@@ -2,13 +2,12 @@ from rest_framework import serializers
 
 from circuits.choices import CircuitStatusChoices
 from circuits.models import *
-from dcim.api.nested_serializers import NestedSiteSerializer
-from dcim.api.serializers import CabledObjectSerializer
+from dcim.api.serializers import CabledObjectSerializer, SiteSerializer
 from ipam.api.nested_serializers import NestedASNSerializer
 from ipam.models import ASN
 from netbox.api.fields import ChoiceField, RelatedObjectCountField, SerializedPKRelatedField
 from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer
-from tenancy.api.nested_serializers import NestedTenantSerializer
+from tenancy.api.serializers import TenantSerializer
 from .nested_serializers import *
 
 
@@ -49,7 +48,7 @@ class ProviderSerializer(NetBoxModelSerializer):
 
 class ProviderAccountSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provideraccount-detail')
-    provider = NestedProviderSerializer()
+    provider = ProviderSerializer(nested=True)
 
     class Meta:
         model = ProviderAccount
@@ -66,7 +65,7 @@ class ProviderAccountSerializer(NetBoxModelSerializer):
 
 class ProviderNetworkSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='circuits-api:providernetwork-detail')
-    provider = NestedProviderSerializer()
+    provider = ProviderSerializer(nested=True)
 
     class Meta:
         model = ProviderNetwork
@@ -98,8 +97,8 @@ class CircuitTypeSerializer(NetBoxModelSerializer):
 
 class CircuitCircuitTerminationSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
-    site = NestedSiteSerializer(allow_null=True)
-    provider_network = NestedProviderNetworkSerializer(allow_null=True)
+    site = SiteSerializer(nested=True, allow_null=True)
+    provider_network = ProviderNetworkSerializer(nested=True, allow_null=True)
 
     class Meta:
         model = CircuitTermination
@@ -111,11 +110,11 @@ class CircuitCircuitTerminationSerializer(WritableNestedSerializer):
 
 class CircuitSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
-    provider = NestedProviderSerializer()
-    provider_account = NestedProviderAccountSerializer(required=False, allow_null=True)
+    provider = ProviderSerializer(nested=True)
+    provider_account = ProviderAccountSerializer(nested=True, required=False, allow_null=True)
     status = ChoiceField(choices=CircuitStatusChoices, required=False)
-    type = NestedCircuitTypeSerializer()
-    tenant = NestedTenantSerializer(required=False, allow_null=True)
+    type = CircuitTypeSerializer(nested=True)
+    tenant = TenantSerializer(nested=True, required=False, allow_null=True)
     termination_a = CircuitCircuitTerminationSerializer(read_only=True, allow_null=True)
     termination_z = CircuitCircuitTerminationSerializer(read_only=True, allow_null=True)
 
@@ -131,9 +130,9 @@ class CircuitSerializer(NetBoxModelSerializer):
 
 class CircuitTerminationSerializer(NetBoxModelSerializer, CabledObjectSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
-    circuit = NestedCircuitSerializer()
-    site = NestedSiteSerializer(required=False, allow_null=True)
-    provider_network = NestedProviderNetworkSerializer(required=False, allow_null=True)
+    circuit = CircuitSerializer(nested=True)
+    site = SiteSerializer(nested=True, required=False, allow_null=True)
+    provider_network = ProviderNetworkSerializer(nested=True, required=False, allow_null=True)
 
     class Meta:
         model = CircuitTermination

+ 3 - 2
netbox/core/api/nested_serializers.py

@@ -4,7 +4,7 @@ from core.choices import JobStatusChoices
 from core.models import *
 from netbox.api.fields import ChoiceField
 from netbox.api.serializers import WritableNestedSerializer
-from users.api.nested_serializers import NestedUserSerializer
+from users.api.serializers import UserSerializer
 
 __all__ = (
     'NestedDataFileSerializer',
@@ -32,7 +32,8 @@ class NestedDataFileSerializer(WritableNestedSerializer):
 class NestedJobSerializer(serializers.ModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='core-api:job-detail')
     status = ChoiceField(choices=JobStatusChoices)
-    user = NestedUserSerializer(
+    user = UserSerializer(
+        nested=True,
         read_only=True
     )
 

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

@@ -5,8 +5,7 @@ from core.models import *
 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
-from .nested_serializers import *
+from users.api.serializers import UserSerializer
 
 __all__ = (
     'DataFileSerializer',
@@ -43,7 +42,8 @@ class DataFileSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(
         view_name='core-api:datafile-detail'
     )
-    source = NestedDataSourceSerializer(
+    source = DataSourceSerializer(
+        nested=True,
         read_only=True
     )
 
@@ -57,7 +57,8 @@ class DataFileSerializer(NetBoxModelSerializer):
 
 class JobSerializer(BaseModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='core-api:job-detail')
-    user = NestedUserSerializer(
+    user = UserSerializer(
+        nested=True,
         read_only=True
     )
     status = ChoiceField(choices=JobStatusChoices, read_only=True)

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

@@ -6,8 +6,6 @@ from netbox.api.fields import RelatedObjectCountField
 from netbox.api.serializers import WritableNestedSerializer
 
 __all__ = [
-    'ComponentNestedModuleSerializer',
-    'ModuleBayNestedModuleSerializer',
     'NestedCableSerializer',
     'NestedConsolePortSerializer',
     'NestedConsolePortTemplateSerializer',
@@ -311,26 +309,6 @@ class ModuleNestedModuleBaySerializer(WritableNestedSerializer):
         fields = ['id', 'url', 'display', 'name']
 
 
-class ModuleBayNestedModuleSerializer(WritableNestedSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail')
-
-    class Meta:
-        model = models.Module
-        fields = ['id', 'url', 'display', 'serial']
-
-
-class ComponentNestedModuleSerializer(WritableNestedSerializer):
-    """
-    Used by device component serializers.
-    """
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail')
-    module_bay = ModuleNestedModuleBaySerializer(read_only=True)
-
-    class Meta:
-        model = models.Module
-        fields = ['id', 'url', 'display', 'device', 'module_bay']
-
-
 class NestedModuleSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail')
     device = NestedDeviceSerializer(read_only=True)

Разница между файлами не показана из-за своего большого размера
+ 333 - 298
netbox/dcim/api/serializers.py


+ 2 - 2
netbox/extras/api/mixins.py

@@ -5,7 +5,7 @@ from rest_framework.response import Response
 from rest_framework.status import HTTP_400_BAD_REQUEST
 
 from netbox.api.renderers import TextRenderer
-from .nested_serializers import NestedConfigTemplateSerializer
+from .serializers import ConfigTemplateSerializer
 
 __all__ = (
     'ConfigContextQuerySetMixin',
@@ -52,7 +52,7 @@ class ConfigTemplateRenderMixin:
         if request.accepted_renderer.format == 'txt':
             return Response(output)
 
-        template_serializer = NestedConfigTemplateSerializer(configtemplate, context={'request': request})
+        template_serializer = ConfigTemplateSerializer(configtemplate, nested=True, context={'request': request})
 
         return Response({
             'configtemplate': template_serializer.data,

+ 42 - 35
netbox/extras/api/serializers.py

@@ -5,8 +5,7 @@ from drf_spectacular.types import OpenApiTypes
 from drf_spectacular.utils import extend_schema_field
 from rest_framework import serializers
 
-from core.api.nested_serializers import NestedDataSourceSerializer, NestedDataFileSerializer, NestedJobSerializer
-from core.api.serializers import JobSerializer
+from core.api.serializers import DataFileSerializer, DataSourceSerializer, JobSerializer
 from core.models import ContentType
 from dcim.api.nested_serializers import (
     NestedDeviceRoleSerializer, NestedDeviceTypeSerializer, NestedLocationSerializer, NestedPlatformSerializer,
@@ -22,7 +21,7 @@ from netbox.api.serializers.features import TaggableModelSerializer
 from netbox.constants import NESTED_SERIALIZER_PREFIX
 from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantGroupSerializer
 from tenancy.models import Tenant, TenantGroup
-from users.api.nested_serializers import NestedUserSerializer
+from users.api.serializers import UserSerializer
 from utilities.api import get_serializer_for_model
 from virtualization.api.nested_serializers import (
     NestedClusterGroupSerializer, NestedClusterSerializer, NestedClusterTypeSerializer,
@@ -115,6 +114,28 @@ class WebhookSerializer(NetBoxModelSerializer):
 # Custom fields
 #
 
+class CustomFieldChoiceSetSerializer(ValidatedModelSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfieldchoiceset-detail')
+    base_choices = ChoiceField(
+        choices=CustomFieldChoiceSetBaseChoices,
+        required=False
+    )
+    extra_choices = serializers.ListField(
+        child=serializers.ListField(
+            min_length=2,
+            max_length=2
+        )
+    )
+
+    class Meta:
+        model = CustomFieldChoiceSet
+        fields = [
+            'id', 'url', 'display', 'name', 'description', 'base_choices', 'extra_choices', 'order_alphabetically',
+            'choices_count', 'created', 'last_updated',
+        ]
+        brief_fields = ('id', 'url', 'display', 'name', 'description', 'choices_count')
+
+
 class CustomFieldSerializer(ValidatedModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfield-detail')
     content_types = ContentTypeField(
@@ -129,7 +150,8 @@ class CustomFieldSerializer(ValidatedModelSerializer):
     )
     filter_logic = ChoiceField(choices=CustomFieldFilterLogicChoices, required=False)
     data_type = serializers.SerializerMethodField()
-    choice_set = NestedCustomFieldChoiceSetSerializer(
+    choice_set = CustomFieldChoiceSetSerializer(
+        nested=True,
         required=False,
         allow_null=True
     )
@@ -168,28 +190,6 @@ class CustomFieldSerializer(ValidatedModelSerializer):
         return 'string'
 
 
-class CustomFieldChoiceSetSerializer(ValidatedModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfieldchoiceset-detail')
-    base_choices = ChoiceField(
-        choices=CustomFieldChoiceSetBaseChoices,
-        required=False
-    )
-    extra_choices = serializers.ListField(
-        child=serializers.ListField(
-            min_length=2,
-            max_length=2
-        )
-    )
-
-    class Meta:
-        model = CustomFieldChoiceSet
-        fields = [
-            'id', 'url', 'display', 'name', 'description', 'base_choices', 'extra_choices', 'order_alphabetically',
-            'choices_count', 'created', 'last_updated',
-        ]
-        brief_fields = ('id', 'url', 'display', 'name', 'description', 'choices_count')
-
-
 #
 # Custom links
 #
@@ -220,10 +220,12 @@ class ExportTemplateSerializer(ValidatedModelSerializer):
         queryset=ContentType.objects.with_feature('export_templates'),
         many=True
     )
-    data_source = NestedDataSourceSerializer(
+    data_source = DataSourceSerializer(
+        nested=True,
         required=False
     )
-    data_file = NestedDataFileSerializer(
+    data_file = DataFileSerializer(
+        nested=True,
         read_only=True
     )
 
@@ -267,7 +269,7 @@ class BookmarkSerializer(ValidatedModelSerializer):
         queryset=ContentType.objects.with_feature('bookmarks'),
     )
     object = serializers.SerializerMethodField(read_only=True)
-    user = NestedUserSerializer()
+    user = UserSerializer(nested=True)
 
     class Meta:
         model = Bookmark
@@ -482,10 +484,12 @@ class ConfigContextSerializer(ValidatedModelSerializer):
         required=False,
         many=True
     )
-    data_source = NestedDataSourceSerializer(
+    data_source = DataSourceSerializer(
+        nested=True,
         required=False
     )
-    data_file = NestedDataFileSerializer(
+    data_file = DataFileSerializer(
+        nested=True,
         read_only=True
     )
 
@@ -506,10 +510,12 @@ class ConfigContextSerializer(ValidatedModelSerializer):
 
 class ConfigTemplateSerializer(TaggableModelSerializer, ValidatedModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='extras-api:configtemplate-detail')
-    data_source = NestedDataSourceSerializer(
+    data_source = DataSourceSerializer(
+        nested=True,
         required=False
     )
-    data_file = NestedDataFileSerializer(
+    data_file = DataFileSerializer(
+        nested=True,
         required=False
     )
 
@@ -530,7 +536,7 @@ class ScriptSerializer(ValidatedModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='extras-api:script-detail')
     description = serializers.SerializerMethodField(read_only=True)
     vars = serializers.SerializerMethodField(read_only=True)
-    result = NestedJobSerializer(read_only=True)
+    result = JobSerializer(nested=True, read_only=True)
 
     class Meta:
         model = Script
@@ -596,7 +602,8 @@ class ScriptInputSerializer(serializers.Serializer):
 
 class ObjectChangeSerializer(BaseModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='extras-api:objectchange-detail')
-    user = NestedUserSerializer(
+    user = UserSerializer(
+        nested=True,
         read_only=True
     )
     action = ChoiceField(

+ 59 - 58
netbox/ipam/api/serializers.py

@@ -2,29 +2,48 @@ from django.contrib.contenttypes.models import ContentType
 from drf_spectacular.utils import extend_schema_field
 from rest_framework import serializers
 
-from dcim.api.nested_serializers import NestedDeviceSerializer, NestedSiteSerializer
+from dcim.api.serializers import DeviceSerializer, SiteSerializer
 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, RelatedObjectCountField, SerializedPKRelatedField
 from netbox.api.serializers import NetBoxModelSerializer
 from netbox.constants import NESTED_SERIALIZER_PREFIX
-from tenancy.api.nested_serializers import NestedTenantSerializer
+from tenancy.api.serializers import TenantSerializer
 from utilities.api import get_serializer_for_model
-from virtualization.api.nested_serializers import NestedVirtualMachineSerializer
+from virtualization.api.serializers import VirtualMachineSerializer
 from vpn.api.nested_serializers import NestedL2VPNTerminationSerializer
 from .field_serializers import IPAddressField, IPNetworkField
 from .nested_serializers import *
 
 
+#
+# RIRs
+#
+
+class RIRSerializer(NetBoxModelSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail')
+
+    # Related object counts
+    aggregate_count = RelatedObjectCountField('aggregates')
+
+    class Meta:
+        model = RIR
+        fields = [
+            'id', 'url', 'display', 'name', 'slug', 'is_private', 'description', 'tags', 'custom_fields', 'created',
+            'last_updated', 'aggregate_count',
+        ]
+        brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'aggregate_count')
+
+
 #
 # ASN ranges
 #
 
 class ASNRangeSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:asnrange-detail')
-    rir = NestedRIRSerializer()
-    tenant = NestedTenantSerializer(required=False, allow_null=True)
+    rir = RIRSerializer(nested=True)
+    tenant = TenantSerializer(nested=True, required=False, allow_null=True)
     asn_count = serializers.IntegerField(read_only=True)
 
     class Meta:
@@ -42,8 +61,8 @@ class ASNRangeSerializer(NetBoxModelSerializer):
 
 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)
+    rir = RIRSerializer(nested=True, required=False, allow_null=True)
+    tenant = TenantSerializer(nested=True, required=False, allow_null=True)
 
     # Related object counts
     site_count = RelatedObjectCountField('sites')
@@ -66,7 +85,7 @@ class AvailableASNSerializer(serializers.Serializer):
     description = serializers.CharField(required=False)
 
     def to_representation(self, asn):
-        rir = NestedRIRSerializer(self.context['range'].rir, context={
+        rir = RIRSerializer(self.context['range'].rir, nested=True, context={
             'request': self.context['request']
         }).data
         return {
@@ -81,7 +100,7 @@ class AvailableASNSerializer(serializers.Serializer):
 
 class VRFSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vrf-detail')
-    tenant = NestedTenantSerializer(required=False, allow_null=True)
+    tenant = TenantSerializer(nested=True, required=False, allow_null=True)
     import_targets = SerializedPKRelatedField(
         queryset=RouteTarget.objects.all(),
         serializer=NestedRouteTargetSerializer,
@@ -109,13 +128,9 @@ class VRFSerializer(NetBoxModelSerializer):
         brief_fields = ('id', 'url', 'display', 'name', 'rd', 'description', 'prefix_count')
 
 
-#
-# Route targets
-#
-
 class RouteTargetSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:routetarget-detail')
-    tenant = NestedTenantSerializer(required=False, allow_null=True)
+    tenant = TenantSerializer(nested=True, required=False, allow_null=True)
 
     class Meta:
         model = RouteTarget
@@ -127,29 +142,14 @@ class RouteTargetSerializer(NetBoxModelSerializer):
 
 
 #
-# RIRs/aggregates
+# Aggregates
 #
 
-class RIRSerializer(NetBoxModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail')
-
-    # Related object counts
-    aggregate_count = RelatedObjectCountField('aggregates')
-
-    class Meta:
-        model = RIR
-        fields = [
-            'id', 'url', 'display', 'name', 'slug', 'is_private', 'description', 'tags', 'custom_fields', 'created',
-            'last_updated', 'aggregate_count',
-        ]
-        brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'aggregate_count')
-
-
 class AggregateSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:aggregate-detail')
     family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
-    rir = NestedRIRSerializer()
-    tenant = NestedTenantSerializer(required=False, allow_null=True)
+    rir = RIRSerializer(nested=True)
+    tenant = TenantSerializer(nested=True, required=False, allow_null=True)
     prefix = IPNetworkField()
 
     class Meta:
@@ -180,7 +180,7 @@ class FHRPGroupSerializer(NetBoxModelSerializer):
 
 class FHRPGroupAssignmentSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:fhrpgroupassignment-detail')
-    group = NestedFHRPGroupSerializer()
+    group = FHRPGroupSerializer(nested=True)
     interface_type = ContentTypeField(
         queryset=ContentType.objects.all()
     )
@@ -261,11 +261,11 @@ class VLANGroupSerializer(NetBoxModelSerializer):
 
 class VLANSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail')
-    site = NestedSiteSerializer(required=False, allow_null=True)
-    group = NestedVLANGroupSerializer(required=False, allow_null=True, default=None)
-    tenant = NestedTenantSerializer(required=False, allow_null=True)
+    site = SiteSerializer(nested=True, required=False, allow_null=True)
+    group = VLANGroupSerializer(nested=True, required=False, allow_null=True, default=None)
+    tenant = TenantSerializer(nested=True, required=False, allow_null=True)
     status = ChoiceField(choices=VLANStatusChoices, required=False)
-    role = NestedRoleSerializer(required=False, allow_null=True)
+    role = RoleSerializer(nested=True, required=False, allow_null=True)
     l2vpn_termination = NestedL2VPNTerminationSerializer(read_only=True, allow_null=True)
 
     # Related object counts
@@ -285,23 +285,24 @@ class AvailableVLANSerializer(serializers.Serializer):
     Representation of a VLAN which does not exist in the database.
     """
     vid = serializers.IntegerField(read_only=True)
-    group = NestedVLANGroupSerializer(read_only=True)
+    group = VLANGroupSerializer(nested=True, read_only=True)
 
     def to_representation(self, instance):
         return {
             'vid': instance,
-            'group': NestedVLANGroupSerializer(
+            'group': VLANGroupSerializer(
                 self.context['group'],
+                nested=True,
                 context={'request': self.context['request']}
             ).data,
         }
 
 
 class CreateAvailableVLANSerializer(NetBoxModelSerializer):
-    site = NestedSiteSerializer(required=False, allow_null=True)
-    tenant = NestedTenantSerializer(required=False, allow_null=True)
+    site = SiteSerializer(nested=True, required=False, allow_null=True)
+    tenant = TenantSerializer(nested=True, required=False, allow_null=True)
     status = ChoiceField(choices=VLANStatusChoices, required=False)
-    role = NestedRoleSerializer(required=False, allow_null=True)
+    role = RoleSerializer(nested=True, required=False, allow_null=True)
 
     class Meta:
         model = VLAN
@@ -321,12 +322,12 @@ class CreateAvailableVLANSerializer(NetBoxModelSerializer):
 class PrefixSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:prefix-detail')
     family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
-    site = NestedSiteSerializer(required=False, allow_null=True)
-    vrf = NestedVRFSerializer(required=False, allow_null=True)
-    tenant = NestedTenantSerializer(required=False, allow_null=True)
-    vlan = NestedVLANSerializer(required=False, allow_null=True)
+    site = SiteSerializer(nested=True, required=False, allow_null=True)
+    vrf = VRFSerializer(nested=True, required=False, allow_null=True)
+    tenant = TenantSerializer(nested=True, required=False, allow_null=True)
+    vlan = VLANSerializer(nested=True, required=False, allow_null=True)
     status = ChoiceField(choices=PrefixStatusChoices, required=False)
-    role = NestedRoleSerializer(required=False, allow_null=True)
+    role = RoleSerializer(nested=True, required=False, allow_null=True)
     children = serializers.IntegerField(read_only=True)
     _depth = serializers.IntegerField(read_only=True)
     prefix = IPNetworkField()
@@ -374,11 +375,11 @@ class AvailablePrefixSerializer(serializers.Serializer):
     """
     family = serializers.IntegerField(read_only=True)
     prefix = serializers.CharField(read_only=True)
-    vrf = NestedVRFSerializer(read_only=True)
+    vrf = VRFSerializer(nested=True, read_only=True)
 
     def to_representation(self, instance):
         if self.context.get('vrf'):
-            vrf = NestedVRFSerializer(self.context['vrf'], context={'request': self.context['request']}).data
+            vrf = VRFSerializer(self.context['vrf'], nested=True, context={'request': self.context['request']}).data
         else:
             vrf = None
         return {
@@ -397,10 +398,10 @@ class IPRangeSerializer(NetBoxModelSerializer):
     family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
     start_address = IPAddressField()
     end_address = IPAddressField()
-    vrf = NestedVRFSerializer(required=False, allow_null=True)
-    tenant = NestedTenantSerializer(required=False, allow_null=True)
+    vrf = VRFSerializer(nested=True, required=False, allow_null=True)
+    tenant = TenantSerializer(nested=True, required=False, allow_null=True)
     status = ChoiceField(choices=IPRangeStatusChoices, required=False)
-    role = NestedRoleSerializer(required=False, allow_null=True)
+    role = RoleSerializer(nested=True, required=False, allow_null=True)
 
     class Meta:
         model = IPRange
@@ -420,8 +421,8 @@ class IPAddressSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail')
     family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
     address = IPAddressField()
-    vrf = NestedVRFSerializer(required=False, allow_null=True)
-    tenant = NestedTenantSerializer(required=False, allow_null=True)
+    vrf = VRFSerializer(nested=True, required=False, allow_null=True)
+    tenant = TenantSerializer(nested=True, required=False, allow_null=True)
     status = ChoiceField(choices=IPAddressStatusChoices, required=False)
     role = ChoiceField(choices=IPAddressRoleChoices, allow_blank=True, required=False)
     assigned_object_type = ContentTypeField(
@@ -457,12 +458,12 @@ class AvailableIPSerializer(serializers.Serializer):
     """
     family = serializers.IntegerField(read_only=True)
     address = serializers.CharField(read_only=True)
-    vrf = NestedVRFSerializer(read_only=True)
+    vrf = VRFSerializer(nested=True, read_only=True)
     description = serializers.CharField(required=False)
 
     def to_representation(self, instance):
         if self.context.get('vrf'):
-            vrf = NestedVRFSerializer(self.context['vrf'], context={'request': self.context['request']}).data
+            vrf = VRFSerializer(self.context['vrf'], nested=True, context={'request': self.context['request']}).data
         else:
             vrf = None
         return {
@@ -491,8 +492,8 @@ class ServiceTemplateSerializer(NetBoxModelSerializer):
 
 class ServiceSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:service-detail')
-    device = NestedDeviceSerializer(required=False, allow_null=True)
-    virtual_machine = NestedVirtualMachineSerializer(required=False, allow_null=True)
+    device = DeviceSerializer(nested=True, required=False, allow_null=True)
+    virtual_machine = VirtualMachineSerializer(nested=True, required=False, allow_null=True)
     protocol = ChoiceField(choices=ServiceProtocolChoices, required=False)
     ipaddresses = SerializedPKRelatedField(
         queryset=IPAddress.objects.all(),

+ 23 - 2
netbox/netbox/api/serializers/base.py

@@ -1,8 +1,9 @@
-from django.db.models import ManyToManyField
 from rest_framework import serializers
 from drf_spectacular.utils import extend_schema_field
 from drf_spectacular.types import OpenApiTypes
 
+from utilities.api import get_related_object_by_attrs
+
 __all__ = (
     'BaseModelSerializer',
     'ValidatedModelSerializer',
@@ -12,15 +13,30 @@ __all__ = (
 class BaseModelSerializer(serializers.ModelSerializer):
     display = serializers.SerializerMethodField(read_only=True)
 
-    def __init__(self, *args, requested_fields=None, **kwargs):
+    def __init__(self, *args, nested=False, requested_fields=None, **kwargs):
         super().__init__(*args, **kwargs)
 
+        self.nested = nested
+
+        if nested and not requested_fields:
+            requested_fields = getattr(self.Meta, 'brief_fields', None)
+
         # If specific fields have been requested, omit the others
         if requested_fields:
             for field in list(self.fields.keys()):
                 if field not in requested_fields:
                     self.fields.pop(field)
 
+    def to_internal_value(self, data):
+
+        # If initialized as a nested serializer, we should expect to receive the attrs or PK
+        # identifying a related object.
+        if self.nested:
+            queryset = self.Meta.model.objects.all()
+            return get_related_object_by_attrs(queryset, data)
+
+        return super().to_internal_value(data)
+
     @extend_schema_field(OpenApiTypes.STR)
     def get_display(self, obj):
         return str(obj)
@@ -32,6 +48,11 @@ class ValidatedModelSerializer(BaseModelSerializer):
     validation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)
     """
     def validate(self, data):
+
+        # Skip validation if we're being used to represent a nested object
+        if self.nested:
+            return data
+
         attrs = data.copy()
 
         # Remove custom field data (if any) prior to model validation

+ 3 - 41
netbox/netbox/api/serializers/nested.py

@@ -1,10 +1,7 @@
-from django.core.exceptions import FieldError, MultipleObjectsReturned, ObjectDoesNotExist
-from django.utils.translation import gettext_lazy as _
 from rest_framework import serializers
-from rest_framework.exceptions import ValidationError
 
 from extras.models import Tag
-from utilities.utils import dict_to_filter_params
+from utilities.api import get_related_object_by_attrs
 from .base import BaseModelSerializer
 
 __all__ = (
@@ -20,43 +17,8 @@ class WritableNestedSerializer(BaseModelSerializer):
     subclassed to return a full representation of the related object on read.
     """
     def to_internal_value(self, data):
-
-        if data is None:
-            return None
-
-        # Dictionary of related object attributes
-        if isinstance(data, dict):
-            params = dict_to_filter_params(data)
-            queryset = self.Meta.model.objects
-            try:
-                return queryset.get(**params)
-            except ObjectDoesNotExist:
-                raise ValidationError(
-                    _("Related object not found using the provided attributes: {params}").format(params=params))
-            except MultipleObjectsReturned:
-                raise ValidationError(
-                    _("Multiple objects match the provided attributes: {params}").format(params=params)
-                )
-            except FieldError as e:
-                raise ValidationError(e)
-
-        # Integer PK of related object
-        try:
-            # Cast as integer in case a PK was mistakenly sent as a string
-            pk = int(data)
-        except (TypeError, ValueError):
-            raise ValidationError(
-                _(
-                    "Related objects must be referenced by numeric ID or by dictionary of attributes. Received an "
-                    "unrecognized value: {value}"
-                ).format(value=data)
-            )
-
-        # Look up object by PK
-        try:
-            return self.Meta.model.objects.get(pk=pk)
-        except ObjectDoesNotExist:
-            raise ValidationError(_("Related object not found using the provided numeric ID: {id}").format(id=pk))
+        queryset = self.Meta.model.objects.all()
+        return get_related_object_by_attrs(queryset, data)
 
 
 # Declared here for use by PrimaryModelSerializer, but should be imported from extras.api.nested_serializers

+ 4 - 4
netbox/tenancy/api/serializers.py

@@ -32,7 +32,7 @@ class TenantGroupSerializer(NestedGroupModelSerializer):
 
 class TenantSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenant-detail')
-    group = NestedTenantGroupSerializer(required=False, allow_null=True)
+    group = TenantGroupSerializer(nested=True, required=False, allow_null=True)
 
     # Related object counts
     circuit_count = RelatedObjectCountField('circuits')
@@ -87,7 +87,7 @@ class ContactRoleSerializer(NetBoxModelSerializer):
 
 class ContactSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contact-detail')
-    group = NestedContactGroupSerializer(required=False, allow_null=True, default=None)
+    group = ContactGroupSerializer(nested=True, required=False, allow_null=True, default=None)
 
     class Meta:
         model = Contact
@@ -104,8 +104,8 @@ class ContactAssignmentSerializer(NetBoxModelSerializer):
         queryset=ContentType.objects.all()
     )
     object = serializers.SerializerMethodField(read_only=True)
-    contact = NestedContactSerializer()
-    role = NestedContactRoleSerializer(required=False, allow_null=True)
+    contact = ContactSerializer(nested=True)
+    role = ContactRoleSerializer(nested=True, required=False, allow_null=True)
     priority = ChoiceField(choices=ContactPriorityChoices, allow_blank=True, required=False, default=lambda: '')
 
     class Meta:

+ 3 - 2
netbox/users/api/serializers.py

@@ -89,7 +89,7 @@ class TokenSerializer(ValidatedModelSerializer):
         required=False,
         write_only=not settings.ALLOW_TOKEN_RETRIEVAL
     )
-    user = NestedUserSerializer()
+    user = UserSerializer(nested=True)
     allowed_ips = serializers.ListField(
         child=IPNetworkSerializer(),
         required=False,
@@ -122,7 +122,8 @@ class TokenSerializer(ValidatedModelSerializer):
 
 
 class TokenProvisionSerializer(TokenSerializer):
-    user = NestedUserSerializer(
+    user = UserSerializer(
+        nested=True,
         read_only=True
     )
     username = serializers.CharField(

+ 52 - 5
netbox/utilities/api.py

@@ -3,23 +3,26 @@ import sys
 
 from django.conf import settings
 from django.contrib.contenttypes.fields import GenericForeignKey
-from django.core.exceptions import FieldDoesNotExist
+from django.core.exceptions import (
+    FieldDoesNotExist, FieldError, MultipleObjectsReturned, ObjectDoesNotExist, ValidationError,
+)
 from django.db.models.fields.related import ManyToOneRel, RelatedField
 from django.http import JsonResponse
 from django.urls import reverse
+from django.utils.translation import gettext_lazy as _
 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
+from .utils import count_related, dict_to_filter_params, dynamic_import
 
 __all__ = (
     'get_annotations_for_serializer',
     'get_graphql_type_for_model',
     'get_prefetches_for_serializer',
+    'get_related_object_by_attrs',
     'get_serializer_for_model',
     'get_view_name',
     'is_api_request',
@@ -103,7 +106,7 @@ def get_prefetches_for_serializer(serializer_class, fields_to_include=None):
     """
     model = serializer_class.Meta.model
 
-    # If specific fields are not specified, default to all
+    # If fields are not specified, default to all
     if not fields_to_include:
         fields_to_include = serializer_class.Meta.fields
 
@@ -128,7 +131,9 @@ def get_prefetches_for_serializer(serializer_class, fields_to_include=None):
         # for the related object.
         if serializer_field:
             if issubclass(type(serializer_field), Serializer):
-                for subfield in get_prefetches_for_serializer(type(serializer_field)):
+                # Determine which fields to prefetch for the nested object
+                subfields = serializer_field.Meta.brief_fields if serializer_field.nested else None
+                for subfield in get_prefetches_for_serializer(type(serializer_field), subfields):
                     prefetch_fields.append(f'{field_name}__{subfield}')
 
     return prefetch_fields
@@ -154,6 +159,48 @@ def get_annotations_for_serializer(serializer_class, fields_to_include=None):
     return annotations
 
 
+def get_related_object_by_attrs(queryset, attrs):
+    """
+    Return an object identified by either a dictionary of attributes or its numeric primary key (ID). This is used
+    for referencing related objects when creating/updating objects via the REST API.
+    """
+    if attrs is None:
+        return None
+
+    # Dictionary of related object attributes
+    if isinstance(attrs, dict):
+        params = dict_to_filter_params(attrs)
+        try:
+            return queryset.get(**params)
+        except ObjectDoesNotExist:
+            raise ValidationError(
+                _("Related object not found using the provided attributes: {params}").format(params=params))
+        except MultipleObjectsReturned:
+            raise ValidationError(
+                _("Multiple objects match the provided attributes: {params}").format(params=params)
+            )
+        except FieldError as e:
+            raise ValidationError(e)
+
+    # Integer PK of related object
+    try:
+        # Cast as integer in case a PK was mistakenly sent as a string
+        pk = int(attrs)
+    except (TypeError, ValueError):
+        raise ValidationError(
+            _(
+                "Related objects must be referenced by numeric ID or by dictionary of attributes. Received an "
+                "unrecognized value: {value}"
+            ).format(value=attrs)
+        )
+
+    # Look up object by PK
+    try:
+        return queryset.get(pk=pk)
+    except ObjectDoesNotExist:
+        raise ValidationError(_("Related object not found using the provided numeric ID: {id}").format(id=pk))
+
+
 def rest_api_server_error(request, *args, **kwargs):
     """
     Handle exceptions and return a useful error message for REST API requests.

+ 16 - 18
netbox/virtualization/api/serializers.py

@@ -1,16 +1,14 @@
 from drf_spectacular.utils import extend_schema_field
 from rest_framework import serializers
 
-from dcim.api.nested_serializers import (
-    NestedDeviceSerializer, NestedDeviceRoleSerializer, NestedPlatformSerializer, NestedSiteSerializer,
-)
+from dcim.api.serializers import DeviceSerializer, DeviceRoleSerializer, PlatformSerializer, SiteSerializer
 from dcim.choices import InterfaceModeChoices
-from extras.api.nested_serializers import NestedConfigTemplateSerializer
+from extras.api.serializers import ConfigTemplateSerializer
 from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer, NestedVRFSerializer
 from ipam.models import VLAN
 from netbox.api.fields import ChoiceField, RelatedObjectCountField, SerializedPKRelatedField
 from netbox.api.serializers import NetBoxModelSerializer
-from tenancy.api.nested_serializers import NestedTenantSerializer
+from tenancy.api.serializers import TenantSerializer
 from virtualization.choices import *
 from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualDisk, VirtualMachine, VMInterface
 from vpn.api.nested_serializers import NestedL2VPNTerminationSerializer
@@ -53,11 +51,11 @@ class ClusterGroupSerializer(NetBoxModelSerializer):
 
 class ClusterSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:cluster-detail')
-    type = NestedClusterTypeSerializer()
-    group = NestedClusterGroupSerializer(required=False, allow_null=True, default=None)
+    type = ClusterTypeSerializer(nested=True)
+    group = ClusterGroupSerializer(nested=True, required=False, allow_null=True, default=None)
     status = ChoiceField(choices=ClusterStatusChoices, required=False)
-    tenant = NestedTenantSerializer(required=False, allow_null=True)
-    site = NestedSiteSerializer(required=False, allow_null=True, default=None)
+    tenant = TenantSerializer(nested=True, required=False, allow_null=True)
+    site = SiteSerializer(nested=True, required=False, allow_null=True, default=None)
 
     # Related object counts
     device_count = RelatedObjectCountField('devices')
@@ -79,16 +77,16 @@ class ClusterSerializer(NetBoxModelSerializer):
 class VirtualMachineSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:virtualmachine-detail')
     status = ChoiceField(choices=VirtualMachineStatusChoices, required=False)
-    site = NestedSiteSerializer(required=False, allow_null=True)
-    cluster = NestedClusterSerializer(required=False, allow_null=True)
-    device = NestedDeviceSerializer(required=False, allow_null=True)
-    role = NestedDeviceRoleSerializer(required=False, allow_null=True)
-    tenant = NestedTenantSerializer(required=False, allow_null=True)
-    platform = NestedPlatformSerializer(required=False, allow_null=True)
+    site = SiteSerializer(nested=True, required=False, allow_null=True)
+    cluster = ClusterSerializer(nested=True, required=False, allow_null=True)
+    device = DeviceSerializer(nested=True, required=False, allow_null=True)
+    role = DeviceRoleSerializer(nested=True, required=False, allow_null=True)
+    tenant = TenantSerializer(nested=True, required=False, allow_null=True)
+    platform = PlatformSerializer(nested=True, required=False, allow_null=True)
     primary_ip = NestedIPAddressSerializer(read_only=True)
     primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True)
     primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True)
-    config_template = NestedConfigTemplateSerializer(required=False, allow_null=True, default=None)
+    config_template = ConfigTemplateSerializer(nested=True, required=False, allow_null=True, default=None)
 
     # Counter fields
     interface_count = serializers.IntegerField(read_only=True)
@@ -128,7 +126,7 @@ class VirtualMachineWithConfigContextSerializer(VirtualMachineSerializer):
 
 class VMInterfaceSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:vminterface-detail')
-    virtual_machine = NestedVirtualMachineSerializer()
+    virtual_machine = VirtualMachineSerializer(nested=True)
     parent = NestedVMInterfaceSerializer(required=False, allow_null=True)
     bridge = NestedVMInterfaceSerializer(required=False, allow_null=True)
     mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False)
@@ -178,7 +176,7 @@ class VMInterfaceSerializer(NetBoxModelSerializer):
 
 class VirtualDiskSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:virtualdisk-detail')
-    virtual_machine = NestedVirtualMachineSerializer()
+    virtual_machine = VirtualMachineSerializer(nested=True)
 
     class Meta:
         model = VirtualDisk

+ 107 - 90
netbox/vpn/api/serializers.py

@@ -2,12 +2,13 @@ from django.contrib.contenttypes.models import ContentType
 from drf_spectacular.utils import extend_schema_field
 from rest_framework import serializers
 
-from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedRouteTargetSerializer
+from ipam.api.serializers import IPAddressSerializer
+from ipam.api.nested_serializers import NestedRouteTargetSerializer
 from ipam.models import RouteTarget
 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
+from tenancy.api.serializers import TenantSerializer
 from utilities.api import get_serializer_for_model
 from vpn.choices import *
 from vpn.models import *
@@ -27,90 +28,6 @@ __all__ = (
 )
 
 
-class TunnelGroupSerializer(NetBoxModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='vpn-api:tunnelgroup-detail')
-
-    # Related object counts
-    tunnel_count = RelatedObjectCountField('tunnels')
-
-    class Meta:
-        model = TunnelGroup
-        fields = [
-            'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated',
-            'tunnel_count',
-        ]
-        brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'tunnel_count')
-
-
-class TunnelSerializer(NetBoxModelSerializer):
-    url = serializers.HyperlinkedIdentityField(
-        view_name='vpn-api:tunnel-detail'
-    )
-    status = ChoiceField(
-        choices=TunnelStatusChoices
-    )
-    group = NestedTunnelGroupSerializer(
-        required=False,
-        allow_null=True
-    )
-    encapsulation = ChoiceField(
-        choices=TunnelEncapsulationChoices
-    )
-    ipsec_profile = NestedIPSecProfileSerializer(
-        required=False,
-        allow_null=True
-    )
-    tenant = NestedTenantSerializer(
-        required=False,
-        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', 'terminations_count',
-        )
-        brief_fields = ('id', 'url', 'display', 'name', 'description')
-
-
-class TunnelTerminationSerializer(NetBoxModelSerializer):
-    url = serializers.HyperlinkedIdentityField(
-        view_name='vpn-api:tunneltermination-detail'
-    )
-    tunnel = NestedTunnelSerializer()
-    role = ChoiceField(
-        choices=TunnelTerminationRoleChoices
-    )
-    termination_type = ContentTypeField(
-        queryset=ContentType.objects.all()
-    )
-    termination = serializers.SerializerMethodField(
-        read_only=True
-    )
-    outside_ip = NestedIPAddressSerializer(
-        required=False,
-        allow_null=True
-    )
-
-    class Meta:
-        model = TunnelTermination
-        fields = (
-            'id', 'url', 'display', 'tunnel', 'role', 'termination_type', 'termination_id', 'termination', 'outside_ip',
-            'tags', 'custom_fields', 'created', 'last_updated',
-        )
-        brief_fields = ('id', 'url', 'display')
-
-    @extend_schema_field(serializers.JSONField(allow_null=True))
-    def get_termination(self, obj):
-        serializer = get_serializer_for_model(obj.termination, prefix=NESTED_SERIALIZER_PREFIX)
-        context = {'request': self.context['request']}
-        return serializer(obj.termination, context=context).data
-
-
 class IKEProposalSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(
         view_name='vpn-api:ikeproposal-detail'
@@ -215,8 +132,12 @@ class IPSecProfileSerializer(NetBoxModelSerializer):
     mode = ChoiceField(
         choices=IPSecModeChoices
     )
-    ike_policy = NestedIKEPolicySerializer()
-    ipsec_policy = NestedIPSecPolicySerializer()
+    ike_policy = IKEPolicySerializer(
+        nested=True
+    )
+    ipsec_policy = IPSecPolicySerializer(
+        nested=True
+    )
 
     class Meta:
         model = IPSecProfile
@@ -227,6 +148,100 @@ class IPSecProfileSerializer(NetBoxModelSerializer):
         brief_fields = ('id', 'url', 'display', 'name', 'description')
 
 
+#
+# Tunnels
+#
+
+class TunnelGroupSerializer(NetBoxModelSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='vpn-api:tunnelgroup-detail')
+
+    # Related object counts
+    tunnel_count = RelatedObjectCountField('tunnels')
+
+    class Meta:
+        model = TunnelGroup
+        fields = [
+            'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated',
+            'tunnel_count',
+        ]
+        brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'tunnel_count')
+
+
+class TunnelSerializer(NetBoxModelSerializer):
+    url = serializers.HyperlinkedIdentityField(
+        view_name='vpn-api:tunnel-detail'
+    )
+    status = ChoiceField(
+        choices=TunnelStatusChoices
+    )
+    group = TunnelGroupSerializer(
+        nested=True,
+        required=False,
+        allow_null=True
+    )
+    encapsulation = ChoiceField(
+        choices=TunnelEncapsulationChoices
+    )
+    ipsec_profile = IPSecProfileSerializer(
+        nested=True,
+        required=False,
+        allow_null=True
+    )
+    tenant = TenantSerializer(
+        nested=True,
+        required=False,
+        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', 'terminations_count',
+        )
+        brief_fields = ('id', 'url', 'display', 'name', 'description')
+
+
+class TunnelTerminationSerializer(NetBoxModelSerializer):
+    url = serializers.HyperlinkedIdentityField(
+        view_name='vpn-api:tunneltermination-detail'
+    )
+    tunnel = TunnelSerializer(
+        nested=True
+    )
+    role = ChoiceField(
+        choices=TunnelTerminationRoleChoices
+    )
+    termination_type = ContentTypeField(
+        queryset=ContentType.objects.all()
+    )
+    termination = serializers.SerializerMethodField(
+        read_only=True
+    )
+    outside_ip = IPAddressSerializer(
+        nested=True,
+        required=False,
+        allow_null=True
+    )
+
+    class Meta:
+        model = TunnelTermination
+        fields = (
+            'id', 'url', 'display', 'tunnel', 'role', 'termination_type', 'termination_id', 'termination', 'outside_ip',
+            'tags', 'custom_fields', 'created', 'last_updated',
+        )
+        brief_fields = ('id', 'url', 'display')
+
+    @extend_schema_field(serializers.JSONField(allow_null=True))
+    def get_termination(self, obj):
+        serializer = get_serializer_for_model(obj.termination, prefix=NESTED_SERIALIZER_PREFIX)
+        context = {'request': self.context['request']}
+        return serializer(obj.termination, context=context).data
+
+
 #
 # L2VPN
 #
@@ -246,7 +261,7 @@ class L2VPNSerializer(NetBoxModelSerializer):
         required=False,
         many=True
     )
-    tenant = NestedTenantSerializer(required=False, allow_null=True)
+    tenant = TenantSerializer(nested=True, required=False, allow_null=True)
 
     class Meta:
         model = L2VPN
@@ -259,7 +274,9 @@ class L2VPNSerializer(NetBoxModelSerializer):
 
 class L2VPNTerminationSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='vpn-api:l2vpntermination-detail')
-    l2vpn = NestedL2VPNSerializer()
+    l2vpn = L2VPNSerializer(
+        nested=True
+    )
     assigned_object_type = ContentTypeField(
         queryset=ContentType.objects.all()
     )

+ 9 - 9
netbox/wireless/api/serializers.py

@@ -1,11 +1,11 @@
 from rest_framework import serializers
 
 from dcim.choices import LinkStatusChoices
-from dcim.api.serializers import NestedInterfaceSerializer
-from ipam.api.serializers import NestedVLANSerializer
+from dcim.api.serializers import InterfaceSerializer
+from ipam.api.serializers import VLANSerializer
 from netbox.api.fields import ChoiceField
 from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer
-from tenancy.api.nested_serializers import NestedTenantSerializer
+from tenancy.api.serializers import TenantSerializer
 from wireless.choices import *
 from wireless.models import *
 from .nested_serializers import *
@@ -33,10 +33,10 @@ class WirelessLANGroupSerializer(NestedGroupModelSerializer):
 
 class WirelessLANSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslan-detail')
-    group = NestedWirelessLANGroupSerializer(required=False, allow_null=True)
+    group = WirelessLANGroupSerializer(nested=True, required=False, allow_null=True)
     status = ChoiceField(choices=WirelessLANStatusChoices, required=False, allow_blank=True)
-    vlan = NestedVLANSerializer(required=False, allow_null=True)
-    tenant = NestedTenantSerializer(required=False, allow_null=True)
+    vlan = VLANSerializer(nested=True, required=False, allow_null=True)
+    tenant = TenantSerializer(nested=True, required=False, allow_null=True)
     auth_type = ChoiceField(choices=WirelessAuthTypeChoices, required=False, allow_blank=True)
     auth_cipher = ChoiceField(choices=WirelessAuthCipherChoices, required=False, allow_blank=True)
 
@@ -52,9 +52,9 @@ class WirelessLANSerializer(NetBoxModelSerializer):
 class WirelessLinkSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslink-detail')
     status = ChoiceField(choices=LinkStatusChoices, required=False)
-    interface_a = NestedInterfaceSerializer()
-    interface_b = NestedInterfaceSerializer()
-    tenant = NestedTenantSerializer(required=False, allow_null=True)
+    interface_a = InterfaceSerializer(nested=True)
+    interface_b = InterfaceSerializer(nested=True)
+    tenant = TenantSerializer(nested=True, required=False, allow_null=True)
     auth_type = ChoiceField(choices=WirelessAuthTypeChoices, required=False, allow_blank=True)
     auth_cipher = ChoiceField(choices=WirelessAuthCipherChoices, required=False, allow_blank=True)
 

Некоторые файлы не были показаны из-за большого количества измененных файлов