1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Replace nested serializers with primary serializers where possible

This commit is contained in:
Jeremy Stretch
2024-02-26 16:49:30 -05:00
parent d042e6f69d
commit c146f5e1b5
16 changed files with 1525 additions and 1437 deletions

View File

@ -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

View File

@ -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
)

View File

@ -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)

View File

@ -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)

File diff suppressed because it is too large Load Diff

View File

@ -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,

View File

@ -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(

View File

@ -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(),

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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(

View File

@ -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.

View File

@ -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

View File

@ -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()
)

View File

@ -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)