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

Finished merging writable serializers

Jeremy Stretch 7 лет назад
Родитель
Сommit
821fb1e01e

+ 2 - 2
netbox/circuits/api/serializers.py

@@ -59,7 +59,7 @@ class CircuitSerializer(CustomFieldModelSerializer):
     provider = NestedProviderSerializer()
     status = ChoiceFieldSerializer(choices=CIRCUIT_STATUS_CHOICES, required=False)
     type = NestedCircuitTypeSerializer()
-    tenant = NestedTenantSerializer(required=False)
+    tenant = NestedTenantSerializer(required=False, allow_null=True)
 
     class Meta:
         model = Circuit
@@ -84,7 +84,7 @@ class NestedCircuitSerializer(WritableNestedSerializer):
 class CircuitTerminationSerializer(ValidatedModelSerializer):
     circuit = NestedCircuitSerializer()
     site = NestedSiteSerializer()
-    interface = InterfaceSerializer(required=False)
+    interface = InterfaceSerializer(required=False, allow_null=True)
 
     class Meta:
         model = CircuitTermination

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

@@ -37,7 +37,7 @@ class NestedRegionSerializer(WritableNestedSerializer):
 
 
 class RegionSerializer(serializers.ModelSerializer):
-    parent = NestedRegionSerializer(required=False)
+    parent = NestedRegionSerializer(required=False, allow_null=True)
 
     class Meta:
         model = Region
@@ -50,8 +50,8 @@ class RegionSerializer(serializers.ModelSerializer):
 
 class SiteSerializer(CustomFieldModelSerializer):
     status = ChoiceFieldSerializer(choices=SITE_STATUS_CHOICES, required=False)
-    region = NestedRegionSerializer(required=False)
-    tenant = NestedTenantSerializer(required=False)
+    region = NestedRegionSerializer(required=False, allow_null=True)
+    tenant = NestedTenantSerializer(required=False, allow_null=True)
     time_zone = TimeZoneField(required=False)
 
     class Meta:
@@ -117,9 +117,9 @@ class NestedRackRoleSerializer(WritableNestedSerializer):
 
 class RackSerializer(CustomFieldModelSerializer):
     site = NestedSiteSerializer()
-    group = NestedRackGroupSerializer(required=False)
-    tenant = NestedTenantSerializer(required=False)
-    role = NestedRackRoleSerializer(required=False)
+    group = NestedRackGroupSerializer(required=False, allow_null=True)
+    tenant = NestedTenantSerializer(required=False, allow_null=True)
+    role = NestedRackRoleSerializer(required=False, allow_null=True)
     type = ChoiceFieldSerializer(choices=RACK_TYPE_CHOICES, required=False)
     width = ChoiceFieldSerializer(choices=RACK_WIDTH_CHOICES, required=False)
 
@@ -186,7 +186,7 @@ class RackUnitSerializer(serializers.Serializer):
 class RackReservationSerializer(ValidatedModelSerializer):
     rack = NestedRackSerializer()
     user = NestedUserSerializer()
-    tenant = NestedTenantSerializer(required=False)
+    tenant = NestedTenantSerializer(required=False, allow_null=True)
 
     class Meta:
         model = RackReservation
@@ -337,7 +337,7 @@ class NestedDeviceRoleSerializer(WritableNestedSerializer):
 #
 
 class PlatformSerializer(ValidatedModelSerializer):
-    manufacturer = NestedManufacturerSerializer(required=False)
+    manufacturer = NestedManufacturerSerializer(required=False, allow_null=True)
 
     class Meta:
         model = Platform
@@ -387,18 +387,18 @@ class DeviceVirtualChassisSerializer(serializers.ModelSerializer):
 class DeviceSerializer(CustomFieldModelSerializer):
     device_type = NestedDeviceTypeSerializer()
     device_role = NestedDeviceRoleSerializer()
-    tenant = NestedTenantSerializer(required=False)
-    platform = NestedPlatformSerializer(required=False)
+    tenant = NestedTenantSerializer(required=False, allow_null=True)
+    platform = NestedPlatformSerializer(required=False, allow_null=True)
     site = NestedSiteSerializer()
-    rack = NestedRackSerializer(required=False)
+    rack = NestedRackSerializer(required=False, allow_null=True)
     face = ChoiceFieldSerializer(choices=RACK_FACE_CHOICES, required=False)
     status = ChoiceFieldSerializer(choices=DEVICE_STATUS_CHOICES, required=False)
     primary_ip = DeviceIPAddressSerializer(read_only=True)
-    primary_ip4 = DeviceIPAddressSerializer(required=False)
-    primary_ip6 = DeviceIPAddressSerializer(required=False)
+    primary_ip4 = DeviceIPAddressSerializer(required=False, allow_null=True)
+    primary_ip6 = DeviceIPAddressSerializer(required=False, allow_null=True)
     parent_device = serializers.SerializerMethodField()
-    cluster = NestedClusterSerializer(required=False)
-    virtual_chassis = DeviceVirtualChassisSerializer(required=False)
+    cluster = NestedClusterSerializer(required=False, allow_null=True)
+    virtual_chassis = DeviceVirtualChassisSerializer(required=False, allow_null=True)
 
     class Meta:
         model = Device
@@ -462,7 +462,7 @@ class NestedConsoleServerPortSerializer(WritableNestedSerializer):
 
 class ConsolePortSerializer(ValidatedModelSerializer):
     device = NestedDeviceSerializer()
-    cs_port = NestedConsoleServerPortSerializer(required=False)
+    cs_port = NestedConsoleServerPortSerializer(required=False, allow_null=True)
 
     class Meta:
         model = ConsolePort
@@ -497,7 +497,7 @@ class NestedPowerOutletSerializer(WritableNestedSerializer):
 
 class PowerPortSerializer(ValidatedModelSerializer):
     device = NestedDeviceSerializer()
-    power_outlet = NestedPowerOutletSerializer(required=False)
+    power_outlet = NestedPowerOutletSerializer(required=False, allow_null=True)
 
     class Meta:
         model = PowerPort
@@ -547,11 +547,11 @@ class InterfaceVLANSerializer(WritableNestedSerializer):
 class InterfaceSerializer(ValidatedModelSerializer):
     device = NestedDeviceSerializer()
     form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES, required=False)
-    lag = NestedInterfaceSerializer(required=False)
+    lag = NestedInterfaceSerializer(required=False, allow_null=True)
     is_connected = serializers.SerializerMethodField(read_only=True)
     interface_connection = serializers.SerializerMethodField(read_only=True)
-    circuit_termination = InterfaceCircuitTerminationSerializer(required=False)
-    untagged_vlan = InterfaceVLANSerializer(required=False)
+    circuit_termination = InterfaceCircuitTerminationSerializer(read_only=True)
+    untagged_vlan = InterfaceVLANSerializer(required=False, allow_null=True)
     mode = ChoiceFieldSerializer(choices=IFACE_MODE_CHOICES, required=False)
     tagged_vlans = InterfaceVLANSerializer(many=True, required=False)
 
@@ -603,27 +603,13 @@ class InterfaceSerializer(ValidatedModelSerializer):
         return None
 
 
-# class PeerInterfaceSerializer(serializers.ModelSerializer):
-#     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
-#     device = NestedDeviceSerializer()
-#     form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES)
-#     lag = NestedInterfaceSerializer()
-#
-#     class Meta:
-#         model = Interface
-#         fields = [
-#             'id', 'url', 'device', 'name', 'form_factor', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only',
-#             'description',
-#         ]
-
-
 #
 # Device bays
 #
 
 class DeviceBaySerializer(ValidatedModelSerializer):
     device = NestedDeviceSerializer()
-    installed_device = NestedDeviceSerializer(required=False)
+    installed_device = NestedDeviceSerializer(required=False, allow_null=True)
 
     class Meta:
         model = DeviceBay

+ 53 - 114
netbox/ipam/api/serializers.py

@@ -14,7 +14,7 @@ from ipam.constants import (
 )
 from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
 from tenancy.api.serializers import NestedTenantSerializer
-from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer
+from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer, WritableNestedSerializer
 from virtualization.api.serializers import NestedVirtualMachineSerializer
 
 
@@ -23,7 +23,7 @@ from virtualization.api.serializers import NestedVirtualMachineSerializer
 #
 
 class VRFSerializer(CustomFieldModelSerializer):
-    tenant = NestedTenantSerializer()
+    tenant = NestedTenantSerializer(required=False, allow_null=True)
 
     class Meta:
         model = VRF
@@ -33,7 +33,7 @@ class VRFSerializer(CustomFieldModelSerializer):
         ]
 
 
-class NestedVRFSerializer(serializers.ModelSerializer):
+class NestedVRFSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vrf-detail')
 
     class Meta:
@@ -41,15 +41,6 @@ class NestedVRFSerializer(serializers.ModelSerializer):
         fields = ['id', 'url', 'name', 'rd']
 
 
-class WritableVRFSerializer(CustomFieldModelSerializer):
-
-    class Meta:
-        model = VRF
-        fields = [
-            'id', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'custom_fields', 'created', 'last_updated',
-        ]
-
-
 #
 # Roles
 #
@@ -61,7 +52,7 @@ class RoleSerializer(ValidatedModelSerializer):
         fields = ['id', 'name', 'slug', 'weight']
 
 
-class NestedRoleSerializer(serializers.ModelSerializer):
+class NestedRoleSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:role-detail')
 
     class Meta:
@@ -80,7 +71,7 @@ class RIRSerializer(ValidatedModelSerializer):
         fields = ['id', 'name', 'slug', 'is_private']
 
 
-class NestedRIRSerializer(serializers.ModelSerializer):
+class NestedRIRSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail')
 
     class Meta:
@@ -100,9 +91,10 @@ class AggregateSerializer(CustomFieldModelSerializer):
         fields = [
             'id', 'family', 'prefix', 'rir', 'date_added', 'description', 'custom_fields', 'created', 'last_updated',
         ]
+        read_only_fields = ['family']
 
 
-class NestedAggregateSerializer(serializers.ModelSerializer):
+class NestedAggregateSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:aggregate-detail')
 
     class Meta(AggregateSerializer.Meta):
@@ -110,34 +102,12 @@ class NestedAggregateSerializer(serializers.ModelSerializer):
         fields = ['id', 'url', 'family', 'prefix']
 
 
-class WritableAggregateSerializer(CustomFieldModelSerializer):
-
-    class Meta:
-        model = Aggregate
-        fields = ['id', 'prefix', 'rir', 'date_added', 'description', 'custom_fields', 'created', 'last_updated']
-
-
 #
 # VLAN groups
 #
 
-class VLANGroupSerializer(serializers.ModelSerializer):
-    site = NestedSiteSerializer()
-
-    class Meta:
-        model = VLANGroup
-        fields = ['id', 'name', 'slug', 'site']
-
-
-class NestedVLANGroupSerializer(serializers.ModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail')
-
-    class Meta:
-        model = VLANGroup
-        fields = ['id', 'url', 'name', 'slug']
-
-
-class WritableVLANGroupSerializer(serializers.ModelSerializer):
+class VLANGroupSerializer(ValidatedModelSerializer):
+    site = NestedSiteSerializer(required=False, allow_null=True)
 
     class Meta:
         model = VLANGroup
@@ -154,21 +124,29 @@ class WritableVLANGroupSerializer(serializers.ModelSerializer):
                 validator(data)
 
         # Enforce model validation
-        super(WritableVLANGroupSerializer, self).validate(data)
+        super(VLANGroupSerializer, self).validate(data)
 
         return data
 
 
+class NestedVLANGroupSerializer(WritableNestedSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail')
+
+    class Meta:
+        model = VLANGroup
+        fields = ['id', 'url', 'name', 'slug']
+
+
 #
 # VLANs
 #
 
 class VLANSerializer(CustomFieldModelSerializer):
-    site = NestedSiteSerializer()
-    group = NestedVLANGroupSerializer()
-    tenant = NestedTenantSerializer()
-    status = ChoiceFieldSerializer(choices=VLAN_STATUS_CHOICES)
-    role = NestedRoleSerializer()
+    site = NestedSiteSerializer(required=False, allow_null=True)
+    group = NestedVLANGroupSerializer(required=False, allow_null=True)
+    tenant = NestedTenantSerializer(required=False, allow_null=True)
+    status = ChoiceFieldSerializer(choices=VLAN_STATUS_CHOICES, required=False)
+    role = NestedRoleSerializer(required=False, allow_null=True)
 
     class Meta:
         model = VLAN
@@ -176,24 +154,6 @@ class VLANSerializer(CustomFieldModelSerializer):
             'id', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', 'display_name',
             'custom_fields', 'created', 'last_updated',
         ]
-
-
-class NestedVLANSerializer(serializers.ModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail')
-
-    class Meta:
-        model = VLAN
-        fields = ['id', 'url', 'vid', 'name', 'display_name']
-
-
-class WritableVLANSerializer(CustomFieldModelSerializer):
-
-    class Meta:
-        model = VLAN
-        fields = [
-            'id', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', 'custom_fields', 'created',
-            'last_updated',
-        ]
         validators = []
 
     def validate(self, data):
@@ -206,22 +166,30 @@ class WritableVLANSerializer(CustomFieldModelSerializer):
                 validator(data)
 
         # Enforce model validation
-        super(WritableVLANSerializer, self).validate(data)
+        super(VLANSerializer, self).validate(data)
 
         return data
 
 
+class NestedVLANSerializer(WritableNestedSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail')
+
+    class Meta:
+        model = VLAN
+        fields = ['id', 'url', 'vid', 'name', 'display_name']
+
+
 #
 # Prefixes
 #
 
 class PrefixSerializer(CustomFieldModelSerializer):
-    site = NestedSiteSerializer()
-    vrf = NestedVRFSerializer()
-    tenant = NestedTenantSerializer()
-    vlan = NestedVLANSerializer()
-    status = ChoiceFieldSerializer(choices=PREFIX_STATUS_CHOICES)
-    role = NestedRoleSerializer()
+    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)
+    status = ChoiceFieldSerializer(choices=PREFIX_STATUS_CHOICES, required=False)
+    role = NestedRoleSerializer(required=False, allow_null=True)
 
     class Meta:
         model = Prefix
@@ -229,9 +197,10 @@ class PrefixSerializer(CustomFieldModelSerializer):
             'id', 'family', 'prefix', 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', 'description',
             'custom_fields', 'created', 'last_updated',
         ]
+        read_only_fields = ['family']
 
 
-class NestedPrefixSerializer(serializers.ModelSerializer):
+class NestedPrefixSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:prefix-detail')
 
     class Meta:
@@ -239,16 +208,6 @@ class NestedPrefixSerializer(serializers.ModelSerializer):
         fields = ['id', 'url', 'family', 'prefix']
 
 
-class WritablePrefixSerializer(CustomFieldModelSerializer):
-
-    class Meta:
-        model = Prefix
-        fields = [
-            'id', 'prefix', 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', 'description',
-            'custom_fields', 'created', 'last_updated',
-        ]
-
-
 class AvailablePrefixSerializer(serializers.Serializer):
 
     def to_representation(self, instance):
@@ -288,11 +247,11 @@ class IPAddressInterfaceSerializer(serializers.ModelSerializer):
 
 
 class IPAddressSerializer(CustomFieldModelSerializer):
-    vrf = NestedVRFSerializer()
-    tenant = NestedTenantSerializer()
-    status = ChoiceFieldSerializer(choices=IPADDRESS_STATUS_CHOICES)
-    role = ChoiceFieldSerializer(choices=IPADDRESS_ROLE_CHOICES)
-    interface = IPAddressInterfaceSerializer()
+    vrf = NestedVRFSerializer(required=False, allow_null=True)
+    tenant = NestedTenantSerializer(required=False, allow_null=True)
+    status = ChoiceFieldSerializer(choices=IPADDRESS_STATUS_CHOICES, required=False)
+    role = ChoiceFieldSerializer(choices=IPADDRESS_ROLE_CHOICES, required=False)
+    interface = IPAddressInterfaceSerializer(required=False, allow_null=True)
 
     class Meta:
         model = IPAddress
@@ -300,9 +259,10 @@ class IPAddressSerializer(CustomFieldModelSerializer):
             'id', 'family', 'address', 'vrf', 'tenant', 'status', 'role', 'interface', 'description', 'nat_inside',
             'nat_outside', 'custom_fields', 'created', 'last_updated',
         ]
+        read_only_fields = ['family']
 
 
-class NestedIPAddressSerializer(serializers.ModelSerializer):
+class NestedIPAddressSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail')
 
     class Meta:
@@ -310,18 +270,8 @@ class NestedIPAddressSerializer(serializers.ModelSerializer):
         fields = ['id', 'url', 'family', 'address']
 
 
-IPAddressSerializer._declared_fields['nat_inside'] = NestedIPAddressSerializer()
-IPAddressSerializer._declared_fields['nat_outside'] = NestedIPAddressSerializer()
-
-
-class WritableIPAddressSerializer(CustomFieldModelSerializer):
-
-    class Meta:
-        model = IPAddress
-        fields = [
-            'id', 'address', 'vrf', 'tenant', 'status', 'role', 'interface', 'description', 'nat_inside',
-            'custom_fields', 'created', 'last_updated',
-        ]
+IPAddressSerializer._declared_fields['nat_inside'] = NestedIPAddressSerializer(required=False, allow_null=True)
+IPAddressSerializer._declared_fields['nat_outside'] = NestedIPAddressSerializer(read_only=True)
 
 
 class AvailableIPSerializer(serializers.Serializer):
@@ -342,22 +292,11 @@ class AvailableIPSerializer(serializers.Serializer):
 # Services
 #
 
-class ServiceSerializer(serializers.ModelSerializer):
-    device = NestedDeviceSerializer()
-    virtual_machine = NestedVirtualMachineSerializer()
+class ServiceSerializer(ValidatedModelSerializer):
+    device = NestedDeviceSerializer(required=False, allow_null=True)
+    virtual_machine = NestedVirtualMachineSerializer(required=False, allow_null=True)
     protocol = ChoiceFieldSerializer(choices=IP_PROTOCOL_CHOICES)
-    ipaddresses = NestedIPAddressSerializer(many=True)
-
-    class Meta:
-        model = Service
-        fields = [
-            'id', 'device', 'virtual_machine', 'name', 'port', 'protocol', 'ipaddresses', 'description', 'created',
-            'last_updated',
-        ]
-
-
-# TODO: Figure out how to use model validation with ManyToManyFields. Calling clean() yields a ValueError.
-class WritableServiceSerializer(serializers.ModelSerializer):
+    ipaddresses = NestedIPAddressSerializer(many=True, required=False)
 
     class Meta:
         model = Service

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

@@ -35,7 +35,6 @@ class IPAMFieldChoicesViewSet(FieldChoicesViewSet):
 class VRFViewSet(CustomFieldModelViewSet):
     queryset = VRF.objects.select_related('tenant')
     serializer_class = serializers.VRFSerializer
-    write_serializer_class = serializers.WritableVRFSerializer
     filter_class = filters.VRFFilter
 
 
@@ -56,7 +55,6 @@ class RIRViewSet(ModelViewSet):
 class AggregateViewSet(CustomFieldModelViewSet):
     queryset = Aggregate.objects.select_related('rir')
     serializer_class = serializers.AggregateSerializer
-    write_serializer_class = serializers.WritableAggregateSerializer
     filter_class = filters.AggregateFilter
 
 
@@ -77,7 +75,6 @@ class RoleViewSet(ModelViewSet):
 class PrefixViewSet(CustomFieldModelViewSet):
     queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
     serializer_class = serializers.PrefixSerializer
-    write_serializer_class = serializers.WritablePrefixSerializer
     filter_class = filters.PrefixFilter
 
     @detail_route(url_path='available-prefixes', methods=['get', 'post'])
@@ -120,9 +117,9 @@ class PrefixViewSet(CustomFieldModelViewSet):
 
             # Initialize the serializer with a list or a single object depending on what was requested
             if isinstance(request.data, list):
-                serializer = serializers.WritablePrefixSerializer(data=requested_prefixes, many=True)
+                serializer = serializers.PrefixSerializer(data=requested_prefixes, many=True)
             else:
-                serializer = serializers.WritablePrefixSerializer(data=requested_prefixes[0])
+                serializer = serializers.PrefixSerializer(data=requested_prefixes[0])
 
             # Create the new Prefix(es)
             if serializer.is_valid():
@@ -177,9 +174,9 @@ class PrefixViewSet(CustomFieldModelViewSet):
 
             # Initialize the serializer with a list or a single object depending on what was requested
             if isinstance(request.data, list):
-                serializer = serializers.WritableIPAddressSerializer(data=requested_ips, many=True)
+                serializer = serializers.IPAddressSerializer(data=requested_ips, many=True)
             else:
-                serializer = serializers.WritableIPAddressSerializer(data=requested_ips[0])
+                serializer = serializers.IPAddressSerializer(data=requested_ips[0])
 
             # Create the new IP address(es)
             if serializer.is_valid():
@@ -223,7 +220,6 @@ class IPAddressViewSet(CustomFieldModelViewSet):
         'nat_outside'
     )
     serializer_class = serializers.IPAddressSerializer
-    write_serializer_class = serializers.WritableIPAddressSerializer
     filter_class = filters.IPAddressFilter
 
 
@@ -234,7 +230,6 @@ class IPAddressViewSet(CustomFieldModelViewSet):
 class VLANGroupViewSet(ModelViewSet):
     queryset = VLANGroup.objects.select_related('site')
     serializer_class = serializers.VLANGroupSerializer
-    write_serializer_class = serializers.WritableVLANGroupSerializer
     filter_class = filters.VLANGroupFilter
 
 
@@ -245,7 +240,6 @@ class VLANGroupViewSet(ModelViewSet):
 class VLANViewSet(CustomFieldModelViewSet):
     queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role')
     serializer_class = serializers.VLANSerializer
-    write_serializer_class = serializers.WritableVLANSerializer
     filter_class = filters.VLANFilter
 
 
@@ -256,5 +250,4 @@ class VLANViewSet(CustomFieldModelViewSet):
 class ServiceViewSet(ModelViewSet):
     queryset = Service.objects.select_related('device')
     serializer_class = serializers.ServiceSerializer
-    write_serializer_class = serializers.WritableServiceSerializer
     filter_class = filters.ServiceFilter

+ 4 - 11
netbox/secrets/api/serializers.py

@@ -5,7 +5,7 @@ from rest_framework.validators import UniqueTogetherValidator
 
 from dcim.api.serializers import NestedDeviceSerializer
 from secrets.models import Secret, SecretRole
-from utilities.api import ValidatedModelSerializer
+from utilities.api import ValidatedModelSerializer, WritableNestedSerializer
 
 
 #
@@ -19,7 +19,7 @@ class SecretRoleSerializer(ValidatedModelSerializer):
         fields = ['id', 'name', 'slug']
 
 
-class NestedSecretRoleSerializer(serializers.ModelSerializer):
+class NestedSecretRoleSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='secrets-api:secretrole-detail')
 
     class Meta:
@@ -31,16 +31,9 @@ class NestedSecretRoleSerializer(serializers.ModelSerializer):
 # Secrets
 #
 
-class SecretSerializer(serializers.ModelSerializer):
+class SecretSerializer(ValidatedModelSerializer):
     device = NestedDeviceSerializer()
     role = NestedSecretRoleSerializer()
-
-    class Meta:
-        model = Secret
-        fields = ['id', 'device', 'role', 'name', 'plaintext', 'hash', 'created', 'last_updated']
-
-
-class WritableSecretSerializer(serializers.ModelSerializer):
     plaintext = serializers.CharField()
 
     class Meta:
@@ -64,6 +57,6 @@ class WritableSecretSerializer(serializers.ModelSerializer):
             validator(data)
 
         # Enforce model validation
-        super(WritableSecretSerializer, self).validate(data)
+        super(SecretSerializer, self).validate(data)
 
         return data

+ 0 - 1
netbox/secrets/api/views.py

@@ -51,7 +51,6 @@ class SecretViewSet(ModelViewSet):
         'role__users', 'role__groups',
     )
     serializer_class = serializers.SecretSerializer
-    write_serializer_class = serializers.WritableSecretSerializer
     filter_class = filters.SecretFilter
 
     master_key = None

+ 3 - 9
netbox/utilities/api.py

@@ -131,6 +131,8 @@ class WritableNestedSerializer(ModelSerializer):
     Returns a nested representation of an object on read, but accepts only a primary key on write.
     """
     def to_internal_value(self, data):
+        if data is None:
+            return None
         try:
             return self.Meta.model.objects.get(pk=data)
         except ObjectDoesNotExist:
@@ -148,16 +150,8 @@ class ModelViewSet(mixins.CreateModelMixin,
                    mixins.ListModelMixin,
                    GenericViewSet):
     """
-    Substitute DRF's built-in ModelViewSet for our own, which introduces a bit of additional functionality:
-    1. Use an alternate serializer (if provided) for write operations
-    2. Accept either a single object or a list of objects to create
+    Accept either a single object or a list of objects to create.
     """
-    def get_serializer_class(self):
-        # Check for a different serializer to use for write operations
-        if self.action in WRITE_OPERATIONS and hasattr(self, 'write_serializer_class'):
-            return self.write_serializer_class
-        return self.serializer_class
-
     def get_serializer(self, *args, **kwargs):
         # If a list of objects has been provided, initialize the serializer with many=True
         if isinstance(kwargs.get('data', {}), list):

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

@@ -8,7 +8,7 @@ from dcim.models import Interface
 from extras.api.customfields import CustomFieldModelSerializer
 from ipam.models import IPAddress
 from tenancy.api.serializers import NestedTenantSerializer
-from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer
+from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer, WritableNestedSerializer
 from virtualization.constants import VM_STATUS_CHOICES
 from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine
 
@@ -24,7 +24,7 @@ class ClusterTypeSerializer(ValidatedModelSerializer):
         fields = ['id', 'name', 'slug']
 
 
-class NestedClusterTypeSerializer(serializers.ModelSerializer):
+class NestedClusterTypeSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustertype-detail')
 
     class Meta:
@@ -43,7 +43,7 @@ class ClusterGroupSerializer(ValidatedModelSerializer):
         fields = ['id', 'name', 'slug']
 
 
-class NestedClusterGroupSerializer(serializers.ModelSerializer):
+class NestedClusterGroupSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustergroup-detail')
 
     class Meta:
@@ -57,15 +57,15 @@ class NestedClusterGroupSerializer(serializers.ModelSerializer):
 
 class ClusterSerializer(CustomFieldModelSerializer):
     type = NestedClusterTypeSerializer()
-    group = NestedClusterGroupSerializer()
-    site = NestedSiteSerializer()
+    group = NestedClusterGroupSerializer(required=False, allow_null=True)
+    site = NestedSiteSerializer(required=False, allow_null=True)
 
     class Meta:
         model = Cluster
         fields = ['id', 'name', 'type', 'group', 'site', 'comments', 'custom_fields', 'created', 'last_updated']
 
 
-class NestedClusterSerializer(serializers.ModelSerializer):
+class NestedClusterSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:cluster-detail')
 
     class Meta:
@@ -73,13 +73,6 @@ class NestedClusterSerializer(serializers.ModelSerializer):
         fields = ['id', 'url', 'name']
 
 
-class WritableClusterSerializer(CustomFieldModelSerializer):
-
-    class Meta:
-        model = Cluster
-        fields = ['id', 'name', 'type', 'group', 'site', 'comments', 'custom_fields', 'created', 'last_updated']
-
-
 #
 # Virtual machines
 #
@@ -94,14 +87,14 @@ class VirtualMachineIPAddressSerializer(serializers.ModelSerializer):
 
 
 class VirtualMachineSerializer(CustomFieldModelSerializer):
-    status = ChoiceFieldSerializer(choices=VM_STATUS_CHOICES)
-    cluster = NestedClusterSerializer()
-    role = NestedDeviceRoleSerializer()
-    tenant = NestedTenantSerializer()
-    platform = NestedPlatformSerializer()
-    primary_ip = VirtualMachineIPAddressSerializer()
-    primary_ip4 = VirtualMachineIPAddressSerializer()
-    primary_ip6 = VirtualMachineIPAddressSerializer()
+    status = ChoiceFieldSerializer(choices=VM_STATUS_CHOICES, required=False)
+    cluster = NestedClusterSerializer(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)
+    primary_ip = VirtualMachineIPAddressSerializer(read_only=True)
+    primary_ip4 = VirtualMachineIPAddressSerializer(required=False, allow_null=True)
+    primary_ip6 = VirtualMachineIPAddressSerializer(required=False, allow_null=True)
 
     class Meta:
         model = VirtualMachine
@@ -111,7 +104,7 @@ class VirtualMachineSerializer(CustomFieldModelSerializer):
         ]
 
 
-class NestedVirtualMachineSerializer(serializers.ModelSerializer):
+class NestedVirtualMachineSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:virtualmachine-detail')
 
     class Meta:
@@ -119,22 +112,13 @@ class NestedVirtualMachineSerializer(serializers.ModelSerializer):
         fields = ['id', 'url', 'name']
 
 
-class WritableVirtualMachineSerializer(CustomFieldModelSerializer):
-
-    class Meta:
-        model = VirtualMachine
-        fields = [
-            'id', 'name', 'status', 'cluster', 'role', 'tenant', 'platform', 'primary_ip4', 'primary_ip6', 'vcpus',
-            'memory', 'disk', 'comments', 'custom_fields', 'created', 'last_updated',
-        ]
-
-
 #
 # VM interfaces
 #
 
-class InterfaceSerializer(serializers.ModelSerializer):
+class InterfaceSerializer(ValidatedModelSerializer):
     virtual_machine = NestedVirtualMachineSerializer()
+    form_factor = serializers.IntegerField(default=IFACE_FF_VIRTUAL)
 
     class Meta:
         model = Interface
@@ -143,19 +127,9 @@ class InterfaceSerializer(serializers.ModelSerializer):
         ]
 
 
-class NestedInterfaceSerializer(serializers.ModelSerializer):
+class NestedInterfaceSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:interface-detail')
 
     class Meta:
         model = Interface
         fields = ['id', 'url', 'name']
-
-
-class WritableInterfaceSerializer(ValidatedModelSerializer):
-    form_factor = serializers.IntegerField(default=IFACE_FF_VIRTUAL)
-
-    class Meta:
-        model = Interface
-        fields = [
-            'id', 'name', 'virtual_machine', 'form_factor', 'enabled', 'mac_address', 'mtu', 'description',
-        ]

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

@@ -37,7 +37,6 @@ class ClusterGroupViewSet(ModelViewSet):
 class ClusterViewSet(CustomFieldModelViewSet):
     queryset = Cluster.objects.select_related('type', 'group')
     serializer_class = serializers.ClusterSerializer
-    write_serializer_class = serializers.WritableClusterSerializer
     filter_class = filters.ClusterFilter
 
 
@@ -48,12 +47,10 @@ class ClusterViewSet(CustomFieldModelViewSet):
 class VirtualMachineViewSet(CustomFieldModelViewSet):
     queryset = VirtualMachine.objects.all()
     serializer_class = serializers.VirtualMachineSerializer
-    write_serializer_class = serializers.WritableVirtualMachineSerializer
     filter_class = filters.VirtualMachineFilter
 
 
 class InterfaceViewSet(ModelViewSet):
     queryset = Interface.objects.filter(virtual_machine__isnull=False).select_related('virtual_machine')
     serializer_class = serializers.InterfaceSerializer
-    write_serializer_class = serializers.WritableInterfaceSerializer
     filter_class = filters.InterfaceFilter