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

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.choices import CircuitStatusChoices
 from circuits.models import *
 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.api.nested_serializers import NestedASNSerializer
 from ipam.models import ASN
 from ipam.models import ASN
 from netbox.api.fields import ChoiceField, RelatedObjectCountField, SerializedPKRelatedField
 from netbox.api.fields import ChoiceField, RelatedObjectCountField, SerializedPKRelatedField
 from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer
 from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer
-from tenancy.api.nested_serializers import NestedTenantSerializer
+from tenancy.api.serializers import TenantSerializer
 from .nested_serializers import *
 from .nested_serializers import *
 
 
 
 
@@ -49,7 +48,7 @@ class ProviderSerializer(NetBoxModelSerializer):
 
 
 class ProviderAccountSerializer(NetBoxModelSerializer):
 class ProviderAccountSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provideraccount-detail')
     url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provideraccount-detail')
-    provider = NestedProviderSerializer()
+    provider = ProviderSerializer(nested=True)
 
 
     class Meta:
     class Meta:
         model = ProviderAccount
         model = ProviderAccount
@@ -66,7 +65,7 @@ class ProviderAccountSerializer(NetBoxModelSerializer):
 
 
 class ProviderNetworkSerializer(NetBoxModelSerializer):
 class ProviderNetworkSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='circuits-api:providernetwork-detail')
     url = serializers.HyperlinkedIdentityField(view_name='circuits-api:providernetwork-detail')
-    provider = NestedProviderSerializer()
+    provider = ProviderSerializer(nested=True)
 
 
     class Meta:
     class Meta:
         model = ProviderNetwork
         model = ProviderNetwork
@@ -98,8 +97,8 @@ class CircuitTypeSerializer(NetBoxModelSerializer):
 
 
 class CircuitCircuitTerminationSerializer(WritableNestedSerializer):
 class CircuitCircuitTerminationSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
     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:
     class Meta:
         model = CircuitTermination
         model = CircuitTermination
@@ -111,11 +110,11 @@ class CircuitCircuitTerminationSerializer(WritableNestedSerializer):
 
 
 class CircuitSerializer(NetBoxModelSerializer):
 class CircuitSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
     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)
     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_a = CircuitCircuitTerminationSerializer(read_only=True, allow_null=True)
     termination_z = 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):
 class CircuitTerminationSerializer(NetBoxModelSerializer, CabledObjectSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
     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:
     class Meta:
         model = CircuitTermination
         model = CircuitTermination

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

@@ -4,7 +4,7 @@ from core.choices import JobStatusChoices
 from core.models import *
 from core.models import *
 from netbox.api.fields import ChoiceField
 from netbox.api.fields import ChoiceField
 from netbox.api.serializers import WritableNestedSerializer
 from netbox.api.serializers import WritableNestedSerializer
-from users.api.nested_serializers import NestedUserSerializer
+from users.api.serializers import UserSerializer
 
 
 __all__ = (
 __all__ = (
     'NestedDataFileSerializer',
     'NestedDataFileSerializer',
@@ -32,7 +32,8 @@ class NestedDataFileSerializer(WritableNestedSerializer):
 class NestedJobSerializer(serializers.ModelSerializer):
 class NestedJobSerializer(serializers.ModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='core-api:job-detail')
     url = serializers.HyperlinkedIdentityField(view_name='core-api:job-detail')
     status = ChoiceField(choices=JobStatusChoices)
     status = ChoiceField(choices=JobStatusChoices)
-    user = NestedUserSerializer(
+    user = UserSerializer(
+        nested=True,
         read_only=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.fields import ChoiceField, ContentTypeField, RelatedObjectCountField
 from netbox.api.serializers import BaseModelSerializer, NetBoxModelSerializer
 from netbox.api.serializers import BaseModelSerializer, NetBoxModelSerializer
 from netbox.utils import get_data_backend_choices
 from netbox.utils import get_data_backend_choices
-from users.api.nested_serializers import NestedUserSerializer
-from .nested_serializers import *
+from users.api.serializers import UserSerializer
 
 
 __all__ = (
 __all__ = (
     'DataFileSerializer',
     'DataFileSerializer',
@@ -43,7 +42,8 @@ class DataFileSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(
     url = serializers.HyperlinkedIdentityField(
         view_name='core-api:datafile-detail'
         view_name='core-api:datafile-detail'
     )
     )
-    source = NestedDataSourceSerializer(
+    source = DataSourceSerializer(
+        nested=True,
         read_only=True
         read_only=True
     )
     )
 
 
@@ -57,7 +57,8 @@ class DataFileSerializer(NetBoxModelSerializer):
 
 
 class JobSerializer(BaseModelSerializer):
 class JobSerializer(BaseModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='core-api:job-detail')
     url = serializers.HyperlinkedIdentityField(view_name='core-api:job-detail')
-    user = NestedUserSerializer(
+    user = UserSerializer(
+        nested=True,
         read_only=True
         read_only=True
     )
     )
     status = ChoiceField(choices=JobStatusChoices, 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
 from netbox.api.serializers import WritableNestedSerializer
 
 
 __all__ = [
 __all__ = [
-    'ComponentNestedModuleSerializer',
-    'ModuleBayNestedModuleSerializer',
     'NestedCableSerializer',
     'NestedCableSerializer',
     'NestedConsolePortSerializer',
     'NestedConsolePortSerializer',
     'NestedConsolePortTemplateSerializer',
     'NestedConsolePortTemplateSerializer',
@@ -311,26 +309,6 @@ class ModuleNestedModuleBaySerializer(WritableNestedSerializer):
         fields = ['id', 'url', 'display', 'name']
         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):
 class NestedModuleSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail')
     device = NestedDeviceSerializer(read_only=True)
     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 rest_framework.status import HTTP_400_BAD_REQUEST
 
 
 from netbox.api.renderers import TextRenderer
 from netbox.api.renderers import TextRenderer
-from .nested_serializers import NestedConfigTemplateSerializer
+from .serializers import ConfigTemplateSerializer
 
 
 __all__ = (
 __all__ = (
     'ConfigContextQuerySetMixin',
     'ConfigContextQuerySetMixin',
@@ -52,7 +52,7 @@ class ConfigTemplateRenderMixin:
         if request.accepted_renderer.format == 'txt':
         if request.accepted_renderer.format == 'txt':
             return Response(output)
             return Response(output)
 
 
-        template_serializer = NestedConfigTemplateSerializer(configtemplate, context={'request': request})
+        template_serializer = ConfigTemplateSerializer(configtemplate, nested=True, context={'request': request})
 
 
         return Response({
         return Response({
             'configtemplate': template_serializer.data,
             '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 drf_spectacular.utils import extend_schema_field
 from rest_framework import serializers
 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 core.models import ContentType
 from dcim.api.nested_serializers import (
 from dcim.api.nested_serializers import (
     NestedDeviceRoleSerializer, NestedDeviceTypeSerializer, NestedLocationSerializer, NestedPlatformSerializer,
     NestedDeviceRoleSerializer, NestedDeviceTypeSerializer, NestedLocationSerializer, NestedPlatformSerializer,
@@ -22,7 +21,7 @@ from netbox.api.serializers.features import TaggableModelSerializer
 from netbox.constants import NESTED_SERIALIZER_PREFIX
 from netbox.constants import NESTED_SERIALIZER_PREFIX
 from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantGroupSerializer
 from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantGroupSerializer
 from tenancy.models import Tenant, TenantGroup
 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 utilities.api import get_serializer_for_model
 from virtualization.api.nested_serializers import (
 from virtualization.api.nested_serializers import (
     NestedClusterGroupSerializer, NestedClusterSerializer, NestedClusterTypeSerializer,
     NestedClusterGroupSerializer, NestedClusterSerializer, NestedClusterTypeSerializer,
@@ -115,6 +114,28 @@ class WebhookSerializer(NetBoxModelSerializer):
 # Custom fields
 # 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):
 class CustomFieldSerializer(ValidatedModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfield-detail')
     url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfield-detail')
     content_types = ContentTypeField(
     content_types = ContentTypeField(
@@ -129,7 +150,8 @@ class CustomFieldSerializer(ValidatedModelSerializer):
     )
     )
     filter_logic = ChoiceField(choices=CustomFieldFilterLogicChoices, required=False)
     filter_logic = ChoiceField(choices=CustomFieldFilterLogicChoices, required=False)
     data_type = serializers.SerializerMethodField()
     data_type = serializers.SerializerMethodField()
-    choice_set = NestedCustomFieldChoiceSetSerializer(
+    choice_set = CustomFieldChoiceSetSerializer(
+        nested=True,
         required=False,
         required=False,
         allow_null=True
         allow_null=True
     )
     )
@@ -168,28 +190,6 @@ class CustomFieldSerializer(ValidatedModelSerializer):
         return 'string'
         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
 # Custom links
 #
 #
@@ -220,10 +220,12 @@ class ExportTemplateSerializer(ValidatedModelSerializer):
         queryset=ContentType.objects.with_feature('export_templates'),
         queryset=ContentType.objects.with_feature('export_templates'),
         many=True
         many=True
     )
     )
-    data_source = NestedDataSourceSerializer(
+    data_source = DataSourceSerializer(
+        nested=True,
         required=False
         required=False
     )
     )
-    data_file = NestedDataFileSerializer(
+    data_file = DataFileSerializer(
+        nested=True,
         read_only=True
         read_only=True
     )
     )
 
 
@@ -267,7 +269,7 @@ class BookmarkSerializer(ValidatedModelSerializer):
         queryset=ContentType.objects.with_feature('bookmarks'),
         queryset=ContentType.objects.with_feature('bookmarks'),
     )
     )
     object = serializers.SerializerMethodField(read_only=True)
     object = serializers.SerializerMethodField(read_only=True)
-    user = NestedUserSerializer()
+    user = UserSerializer(nested=True)
 
 
     class Meta:
     class Meta:
         model = Bookmark
         model = Bookmark
@@ -482,10 +484,12 @@ class ConfigContextSerializer(ValidatedModelSerializer):
         required=False,
         required=False,
         many=True
         many=True
     )
     )
-    data_source = NestedDataSourceSerializer(
+    data_source = DataSourceSerializer(
+        nested=True,
         required=False
         required=False
     )
     )
-    data_file = NestedDataFileSerializer(
+    data_file = DataFileSerializer(
+        nested=True,
         read_only=True
         read_only=True
     )
     )
 
 
@@ -506,10 +510,12 @@ class ConfigContextSerializer(ValidatedModelSerializer):
 
 
 class ConfigTemplateSerializer(TaggableModelSerializer, ValidatedModelSerializer):
 class ConfigTemplateSerializer(TaggableModelSerializer, ValidatedModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='extras-api:configtemplate-detail')
     url = serializers.HyperlinkedIdentityField(view_name='extras-api:configtemplate-detail')
-    data_source = NestedDataSourceSerializer(
+    data_source = DataSourceSerializer(
+        nested=True,
         required=False
         required=False
     )
     )
-    data_file = NestedDataFileSerializer(
+    data_file = DataFileSerializer(
+        nested=True,
         required=False
         required=False
     )
     )
 
 
@@ -530,7 +536,7 @@ class ScriptSerializer(ValidatedModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='extras-api:script-detail')
     url = serializers.HyperlinkedIdentityField(view_name='extras-api:script-detail')
     description = serializers.SerializerMethodField(read_only=True)
     description = serializers.SerializerMethodField(read_only=True)
     vars = 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:
     class Meta:
         model = Script
         model = Script
@@ -596,7 +602,8 @@ class ScriptInputSerializer(serializers.Serializer):
 
 
 class ObjectChangeSerializer(BaseModelSerializer):
 class ObjectChangeSerializer(BaseModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='extras-api:objectchange-detail')
     url = serializers.HyperlinkedIdentityField(view_name='extras-api:objectchange-detail')
-    user = NestedUserSerializer(
+    user = UserSerializer(
+        nested=True,
         read_only=True
         read_only=True
     )
     )
     action = ChoiceField(
     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 drf_spectacular.utils import extend_schema_field
 from rest_framework import serializers
 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.choices import *
 from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS, VLANGROUP_SCOPE_TYPES
 from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS, VLANGROUP_SCOPE_TYPES
 from ipam.models import *
 from ipam.models import *
 from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField, SerializedPKRelatedField
 from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField, SerializedPKRelatedField
 from netbox.api.serializers import NetBoxModelSerializer
 from netbox.api.serializers import NetBoxModelSerializer
 from netbox.constants import NESTED_SERIALIZER_PREFIX
 from netbox.constants import NESTED_SERIALIZER_PREFIX
-from tenancy.api.nested_serializers import NestedTenantSerializer
+from tenancy.api.serializers import TenantSerializer
 from utilities.api import get_serializer_for_model
 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 vpn.api.nested_serializers import NestedL2VPNTerminationSerializer
 from .field_serializers import IPAddressField, IPNetworkField
 from .field_serializers import IPAddressField, IPNetworkField
 from .nested_serializers import *
 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
 # ASN ranges
 #
 #
 
 
 class ASNRangeSerializer(NetBoxModelSerializer):
 class ASNRangeSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:asnrange-detail')
     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)
     asn_count = serializers.IntegerField(read_only=True)
 
 
     class Meta:
     class Meta:
@@ -42,8 +61,8 @@ class ASNRangeSerializer(NetBoxModelSerializer):
 
 
 class ASNSerializer(NetBoxModelSerializer):
 class ASNSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:asn-detail')
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:asn-detail')
-    rir = NestedRIRSerializer(required=False, allow_null=True)
-    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
     # Related object counts
     site_count = RelatedObjectCountField('sites')
     site_count = RelatedObjectCountField('sites')
@@ -66,7 +85,7 @@ class AvailableASNSerializer(serializers.Serializer):
     description = serializers.CharField(required=False)
     description = serializers.CharField(required=False)
 
 
     def to_representation(self, asn):
     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']
             'request': self.context['request']
         }).data
         }).data
         return {
         return {
@@ -81,7 +100,7 @@ class AvailableASNSerializer(serializers.Serializer):
 
 
 class VRFSerializer(NetBoxModelSerializer):
 class VRFSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vrf-detail')
     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(
     import_targets = SerializedPKRelatedField(
         queryset=RouteTarget.objects.all(),
         queryset=RouteTarget.objects.all(),
         serializer=NestedRouteTargetSerializer,
         serializer=NestedRouteTargetSerializer,
@@ -109,13 +128,9 @@ class VRFSerializer(NetBoxModelSerializer):
         brief_fields = ('id', 'url', 'display', 'name', 'rd', 'description', 'prefix_count')
         brief_fields = ('id', 'url', 'display', 'name', 'rd', 'description', 'prefix_count')
 
 
 
 
-#
-# Route targets
-#
-
 class RouteTargetSerializer(NetBoxModelSerializer):
 class RouteTargetSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:routetarget-detail')
     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:
     class Meta:
         model = RouteTarget
         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):
 class AggregateSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:aggregate-detail')
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:aggregate-detail')
     family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
     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()
     prefix = IPNetworkField()
 
 
     class Meta:
     class Meta:
@@ -180,7 +180,7 @@ class FHRPGroupSerializer(NetBoxModelSerializer):
 
 
 class FHRPGroupAssignmentSerializer(NetBoxModelSerializer):
 class FHRPGroupAssignmentSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:fhrpgroupassignment-detail')
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:fhrpgroupassignment-detail')
-    group = NestedFHRPGroupSerializer()
+    group = FHRPGroupSerializer(nested=True)
     interface_type = ContentTypeField(
     interface_type = ContentTypeField(
         queryset=ContentType.objects.all()
         queryset=ContentType.objects.all()
     )
     )
@@ -261,11 +261,11 @@ class VLANGroupSerializer(NetBoxModelSerializer):
 
 
 class VLANSerializer(NetBoxModelSerializer):
 class VLANSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail')
     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)
     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)
     l2vpn_termination = NestedL2VPNTerminationSerializer(read_only=True, allow_null=True)
 
 
     # Related object counts
     # Related object counts
@@ -285,23 +285,24 @@ class AvailableVLANSerializer(serializers.Serializer):
     Representation of a VLAN which does not exist in the database.
     Representation of a VLAN which does not exist in the database.
     """
     """
     vid = serializers.IntegerField(read_only=True)
     vid = serializers.IntegerField(read_only=True)
-    group = NestedVLANGroupSerializer(read_only=True)
+    group = VLANGroupSerializer(nested=True, read_only=True)
 
 
     def to_representation(self, instance):
     def to_representation(self, instance):
         return {
         return {
             'vid': instance,
             'vid': instance,
-            'group': NestedVLANGroupSerializer(
+            'group': VLANGroupSerializer(
                 self.context['group'],
                 self.context['group'],
+                nested=True,
                 context={'request': self.context['request']}
                 context={'request': self.context['request']}
             ).data,
             ).data,
         }
         }
 
 
 
 
 class CreateAvailableVLANSerializer(NetBoxModelSerializer):
 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)
     status = ChoiceField(choices=VLANStatusChoices, required=False)
-    role = NestedRoleSerializer(required=False, allow_null=True)
+    role = RoleSerializer(nested=True, required=False, allow_null=True)
 
 
     class Meta:
     class Meta:
         model = VLAN
         model = VLAN
@@ -321,12 +322,12 @@ class CreateAvailableVLANSerializer(NetBoxModelSerializer):
 class PrefixSerializer(NetBoxModelSerializer):
 class PrefixSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:prefix-detail')
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:prefix-detail')
     family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
     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)
     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)
     children = serializers.IntegerField(read_only=True)
     _depth = serializers.IntegerField(read_only=True)
     _depth = serializers.IntegerField(read_only=True)
     prefix = IPNetworkField()
     prefix = IPNetworkField()
@@ -374,11 +375,11 @@ class AvailablePrefixSerializer(serializers.Serializer):
     """
     """
     family = serializers.IntegerField(read_only=True)
     family = serializers.IntegerField(read_only=True)
     prefix = serializers.CharField(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):
     def to_representation(self, instance):
         if self.context.get('vrf'):
         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:
         else:
             vrf = None
             vrf = None
         return {
         return {
@@ -397,10 +398,10 @@ class IPRangeSerializer(NetBoxModelSerializer):
     family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
     family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
     start_address = IPAddressField()
     start_address = IPAddressField()
     end_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)
     status = ChoiceField(choices=IPRangeStatusChoices, required=False)
-    role = NestedRoleSerializer(required=False, allow_null=True)
+    role = RoleSerializer(nested=True, required=False, allow_null=True)
 
 
     class Meta:
     class Meta:
         model = IPRange
         model = IPRange
@@ -420,8 +421,8 @@ class IPAddressSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail')
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail')
     family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
     family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
     address = IPAddressField()
     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)
     status = ChoiceField(choices=IPAddressStatusChoices, required=False)
     role = ChoiceField(choices=IPAddressRoleChoices, allow_blank=True, required=False)
     role = ChoiceField(choices=IPAddressRoleChoices, allow_blank=True, required=False)
     assigned_object_type = ContentTypeField(
     assigned_object_type = ContentTypeField(
@@ -457,12 +458,12 @@ class AvailableIPSerializer(serializers.Serializer):
     """
     """
     family = serializers.IntegerField(read_only=True)
     family = serializers.IntegerField(read_only=True)
     address = serializers.CharField(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)
     description = serializers.CharField(required=False)
 
 
     def to_representation(self, instance):
     def to_representation(self, instance):
         if self.context.get('vrf'):
         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:
         else:
             vrf = None
             vrf = None
         return {
         return {
@@ -491,8 +492,8 @@ class ServiceTemplateSerializer(NetBoxModelSerializer):
 
 
 class ServiceSerializer(NetBoxModelSerializer):
 class ServiceSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:service-detail')
     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)
     protocol = ChoiceField(choices=ServiceProtocolChoices, required=False)
     ipaddresses = SerializedPKRelatedField(
     ipaddresses = SerializedPKRelatedField(
         queryset=IPAddress.objects.all(),
         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 rest_framework import serializers
 from drf_spectacular.utils import extend_schema_field
 from drf_spectacular.utils import extend_schema_field
 from drf_spectacular.types import OpenApiTypes
 from drf_spectacular.types import OpenApiTypes
 
 
+from utilities.api import get_related_object_by_attrs
+
 __all__ = (
 __all__ = (
     'BaseModelSerializer',
     'BaseModelSerializer',
     'ValidatedModelSerializer',
     'ValidatedModelSerializer',
@@ -12,15 +13,30 @@ __all__ = (
 class BaseModelSerializer(serializers.ModelSerializer):
 class BaseModelSerializer(serializers.ModelSerializer):
     display = serializers.SerializerMethodField(read_only=True)
     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)
         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 specific fields have been requested, omit the others
         if requested_fields:
         if requested_fields:
             for field in list(self.fields.keys()):
             for field in list(self.fields.keys()):
                 if field not in requested_fields:
                 if field not in requested_fields:
                     self.fields.pop(field)
                     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)
     @extend_schema_field(OpenApiTypes.STR)
     def get_display(self, obj):
     def get_display(self, obj):
         return str(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)
     validation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)
     """
     """
     def validate(self, data):
     def validate(self, data):
+
+        # Skip validation if we're being used to represent a nested object
+        if self.nested:
+            return data
+
         attrs = data.copy()
         attrs = data.copy()
 
 
         # Remove custom field data (if any) prior to model validation
         # 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 import serializers
-from rest_framework.exceptions import ValidationError
 
 
 from extras.models import Tag
 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
 from .base import BaseModelSerializer
 
 
 __all__ = (
 __all__ = (
@@ -20,43 +17,8 @@ class WritableNestedSerializer(BaseModelSerializer):
     subclassed to return a full representation of the related object on read.
     subclassed to return a full representation of the related object on read.
     """
     """
     def to_internal_value(self, data):
     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
 # 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):
 class TenantSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenant-detail')
     url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenant-detail')
-    group = NestedTenantGroupSerializer(required=False, allow_null=True)
+    group = TenantGroupSerializer(nested=True, required=False, allow_null=True)
 
 
     # Related object counts
     # Related object counts
     circuit_count = RelatedObjectCountField('circuits')
     circuit_count = RelatedObjectCountField('circuits')
@@ -87,7 +87,7 @@ class ContactRoleSerializer(NetBoxModelSerializer):
 
 
 class ContactSerializer(NetBoxModelSerializer):
 class ContactSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contact-detail')
     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:
     class Meta:
         model = Contact
         model = Contact
@@ -104,8 +104,8 @@ class ContactAssignmentSerializer(NetBoxModelSerializer):
         queryset=ContentType.objects.all()
         queryset=ContentType.objects.all()
     )
     )
     object = serializers.SerializerMethodField(read_only=True)
     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: '')
     priority = ChoiceField(choices=ContactPriorityChoices, allow_blank=True, required=False, default=lambda: '')
 
 
     class Meta:
     class Meta:

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

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

+ 52 - 5
netbox/utilities/api.py

@@ -3,23 +3,26 @@ import sys
 
 
 from django.conf import settings
 from django.conf import settings
 from django.contrib.contenttypes.fields import GenericForeignKey
 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.db.models.fields.related import ManyToOneRel, RelatedField
 from django.http import JsonResponse
 from django.http import JsonResponse
 from django.urls import reverse
 from django.urls import reverse
+from django.utils.translation import gettext_lazy as _
 from rest_framework import status
 from rest_framework import status
 from rest_framework.serializers import Serializer
 from rest_framework.serializers import Serializer
 from rest_framework.utils import formatting
 from rest_framework.utils import formatting
 
 
 from netbox.api.fields import RelatedObjectCountField
 from netbox.api.fields import RelatedObjectCountField
 from netbox.api.exceptions import GraphQLTypeNotFound, SerializerNotFound
 from netbox.api.exceptions import GraphQLTypeNotFound, SerializerNotFound
-from utilities.utils import count_related
-from .utils import dynamic_import
+from .utils import count_related, dict_to_filter_params, dynamic_import
 
 
 __all__ = (
 __all__ = (
     'get_annotations_for_serializer',
     'get_annotations_for_serializer',
     'get_graphql_type_for_model',
     'get_graphql_type_for_model',
     'get_prefetches_for_serializer',
     'get_prefetches_for_serializer',
+    'get_related_object_by_attrs',
     'get_serializer_for_model',
     'get_serializer_for_model',
     'get_view_name',
     'get_view_name',
     'is_api_request',
     'is_api_request',
@@ -103,7 +106,7 @@ def get_prefetches_for_serializer(serializer_class, fields_to_include=None):
     """
     """
     model = serializer_class.Meta.model
     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:
     if not fields_to_include:
         fields_to_include = serializer_class.Meta.fields
         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.
         # for the related object.
         if serializer_field:
         if serializer_field:
             if issubclass(type(serializer_field), Serializer):
             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}')
                     prefetch_fields.append(f'{field_name}__{subfield}')
 
 
     return prefetch_fields
     return prefetch_fields
@@ -154,6 +159,48 @@ def get_annotations_for_serializer(serializer_class, fields_to_include=None):
     return annotations
     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):
 def rest_api_server_error(request, *args, **kwargs):
     """
     """
     Handle exceptions and return a useful error message for REST API requests.
     Handle exceptions and return a useful error message for REST API requests.

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

@@ -1,16 +1,14 @@
 from drf_spectacular.utils import extend_schema_field
 from drf_spectacular.utils import extend_schema_field
 from rest_framework import serializers
 from rest_framework import serializers
 
 
-from dcim.api.nested_serializers import (
-    NestedDeviceSerializer, NestedDeviceRoleSerializer, NestedPlatformSerializer, NestedSiteSerializer,
-)
+from dcim.api.serializers import DeviceSerializer, DeviceRoleSerializer, PlatformSerializer, SiteSerializer
 from dcim.choices import InterfaceModeChoices
 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.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer, NestedVRFSerializer
 from ipam.models import VLAN
 from ipam.models import VLAN
 from netbox.api.fields import ChoiceField, RelatedObjectCountField, SerializedPKRelatedField
 from netbox.api.fields import ChoiceField, RelatedObjectCountField, SerializedPKRelatedField
 from netbox.api.serializers import NetBoxModelSerializer
 from netbox.api.serializers import NetBoxModelSerializer
-from tenancy.api.nested_serializers import NestedTenantSerializer
+from tenancy.api.serializers import TenantSerializer
 from virtualization.choices import *
 from virtualization.choices import *
 from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualDisk, VirtualMachine, VMInterface
 from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualDisk, VirtualMachine, VMInterface
 from vpn.api.nested_serializers import NestedL2VPNTerminationSerializer
 from vpn.api.nested_serializers import NestedL2VPNTerminationSerializer
@@ -53,11 +51,11 @@ class ClusterGroupSerializer(NetBoxModelSerializer):
 
 
 class ClusterSerializer(NetBoxModelSerializer):
 class ClusterSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:cluster-detail')
     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)
     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
     # Related object counts
     device_count = RelatedObjectCountField('devices')
     device_count = RelatedObjectCountField('devices')
@@ -79,16 +77,16 @@ class ClusterSerializer(NetBoxModelSerializer):
 class VirtualMachineSerializer(NetBoxModelSerializer):
 class VirtualMachineSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:virtualmachine-detail')
     url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:virtualmachine-detail')
     status = ChoiceField(choices=VirtualMachineStatusChoices, required=False)
     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_ip = NestedIPAddressSerializer(read_only=True)
     primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True)
     primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True)
     primary_ip6 = 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
     # Counter fields
     interface_count = serializers.IntegerField(read_only=True)
     interface_count = serializers.IntegerField(read_only=True)
@@ -128,7 +126,7 @@ class VirtualMachineWithConfigContextSerializer(VirtualMachineSerializer):
 
 
 class VMInterfaceSerializer(NetBoxModelSerializer):
 class VMInterfaceSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:vminterface-detail')
     url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:vminterface-detail')
-    virtual_machine = NestedVirtualMachineSerializer()
+    virtual_machine = VirtualMachineSerializer(nested=True)
     parent = NestedVMInterfaceSerializer(required=False, allow_null=True)
     parent = NestedVMInterfaceSerializer(required=False, allow_null=True)
     bridge = NestedVMInterfaceSerializer(required=False, allow_null=True)
     bridge = NestedVMInterfaceSerializer(required=False, allow_null=True)
     mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False)
     mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False)
@@ -178,7 +176,7 @@ class VMInterfaceSerializer(NetBoxModelSerializer):
 
 
 class VirtualDiskSerializer(NetBoxModelSerializer):
 class VirtualDiskSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:virtualdisk-detail')
     url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:virtualdisk-detail')
-    virtual_machine = NestedVirtualMachineSerializer()
+    virtual_machine = VirtualMachineSerializer(nested=True)
 
 
     class Meta:
     class Meta:
         model = VirtualDisk
         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 drf_spectacular.utils import extend_schema_field
 from rest_framework import serializers
 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 ipam.models import RouteTarget
 from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField, SerializedPKRelatedField
 from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField, SerializedPKRelatedField
 from netbox.api.serializers import NetBoxModelSerializer
 from netbox.api.serializers import NetBoxModelSerializer
 from netbox.constants import NESTED_SERIALIZER_PREFIX
 from netbox.constants import NESTED_SERIALIZER_PREFIX
-from tenancy.api.nested_serializers import NestedTenantSerializer
+from tenancy.api.serializers import TenantSerializer
 from utilities.api import get_serializer_for_model
 from utilities.api import get_serializer_for_model
 from vpn.choices import *
 from vpn.choices import *
 from vpn.models 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):
 class IKEProposalSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(
     url = serializers.HyperlinkedIdentityField(
         view_name='vpn-api:ikeproposal-detail'
         view_name='vpn-api:ikeproposal-detail'
@@ -215,8 +132,12 @@ class IPSecProfileSerializer(NetBoxModelSerializer):
     mode = ChoiceField(
     mode = ChoiceField(
         choices=IPSecModeChoices
         choices=IPSecModeChoices
     )
     )
-    ike_policy = NestedIKEPolicySerializer()
-    ipsec_policy = NestedIPSecPolicySerializer()
+    ike_policy = IKEPolicySerializer(
+        nested=True
+    )
+    ipsec_policy = IPSecPolicySerializer(
+        nested=True
+    )
 
 
     class Meta:
     class Meta:
         model = IPSecProfile
         model = IPSecProfile
@@ -227,6 +148,100 @@ class IPSecProfileSerializer(NetBoxModelSerializer):
         brief_fields = ('id', 'url', 'display', 'name', 'description')
         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
 # L2VPN
 #
 #
@@ -246,7 +261,7 @@ class L2VPNSerializer(NetBoxModelSerializer):
         required=False,
         required=False,
         many=True
         many=True
     )
     )
-    tenant = NestedTenantSerializer(required=False, allow_null=True)
+    tenant = TenantSerializer(nested=True, required=False, allow_null=True)
 
 
     class Meta:
     class Meta:
         model = L2VPN
         model = L2VPN
@@ -259,7 +274,9 @@ class L2VPNSerializer(NetBoxModelSerializer):
 
 
 class L2VPNTerminationSerializer(NetBoxModelSerializer):
 class L2VPNTerminationSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='vpn-api:l2vpntermination-detail')
     url = serializers.HyperlinkedIdentityField(view_name='vpn-api:l2vpntermination-detail')
-    l2vpn = NestedL2VPNSerializer()
+    l2vpn = L2VPNSerializer(
+        nested=True
+    )
     assigned_object_type = ContentTypeField(
     assigned_object_type = ContentTypeField(
         queryset=ContentType.objects.all()
         queryset=ContentType.objects.all()
     )
     )

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

@@ -1,11 +1,11 @@
 from rest_framework import serializers
 from rest_framework import serializers
 
 
 from dcim.choices import LinkStatusChoices
 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.fields import ChoiceField
 from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer
 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.choices import *
 from wireless.models import *
 from wireless.models import *
 from .nested_serializers import *
 from .nested_serializers import *
@@ -33,10 +33,10 @@ class WirelessLANGroupSerializer(NestedGroupModelSerializer):
 
 
 class WirelessLANSerializer(NetBoxModelSerializer):
 class WirelessLANSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslan-detail')
     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)
     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_type = ChoiceField(choices=WirelessAuthTypeChoices, required=False, allow_blank=True)
     auth_cipher = ChoiceField(choices=WirelessAuthCipherChoices, 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):
 class WirelessLinkSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslink-detail')
     url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslink-detail')
     status = ChoiceField(choices=LinkStatusChoices, required=False)
     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_type = ChoiceField(choices=WirelessAuthTypeChoices, required=False, allow_blank=True)
     auth_cipher = ChoiceField(choices=WirelessAuthCipherChoices, required=False, allow_blank=True)
     auth_cipher = ChoiceField(choices=WirelessAuthCipherChoices, required=False, allow_blank=True)
 
 

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