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

Merge pull request #2024 from digitalocean/1794-writable-nested-serializers

1794 writable nested serializers
Jeremy Stretch 7 лет назад
Родитель
Сommit
7805848e6c

+ 8 - 37
netbox/circuits/api/serializers.py

@@ -7,7 +7,7 @@ from circuits.models import Provider, Circuit, CircuitTermination, CircuitType
 from dcim.api.serializers import NestedSiteSerializer, InterfaceSerializer
 from dcim.api.serializers import NestedSiteSerializer, InterfaceSerializer
 from extras.api.customfields import CustomFieldModelSerializer
 from extras.api.customfields import CustomFieldModelSerializer
 from tenancy.api.serializers import NestedTenantSerializer
 from tenancy.api.serializers import NestedTenantSerializer
-from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer
+from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer, WritableNestedSerializer
 
 
 
 
 #
 #
@@ -24,7 +24,7 @@ class ProviderSerializer(CustomFieldModelSerializer):
         ]
         ]
 
 
 
 
-class NestedProviderSerializer(serializers.ModelSerializer):
+class NestedProviderSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
     url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
 
 
     class Meta:
     class Meta:
@@ -32,16 +32,6 @@ class NestedProviderSerializer(serializers.ModelSerializer):
         fields = ['id', 'url', 'name', 'slug']
         fields = ['id', 'url', 'name', 'slug']
 
 
 
 
-class WritableProviderSerializer(CustomFieldModelSerializer):
-
-    class Meta:
-        model = Provider
-        fields = [
-            'id', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments',
-            'custom_fields', 'created', 'last_updated',
-        ]
-
-
 #
 #
 # Circuit types
 # Circuit types
 #
 #
@@ -53,7 +43,7 @@ class CircuitTypeSerializer(ValidatedModelSerializer):
         fields = ['id', 'name', 'slug']
         fields = ['id', 'name', 'slug']
 
 
 
 
-class NestedCircuitTypeSerializer(serializers.ModelSerializer):
+class NestedCircuitTypeSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
     url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
 
 
     class Meta:
     class Meta:
@@ -67,9 +57,9 @@ class NestedCircuitTypeSerializer(serializers.ModelSerializer):
 
 
 class CircuitSerializer(CustomFieldModelSerializer):
 class CircuitSerializer(CustomFieldModelSerializer):
     provider = NestedProviderSerializer()
     provider = NestedProviderSerializer()
-    status = ChoiceFieldSerializer(choices=CIRCUIT_STATUS_CHOICES)
+    status = ChoiceFieldSerializer(choices=CIRCUIT_STATUS_CHOICES, required=False)
     type = NestedCircuitTypeSerializer()
     type = NestedCircuitTypeSerializer()
-    tenant = NestedTenantSerializer()
+    tenant = NestedTenantSerializer(required=False, allow_null=True)
 
 
     class Meta:
     class Meta:
         model = Circuit
         model = Circuit
@@ -79,7 +69,7 @@ class CircuitSerializer(CustomFieldModelSerializer):
         ]
         ]
 
 
 
 
-class NestedCircuitSerializer(serializers.ModelSerializer):
+class NestedCircuitSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
     url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
 
 
     class Meta:
     class Meta:
@@ -87,33 +77,14 @@ class NestedCircuitSerializer(serializers.ModelSerializer):
         fields = ['id', 'url', 'cid']
         fields = ['id', 'url', 'cid']
 
 
 
 
-class WritableCircuitSerializer(CustomFieldModelSerializer):
-
-    class Meta:
-        model = Circuit
-        fields = [
-            'id', 'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description',
-            'comments', 'custom_fields', 'created', 'last_updated',
-        ]
-
-
 #
 #
 # Circuit Terminations
 # Circuit Terminations
 #
 #
 
 
-class CircuitTerminationSerializer(serializers.ModelSerializer):
+class CircuitTerminationSerializer(ValidatedModelSerializer):
     circuit = NestedCircuitSerializer()
     circuit = NestedCircuitSerializer()
     site = NestedSiteSerializer()
     site = NestedSiteSerializer()
-    interface = InterfaceSerializer()
-
-    class Meta:
-        model = CircuitTermination
-        fields = [
-            'id', 'circuit', 'term_side', 'site', 'interface', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info',
-        ]
-
-
-class WritableCircuitTerminationSerializer(ValidatedModelSerializer):
+    interface = InterfaceSerializer(required=False, allow_null=True)
 
 
     class Meta:
     class Meta:
         model = CircuitTermination
         model = CircuitTermination

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

@@ -30,7 +30,6 @@ class CircuitsFieldChoicesViewSet(FieldChoicesViewSet):
 class ProviderViewSet(CustomFieldModelViewSet):
 class ProviderViewSet(CustomFieldModelViewSet):
     queryset = Provider.objects.all()
     queryset = Provider.objects.all()
     serializer_class = serializers.ProviderSerializer
     serializer_class = serializers.ProviderSerializer
-    write_serializer_class = serializers.WritableProviderSerializer
     filter_class = filters.ProviderFilter
     filter_class = filters.ProviderFilter
 
 
     @detail_route()
     @detail_route()
@@ -61,7 +60,6 @@ class CircuitTypeViewSet(ModelViewSet):
 class CircuitViewSet(CustomFieldModelViewSet):
 class CircuitViewSet(CustomFieldModelViewSet):
     queryset = Circuit.objects.select_related('type', 'tenant', 'provider')
     queryset = Circuit.objects.select_related('type', 'tenant', 'provider')
     serializer_class = serializers.CircuitSerializer
     serializer_class = serializers.CircuitSerializer
-    write_serializer_class = serializers.WritableCircuitSerializer
     filter_class = filters.CircuitFilter
     filter_class = filters.CircuitFilter
 
 
 
 
@@ -72,5 +70,4 @@ class CircuitViewSet(CustomFieldModelViewSet):
 class CircuitTerminationViewSet(ModelViewSet):
 class CircuitTerminationViewSet(ModelViewSet):
     queryset = CircuitTermination.objects.select_related('circuit', 'site', 'interface__device')
     queryset = CircuitTermination.objects.select_related('circuit', 'site', 'interface__device')
     serializer_class = serializers.CircuitTerminationSerializer
     serializer_class = serializers.CircuitTerminationSerializer
-    write_serializer_class = serializers.WritableCircuitTerminationSerializer
     filter_class = filters.CircuitTerminationFilter
     filter_class = filters.CircuitTerminationFilter

+ 130 - 300
netbox/dcim/api/serializers.py

@@ -20,7 +20,9 @@ from extras.api.customfields import CustomFieldModelSerializer
 from ipam.models import IPAddress, VLAN
 from ipam.models import IPAddress, VLAN
 from tenancy.api.serializers import NestedTenantSerializer
 from tenancy.api.serializers import NestedTenantSerializer
 from users.api.serializers import NestedUserSerializer
 from users.api.serializers import NestedUserSerializer
-from utilities.api import ChoiceFieldSerializer, TimeZoneField, ValidatedModelSerializer
+from utilities.api import (
+    ChoiceFieldSerializer, SerializedPKRelatedField, TimeZoneField, ValidatedModelSerializer, WritableNestedSerializer,
+)
 from virtualization.models import Cluster
 from virtualization.models import Cluster
 
 
 
 
@@ -28,7 +30,7 @@ from virtualization.models import Cluster
 # Regions
 # Regions
 #
 #
 
 
-class NestedRegionSerializer(serializers.ModelSerializer):
+class NestedRegionSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail')
 
 
     class Meta:
     class Meta:
@@ -37,14 +39,7 @@ class NestedRegionSerializer(serializers.ModelSerializer):
 
 
 
 
 class RegionSerializer(serializers.ModelSerializer):
 class RegionSerializer(serializers.ModelSerializer):
-    parent = NestedRegionSerializer()
-
-    class Meta:
-        model = Region
-        fields = ['id', 'name', 'slug', 'parent']
-
-
-class WritableRegionSerializer(ValidatedModelSerializer):
+    parent = NestedRegionSerializer(required=False, allow_null=True)
 
 
     class Meta:
     class Meta:
         model = Region
         model = Region
@@ -56,9 +51,9 @@ class WritableRegionSerializer(ValidatedModelSerializer):
 #
 #
 
 
 class SiteSerializer(CustomFieldModelSerializer):
 class SiteSerializer(CustomFieldModelSerializer):
-    status = ChoiceFieldSerializer(choices=SITE_STATUS_CHOICES)
-    region = NestedRegionSerializer()
-    tenant = NestedTenantSerializer()
+    status = ChoiceFieldSerializer(choices=SITE_STATUS_CHOICES, required=False)
+    region = NestedRegionSerializer(required=False, allow_null=True)
+    tenant = NestedTenantSerializer(required=False, allow_null=True)
     time_zone = TimeZoneField(required=False)
     time_zone = TimeZoneField(required=False)
 
 
     class Meta:
     class Meta:
@@ -71,7 +66,7 @@ class SiteSerializer(CustomFieldModelSerializer):
         ]
         ]
 
 
 
 
-class NestedSiteSerializer(serializers.ModelSerializer):
+class NestedSiteSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:site-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:site-detail')
 
 
     class Meta:
     class Meta:
@@ -79,23 +74,11 @@ class NestedSiteSerializer(serializers.ModelSerializer):
         fields = ['id', 'url', 'name', 'slug']
         fields = ['id', 'url', 'name', 'slug']
 
 
 
 
-class WritableSiteSerializer(CustomFieldModelSerializer):
-    time_zone = TimeZoneField(required=False)
-
-    class Meta:
-        model = Site
-        fields = [
-            'id', 'name', 'slug', 'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'description',
-            'physical_address', 'shipping_address', 'contact_name', 'contact_phone', 'contact_email', 'comments',
-            'custom_fields', 'created', 'last_updated',
-        ]
-
-
 #
 #
 # Rack groups
 # Rack groups
 #
 #
 
 
-class RackGroupSerializer(serializers.ModelSerializer):
+class RackGroupSerializer(ValidatedModelSerializer):
     site = NestedSiteSerializer()
     site = NestedSiteSerializer()
 
 
     class Meta:
     class Meta:
@@ -103,7 +86,7 @@ class RackGroupSerializer(serializers.ModelSerializer):
         fields = ['id', 'name', 'slug', 'site']
         fields = ['id', 'name', 'slug', 'site']
 
 
 
 
-class NestedRackGroupSerializer(serializers.ModelSerializer):
+class NestedRackGroupSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackgroup-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackgroup-detail')
 
 
     class Meta:
     class Meta:
@@ -111,13 +94,6 @@ class NestedRackGroupSerializer(serializers.ModelSerializer):
         fields = ['id', 'url', 'name', 'slug']
         fields = ['id', 'url', 'name', 'slug']
 
 
 
 
-class WritableRackGroupSerializer(ValidatedModelSerializer):
-
-    class Meta:
-        model = RackGroup
-        fields = ['id', 'name', 'slug', 'site']
-
-
 #
 #
 # Rack roles
 # Rack roles
 #
 #
@@ -129,7 +105,7 @@ class RackRoleSerializer(ValidatedModelSerializer):
         fields = ['id', 'name', 'slug', 'color']
         fields = ['id', 'name', 'slug', 'color']
 
 
 
 
-class NestedRackRoleSerializer(serializers.ModelSerializer):
+class NestedRackRoleSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
 
 
     class Meta:
     class Meta:
@@ -143,11 +119,11 @@ class NestedRackRoleSerializer(serializers.ModelSerializer):
 
 
 class RackSerializer(CustomFieldModelSerializer):
 class RackSerializer(CustomFieldModelSerializer):
     site = NestedSiteSerializer()
     site = NestedSiteSerializer()
-    group = NestedRackGroupSerializer()
-    tenant = NestedTenantSerializer()
-    role = NestedRackRoleSerializer()
-    type = ChoiceFieldSerializer(choices=RACK_TYPE_CHOICES)
-    width = ChoiceFieldSerializer(choices=RACK_WIDTH_CHOICES)
+    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)
 
 
     class Meta:
     class Meta:
         model = Rack
         model = Rack
@@ -155,24 +131,6 @@ class RackSerializer(CustomFieldModelSerializer):
             'id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'role', 'serial', 'type', 'width',
             'id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'role', 'serial', 'type', 'width',
             'u_height', 'desc_units', 'comments', 'custom_fields', 'created', 'last_updated',
             'u_height', 'desc_units', 'comments', 'custom_fields', 'created', 'last_updated',
         ]
         ]
-
-
-class NestedRackSerializer(serializers.ModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail')
-
-    class Meta:
-        model = Rack
-        fields = ['id', 'url', 'name', 'display_name']
-
-
-class WritableRackSerializer(CustomFieldModelSerializer):
-
-    class Meta:
-        model = Rack
-        fields = [
-            'id', 'name', 'facility_id', 'site', 'group', 'tenant', 'role', 'serial', 'type', 'width', 'u_height',
-            'desc_units', 'comments', 'custom_fields', 'created', 'last_updated',
-        ]
         # Omit the UniqueTogetherValidator that would be automatically added to validate (site, facility_id). This
         # Omit the UniqueTogetherValidator that would be automatically added to validate (site, facility_id). This
         # prevents facility_id from being interpreted as a required field.
         # prevents facility_id from being interpreted as a required field.
         validators = [
         validators = [
@@ -188,16 +146,24 @@ class WritableRackSerializer(CustomFieldModelSerializer):
             validator(data)
             validator(data)
 
 
         # Enforce model validation
         # Enforce model validation
-        super(WritableRackSerializer, self).validate(data)
+        super(RackSerializer, self).validate(data)
 
 
         return data
         return data
 
 
 
 
+class NestedRackSerializer(WritableNestedSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail')
+
+    class Meta:
+        model = Rack
+        fields = ['id', 'url', 'name', 'display_name']
+
+
 #
 #
 # Rack units
 # Rack units
 #
 #
 
 
-class NestedDeviceSerializer(serializers.ModelSerializer):
+class NestedDeviceSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail')
 
 
     class Meta:
     class Meta:
@@ -219,23 +185,16 @@ class RackUnitSerializer(serializers.Serializer):
 # Rack reservations
 # Rack reservations
 #
 #
 
 
-class RackReservationSerializer(serializers.ModelSerializer):
+class RackReservationSerializer(ValidatedModelSerializer):
     rack = NestedRackSerializer()
     rack = NestedRackSerializer()
     user = NestedUserSerializer()
     user = NestedUserSerializer()
-    tenant = NestedTenantSerializer()
+    tenant = NestedTenantSerializer(required=False, allow_null=True)
 
 
     class Meta:
     class Meta:
         model = RackReservation
         model = RackReservation
         fields = ['id', 'rack', 'units', 'created', 'user', 'tenant', 'description']
         fields = ['id', 'rack', 'units', 'created', 'user', 'tenant', 'description']
 
 
 
 
-class WritableRackReservationSerializer(ValidatedModelSerializer):
-
-    class Meta:
-        model = RackReservation
-        fields = ['id', 'rack', 'units', 'user', 'description']
-
-
 #
 #
 # Manufacturers
 # Manufacturers
 #
 #
@@ -247,7 +206,7 @@ class ManufacturerSerializer(ValidatedModelSerializer):
         fields = ['id', 'name', 'slug']
         fields = ['id', 'name', 'slug']
 
 
 
 
-class NestedManufacturerSerializer(serializers.ModelSerializer):
+class NestedManufacturerSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
 
 
     class Meta:
     class Meta:
@@ -261,8 +220,8 @@ class NestedManufacturerSerializer(serializers.ModelSerializer):
 
 
 class DeviceTypeSerializer(CustomFieldModelSerializer):
 class DeviceTypeSerializer(CustomFieldModelSerializer):
     manufacturer = NestedManufacturerSerializer()
     manufacturer = NestedManufacturerSerializer()
-    interface_ordering = ChoiceFieldSerializer(choices=IFACE_ORDERING_CHOICES)
-    subdevice_role = ChoiceFieldSerializer(choices=SUBDEVICE_ROLE_CHOICES)
+    interface_ordering = ChoiceFieldSerializer(choices=IFACE_ORDERING_CHOICES, required=False)
+    subdevice_role = ChoiceFieldSerializer(choices=SUBDEVICE_ROLE_CHOICES, required=False)
     instance_count = serializers.IntegerField(source='instances.count', read_only=True)
     instance_count = serializers.IntegerField(source='instances.count', read_only=True)
 
 
     class Meta:
     class Meta:
@@ -274,30 +233,20 @@ class DeviceTypeSerializer(CustomFieldModelSerializer):
         ]
         ]
 
 
 
 
-class NestedDeviceTypeSerializer(serializers.ModelSerializer):
+class NestedDeviceTypeSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail')
-    manufacturer = NestedManufacturerSerializer()
+    manufacturer = NestedManufacturerSerializer(read_only=True)
 
 
     class Meta:
     class Meta:
         model = DeviceType
         model = DeviceType
         fields = ['id', 'url', 'manufacturer', 'model', 'slug']
         fields = ['id', 'url', 'manufacturer', 'model', 'slug']
 
 
 
 
-class WritableDeviceTypeSerializer(CustomFieldModelSerializer):
-
-    class Meta:
-        model = DeviceType
-        fields = [
-            'id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'interface_ordering',
-            'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role', 'comments', 'custom_fields',
-        ]
-
-
 #
 #
 # Console port templates
 # Console port templates
 #
 #
 
 
-class ConsolePortTemplateSerializer(serializers.ModelSerializer):
+class ConsolePortTemplateSerializer(ValidatedModelSerializer):
     device_type = NestedDeviceTypeSerializer()
     device_type = NestedDeviceTypeSerializer()
 
 
     class Meta:
     class Meta:
@@ -305,18 +254,11 @@ class ConsolePortTemplateSerializer(serializers.ModelSerializer):
         fields = ['id', 'device_type', 'name']
         fields = ['id', 'device_type', 'name']
 
 
 
 
-class WritableConsolePortTemplateSerializer(ValidatedModelSerializer):
-
-    class Meta:
-        model = ConsolePortTemplate
-        fields = ['id', 'device_type', 'name']
-
-
 #
 #
 # Console server port templates
 # Console server port templates
 #
 #
 
 
-class ConsoleServerPortTemplateSerializer(serializers.ModelSerializer):
+class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
     device_type = NestedDeviceTypeSerializer()
     device_type = NestedDeviceTypeSerializer()
 
 
     class Meta:
     class Meta:
@@ -324,18 +266,11 @@ class ConsoleServerPortTemplateSerializer(serializers.ModelSerializer):
         fields = ['id', 'device_type', 'name']
         fields = ['id', 'device_type', 'name']
 
 
 
 
-class WritableConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
-
-    class Meta:
-        model = ConsoleServerPortTemplate
-        fields = ['id', 'device_type', 'name']
-
-
 #
 #
 # Power port templates
 # Power port templates
 #
 #
 
 
-class PowerPortTemplateSerializer(serializers.ModelSerializer):
+class PowerPortTemplateSerializer(ValidatedModelSerializer):
     device_type = NestedDeviceTypeSerializer()
     device_type = NestedDeviceTypeSerializer()
 
 
     class Meta:
     class Meta:
@@ -343,18 +278,11 @@ class PowerPortTemplateSerializer(serializers.ModelSerializer):
         fields = ['id', 'device_type', 'name']
         fields = ['id', 'device_type', 'name']
 
 
 
 
-class WritablePowerPortTemplateSerializer(ValidatedModelSerializer):
-
-    class Meta:
-        model = PowerPortTemplate
-        fields = ['id', 'device_type', 'name']
-
-
 #
 #
 # Power outlet templates
 # Power outlet templates
 #
 #
 
 
-class PowerOutletTemplateSerializer(serializers.ModelSerializer):
+class PowerOutletTemplateSerializer(ValidatedModelSerializer):
     device_type = NestedDeviceTypeSerializer()
     device_type = NestedDeviceTypeSerializer()
 
 
     class Meta:
     class Meta:
@@ -362,27 +290,13 @@ class PowerOutletTemplateSerializer(serializers.ModelSerializer):
         fields = ['id', 'device_type', 'name']
         fields = ['id', 'device_type', 'name']
 
 
 
 
-class WritablePowerOutletTemplateSerializer(ValidatedModelSerializer):
-
-    class Meta:
-        model = PowerOutletTemplate
-        fields = ['id', 'device_type', 'name']
-
-
 #
 #
 # Interface templates
 # Interface templates
 #
 #
 
 
-class InterfaceTemplateSerializer(serializers.ModelSerializer):
+class InterfaceTemplateSerializer(ValidatedModelSerializer):
     device_type = NestedDeviceTypeSerializer()
     device_type = NestedDeviceTypeSerializer()
-    form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES)
-
-    class Meta:
-        model = InterfaceTemplate
-        fields = ['id', 'device_type', 'name', 'form_factor', 'mgmt_only']
-
-
-class WritableInterfaceTemplateSerializer(ValidatedModelSerializer):
+    form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES, required=False)
 
 
     class Meta:
     class Meta:
         model = InterfaceTemplate
         model = InterfaceTemplate
@@ -393,7 +307,7 @@ class WritableInterfaceTemplateSerializer(ValidatedModelSerializer):
 # Device bay templates
 # Device bay templates
 #
 #
 
 
-class DeviceBayTemplateSerializer(serializers.ModelSerializer):
+class DeviceBayTemplateSerializer(ValidatedModelSerializer):
     device_type = NestedDeviceTypeSerializer()
     device_type = NestedDeviceTypeSerializer()
 
 
     class Meta:
     class Meta:
@@ -401,13 +315,6 @@ class DeviceBayTemplateSerializer(serializers.ModelSerializer):
         fields = ['id', 'device_type', 'name']
         fields = ['id', 'device_type', 'name']
 
 
 
 
-class WritableDeviceBayTemplateSerializer(ValidatedModelSerializer):
-
-    class Meta:
-        model = DeviceBayTemplate
-        fields = ['id', 'device_type', 'name']
-
-
 #
 #
 # Device roles
 # Device roles
 #
 #
@@ -419,7 +326,7 @@ class DeviceRoleSerializer(ValidatedModelSerializer):
         fields = ['id', 'name', 'slug', 'color', 'vm_role']
         fields = ['id', 'name', 'slug', 'color', 'vm_role']
 
 
 
 
-class NestedDeviceRoleSerializer(serializers.ModelSerializer):
+class NestedDeviceRoleSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
 
 
     class Meta:
     class Meta:
@@ -431,15 +338,15 @@ class NestedDeviceRoleSerializer(serializers.ModelSerializer):
 # Platforms
 # Platforms
 #
 #
 
 
-class PlatformSerializer(serializers.ModelSerializer):
-    manufacturer = NestedManufacturerSerializer()
+class PlatformSerializer(ValidatedModelSerializer):
+    manufacturer = NestedManufacturerSerializer(required=False, allow_null=True)
 
 
     class Meta:
     class Meta:
         model = Platform
         model = Platform
         fields = ['id', 'name', 'slug', 'manufacturer', 'napalm_driver', 'rpc_client']
         fields = ['id', 'name', 'slug', 'manufacturer', 'napalm_driver', 'rpc_client']
 
 
 
 
-class NestedPlatformSerializer(serializers.ModelSerializer):
+class NestedPlatformSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
 
 
     class Meta:
     class Meta:
@@ -447,13 +354,6 @@ class NestedPlatformSerializer(serializers.ModelSerializer):
         fields = ['id', 'url', 'name', 'slug']
         fields = ['id', 'url', 'name', 'slug']
 
 
 
 
-class WritablePlatformSerializer(ValidatedModelSerializer):
-
-    class Meta:
-        model = Platform
-        fields = ['id', 'name', 'slug', 'manufacturer', 'napalm_driver', 'rpc_client']
-
-
 #
 #
 # Devices
 # Devices
 #
 #
@@ -489,18 +389,18 @@ class DeviceVirtualChassisSerializer(serializers.ModelSerializer):
 class DeviceSerializer(CustomFieldModelSerializer):
 class DeviceSerializer(CustomFieldModelSerializer):
     device_type = NestedDeviceTypeSerializer()
     device_type = NestedDeviceTypeSerializer()
     device_role = NestedDeviceRoleSerializer()
     device_role = NestedDeviceRoleSerializer()
-    tenant = NestedTenantSerializer()
-    platform = NestedPlatformSerializer()
+    tenant = NestedTenantSerializer(required=False, allow_null=True)
+    platform = NestedPlatformSerializer(required=False, allow_null=True)
     site = NestedSiteSerializer()
     site = NestedSiteSerializer()
-    rack = NestedRackSerializer()
-    face = ChoiceFieldSerializer(choices=RACK_FACE_CHOICES)
-    status = ChoiceFieldSerializer(choices=DEVICE_STATUS_CHOICES)
-    primary_ip = DeviceIPAddressSerializer()
-    primary_ip4 = DeviceIPAddressSerializer()
-    primary_ip6 = DeviceIPAddressSerializer()
+    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, allow_null=True)
+    primary_ip6 = DeviceIPAddressSerializer(required=False, allow_null=True)
     parent_device = serializers.SerializerMethodField()
     parent_device = serializers.SerializerMethodField()
-    cluster = NestedClusterSerializer()
-    virtual_chassis = DeviceVirtualChassisSerializer()
+    cluster = NestedClusterSerializer(required=False, allow_null=True)
+    virtual_chassis = DeviceVirtualChassisSerializer(required=False, allow_null=True)
 
 
     class Meta:
     class Meta:
         model = Device
         model = Device
@@ -510,27 +410,6 @@ class DeviceSerializer(CustomFieldModelSerializer):
             'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'custom_fields', 'created',
             'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'custom_fields', 'created',
             'last_updated',
             'last_updated',
         ]
         ]
-
-    def get_parent_device(self, obj):
-        try:
-            device_bay = obj.parent_bay
-        except DeviceBay.DoesNotExist:
-            return None
-        context = {'request': self.context['request']}
-        data = NestedDeviceSerializer(instance=device_bay.device, context=context).data
-        data['device_bay'] = NestedDeviceBaySerializer(instance=device_bay, context=context).data
-        return data
-
-
-class WritableDeviceSerializer(CustomFieldModelSerializer):
-
-    class Meta:
-        model = Device
-        fields = [
-            'id', 'name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag', 'site', 'rack',
-            'position', 'face', 'status', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position',
-            'vc_priority', 'comments', 'custom_fields', 'created', 'last_updated',
-        ]
         validators = []
         validators = []
 
 
     def validate(self, data):
     def validate(self, data):
@@ -542,8 +421,18 @@ class WritableDeviceSerializer(CustomFieldModelSerializer):
             validator(data)
             validator(data)
 
 
         # Enforce model validation
         # Enforce model validation
-        super(WritableDeviceSerializer, self).validate(data)
+        super(DeviceSerializer, self).validate(data)
+
+        return data
 
 
+    def get_parent_device(self, obj):
+        try:
+            device_bay = obj.parent_bay
+        except DeviceBay.DoesNotExist:
+            return None
+        context = {'request': self.context['request']}
+        data = NestedDeviceSerializer(instance=device_bay.device, context=context).data
+        data['device_bay'] = NestedDeviceBaySerializer(instance=device_bay, context=context).data
         return data
         return data
 
 
 
 
@@ -551,7 +440,7 @@ class WritableDeviceSerializer(CustomFieldModelSerializer):
 # Console server ports
 # Console server ports
 #
 #
 
 
-class ConsoleServerPortSerializer(serializers.ModelSerializer):
+class ConsoleServerPortSerializer(ValidatedModelSerializer):
     device = NestedDeviceSerializer()
     device = NestedDeviceSerializer()
 
 
     class Meta:
     class Meta:
@@ -560,27 +449,22 @@ class ConsoleServerPortSerializer(serializers.ModelSerializer):
         read_only_fields = ['connected_console']
         read_only_fields = ['connected_console']
 
 
 
 
-class WritableConsoleServerPortSerializer(ValidatedModelSerializer):
+class NestedConsoleServerPortSerializer(WritableNestedSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
+    device = NestedDeviceSerializer(read_only=True)
 
 
     class Meta:
     class Meta:
         model = ConsoleServerPort
         model = ConsoleServerPort
-        fields = ['id', 'device', 'name']
+        fields = ['id', 'url', 'device', 'name']
 
 
 
 
 #
 #
 # Console ports
 # Console ports
 #
 #
 
 
-class ConsolePortSerializer(serializers.ModelSerializer):
+class ConsolePortSerializer(ValidatedModelSerializer):
     device = NestedDeviceSerializer()
     device = NestedDeviceSerializer()
-    cs_port = ConsoleServerPortSerializer()
-
-    class Meta:
-        model = ConsolePort
-        fields = ['id', 'device', 'name', 'cs_port', 'connection_status']
-
-
-class WritableConsolePortSerializer(ValidatedModelSerializer):
+    cs_port = NestedConsoleServerPortSerializer(required=False, allow_null=True)
 
 
     class Meta:
     class Meta:
         model = ConsolePort
         model = ConsolePort
@@ -591,7 +475,7 @@ class WritableConsolePortSerializer(ValidatedModelSerializer):
 # Power outlets
 # Power outlets
 #
 #
 
 
-class PowerOutletSerializer(serializers.ModelSerializer):
+class PowerOutletSerializer(ValidatedModelSerializer):
     device = NestedDeviceSerializer()
     device = NestedDeviceSerializer()
 
 
     class Meta:
     class Meta:
@@ -600,27 +484,22 @@ class PowerOutletSerializer(serializers.ModelSerializer):
         read_only_fields = ['connected_port']
         read_only_fields = ['connected_port']
 
 
 
 
-class WritablePowerOutletSerializer(ValidatedModelSerializer):
+class NestedPowerOutletSerializer(WritableNestedSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail')
+    device = NestedDeviceSerializer(read_only=True)
 
 
     class Meta:
     class Meta:
         model = PowerOutlet
         model = PowerOutlet
-        fields = ['id', 'device', 'name']
+        fields = ['id', 'url', 'device', 'name']
 
 
 
 
 #
 #
 # Power ports
 # Power ports
 #
 #
 
 
-class PowerPortSerializer(serializers.ModelSerializer):
+class PowerPortSerializer(ValidatedModelSerializer):
     device = NestedDeviceSerializer()
     device = NestedDeviceSerializer()
-    power_outlet = PowerOutletSerializer()
-
-    class Meta:
-        model = PowerPort
-        fields = ['id', 'device', 'name', 'power_outlet', 'connection_status']
-
-
-class WritablePowerPortSerializer(ValidatedModelSerializer):
+    power_outlet = NestedPowerOutletSerializer(required=False, allow_null=True)
 
 
     class Meta:
     class Meta:
         model = PowerPort
         model = PowerPort
@@ -631,12 +510,13 @@ class WritablePowerPortSerializer(ValidatedModelSerializer):
 # Interfaces
 # Interfaces
 #
 #
 
 
-class NestedInterfaceSerializer(serializers.ModelSerializer):
+class NestedInterfaceSerializer(WritableNestedSerializer):
+    device = NestedDeviceSerializer(read_only=True)
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
 
 
     class Meta:
     class Meta:
         model = Interface
         model = Interface
-        fields = ['id', 'url', 'name']
+        fields = ['id', 'url', 'device', 'name']
 
 
 
 
 class InterfaceNestedCircuitSerializer(serializers.ModelSerializer):
 class InterfaceNestedCircuitSerializer(serializers.ModelSerializer):
@@ -647,8 +527,8 @@ class InterfaceNestedCircuitSerializer(serializers.ModelSerializer):
         fields = ['id', 'url', 'cid']
         fields = ['id', 'url', 'cid']
 
 
 
 
-class InterfaceCircuitTerminationSerializer(serializers.ModelSerializer):
-    circuit = InterfaceNestedCircuitSerializer()
+class InterfaceCircuitTerminationSerializer(WritableNestedSerializer):
+    circuit = InterfaceNestedCircuitSerializer(read_only=True)
 
 
     class Meta:
     class Meta:
         model = CircuitTermination
         model = CircuitTermination
@@ -658,7 +538,7 @@ class InterfaceCircuitTerminationSerializer(serializers.ModelSerializer):
 
 
 
 
 # Cannot import ipam.api.NestedVLANSerializer due to circular dependency
 # Cannot import ipam.api.NestedVLANSerializer due to circular dependency
-class InterfaceVLANSerializer(serializers.ModelSerializer):
+class InterfaceVLANSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail')
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail')
 
 
     class Meta:
     class Meta:
@@ -666,16 +546,21 @@ class InterfaceVLANSerializer(serializers.ModelSerializer):
         fields = ['id', 'url', 'vid', 'name', 'display_name']
         fields = ['id', 'url', 'vid', 'name', 'display_name']
 
 
 
 
-class InterfaceSerializer(serializers.ModelSerializer):
+class InterfaceSerializer(ValidatedModelSerializer):
     device = NestedDeviceSerializer()
     device = NestedDeviceSerializer()
-    form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES)
-    lag = NestedInterfaceSerializer()
+    form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES, required=False)
+    lag = NestedInterfaceSerializer(required=False, allow_null=True)
     is_connected = serializers.SerializerMethodField(read_only=True)
     is_connected = serializers.SerializerMethodField(read_only=True)
     interface_connection = serializers.SerializerMethodField(read_only=True)
     interface_connection = serializers.SerializerMethodField(read_only=True)
-    circuit_termination = InterfaceCircuitTerminationSerializer()
-    untagged_vlan = InterfaceVLANSerializer()
-    mode = ChoiceFieldSerializer(choices=IFACE_MODE_CHOICES)
-    tagged_vlans = InterfaceVLANSerializer(many=True)
+    circuit_termination = InterfaceCircuitTerminationSerializer(read_only=True)
+    mode = ChoiceFieldSerializer(choices=IFACE_MODE_CHOICES, required=False)
+    untagged_vlan = InterfaceVLANSerializer(required=False, allow_null=True)
+    tagged_vlans = SerializedPKRelatedField(
+        queryset=VLAN.objects.all(),
+        serializer=InterfaceVLANSerializer,
+        required=False,
+        many=True
+    )
 
 
     class Meta:
     class Meta:
         model = Interface
         model = Interface
@@ -684,6 +569,25 @@ class InterfaceSerializer(serializers.ModelSerializer):
             'is_connected', 'interface_connection', 'circuit_termination', 'mode', 'untagged_vlan', 'tagged_vlans',
             'is_connected', 'interface_connection', 'circuit_termination', 'mode', 'untagged_vlan', 'tagged_vlans',
         ]
         ]
 
 
+    def validate(self, data):
+
+        # All associated VLANs be global or assigned to the parent device's site.
+        device = self.instance.device if self.instance else data.get('device')
+        untagged_vlan = data.get('untagged_vlan')
+        if untagged_vlan and untagged_vlan.site not in [device.site, None]:
+            raise serializers.ValidationError({
+                'untagged_vlan': "VLAN {} must belong to the same site as the interface's parent device, or it must be "
+                                 "global.".format(untagged_vlan)
+            })
+        for vlan in data.get('tagged_vlans', []):
+            if vlan.site not in [device.site, None]:
+                raise serializers.ValidationError({
+                    'tagged_vlans': "VLAN {} must belong to the same site as the interface's parent device, or it must "
+                                    "be global.".format(vlan)
+                })
+
+        return super(InterfaceSerializer, self).validate(data)
+
     def get_is_connected(self, obj):
     def get_is_connected(self, obj):
         """
         """
         Return True if the interface has a connected interface or circuit termination.
         Return True if the interface has a connected interface or circuit termination.
@@ -700,69 +604,26 @@ class InterfaceSerializer(serializers.ModelSerializer):
     def get_interface_connection(self, obj):
     def get_interface_connection(self, obj):
         if obj.connection:
         if obj.connection:
             return OrderedDict((
             return OrderedDict((
-                ('interface', PeerInterfaceSerializer(obj.connected_interface, context=self.context).data),
+                ('interface', NestedInterfaceSerializer(obj.connected_interface, context=self.context).data),
                 ('status', obj.connection.connection_status),
                 ('status', obj.connection.connection_status),
             ))
             ))
         return None
         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',
-        ]
-
-
-class WritableInterfaceSerializer(ValidatedModelSerializer):
-
-    class Meta:
-        model = Interface
-        fields = [
-            'id', 'device', 'name', 'form_factor', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', 'description',
-            'mode', 'untagged_vlan', 'tagged_vlans',
-        ]
-
-    def validate(self, data):
-
-        # All associated VLANs be global or assigned to the parent device's site.
-        device = self.instance.device if self.instance else data.get('device')
-        untagged_vlan = data.get('untagged_vlan')
-        if untagged_vlan and untagged_vlan.site not in [device.site, None]:
-            raise serializers.ValidationError({
-                'untagged_vlan': "VLAN {} must belong to the same site as the interface's parent device, or it must be "
-                                 "global.".format(untagged_vlan)
-            })
-        for vlan in data.get('tagged_vlans', []):
-            if vlan.site not in [device.site, None]:
-                raise serializers.ValidationError({
-                    'tagged_vlans': "VLAN {} must belong to the same site as the interface's parent device, or it must "
-                                    "be global.".format(vlan)
-                })
-
-        return super(WritableInterfaceSerializer, self).validate(data)
-
-
 #
 #
 # Device bays
 # Device bays
 #
 #
 
 
-class DeviceBaySerializer(serializers.ModelSerializer):
+class DeviceBaySerializer(ValidatedModelSerializer):
     device = NestedDeviceSerializer()
     device = NestedDeviceSerializer()
-    installed_device = NestedDeviceSerializer()
+    installed_device = NestedDeviceSerializer(required=False, allow_null=True)
 
 
     class Meta:
     class Meta:
         model = DeviceBay
         model = DeviceBay
         fields = ['id', 'device', 'name', 'installed_device']
         fields = ['id', 'device', 'name', 'installed_device']
 
 
 
 
-class NestedDeviceBaySerializer(serializers.ModelSerializer):
+class NestedDeviceBaySerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail')
 
 
     class Meta:
     class Meta:
@@ -770,32 +631,15 @@ class NestedDeviceBaySerializer(serializers.ModelSerializer):
         fields = ['id', 'url', 'name']
         fields = ['id', 'url', 'name']
 
 
 
 
-class WritableDeviceBaySerializer(ValidatedModelSerializer):
-
-    class Meta:
-        model = DeviceBay
-        fields = ['id', 'device', 'name', 'installed_device']
-
-
 #
 #
 # Inventory items
 # Inventory items
 #
 #
 
 
-class InventoryItemSerializer(serializers.ModelSerializer):
+class InventoryItemSerializer(ValidatedModelSerializer):
     device = NestedDeviceSerializer()
     device = NestedDeviceSerializer()
-    manufacturer = NestedManufacturerSerializer()
-
-    class Meta:
-        model = InventoryItem
-        fields = [
-            'id', 'device', 'parent', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered',
-            'description',
-        ]
-
-
-class WritableInventoryItemSerializer(ValidatedModelSerializer):
     # Provide a default value to satisfy UniqueTogetherValidator
     # Provide a default value to satisfy UniqueTogetherValidator
     parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None)
     parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None)
+    manufacturer = NestedManufacturerSerializer()
 
 
     class Meta:
     class Meta:
         model = InventoryItem
         model = InventoryItem
@@ -809,17 +653,17 @@ class WritableInventoryItemSerializer(ValidatedModelSerializer):
 # Interface connections
 # Interface connections
 #
 #
 
 
-class InterfaceConnectionSerializer(serializers.ModelSerializer):
-    interface_a = PeerInterfaceSerializer()
-    interface_b = PeerInterfaceSerializer()
-    connection_status = ChoiceFieldSerializer(choices=CONNECTION_STATUS_CHOICES)
+class InterfaceConnectionSerializer(ValidatedModelSerializer):
+    interface_a = NestedInterfaceSerializer()
+    interface_b = NestedInterfaceSerializer()
+    connection_status = ChoiceFieldSerializer(choices=CONNECTION_STATUS_CHOICES, required=False)
 
 
     class Meta:
     class Meta:
         model = InterfaceConnection
         model = InterfaceConnection
         fields = ['id', 'interface_a', 'interface_b', 'connection_status']
         fields = ['id', 'interface_a', 'interface_b', 'connection_status']
 
 
 
 
-class NestedInterfaceConnectionSerializer(serializers.ModelSerializer):
+class NestedInterfaceConnectionSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interfaceconnection-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interfaceconnection-detail')
 
 
     class Meta:
     class Meta:
@@ -827,18 +671,11 @@ class NestedInterfaceConnectionSerializer(serializers.ModelSerializer):
         fields = ['id', 'url', 'connection_status']
         fields = ['id', 'url', 'connection_status']
 
 
 
 
-class WritableInterfaceConnectionSerializer(ValidatedModelSerializer):
-
-    class Meta:
-        model = InterfaceConnection
-        fields = ['id', 'interface_a', 'interface_b', 'connection_status']
-
-
 #
 #
 # Virtual chassis
 # Virtual chassis
 #
 #
 
 
-class VirtualChassisSerializer(serializers.ModelSerializer):
+class VirtualChassisSerializer(ValidatedModelSerializer):
     master = NestedDeviceSerializer()
     master = NestedDeviceSerializer()
 
 
     class Meta:
     class Meta:
@@ -846,16 +683,9 @@ class VirtualChassisSerializer(serializers.ModelSerializer):
         fields = ['id', 'master', 'domain']
         fields = ['id', 'master', 'domain']
 
 
 
 
-class NestedVirtualChassisSerializer(serializers.ModelSerializer):
+class NestedVirtualChassisSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
 
 
     class Meta:
     class Meta:
         model = VirtualChassis
         model = VirtualChassis
         fields = ['id', 'url']
         fields = ['id', 'url']
-
-
-class WritableVirtualChassisSerializer(ValidatedModelSerializer):
-
-    class Meta:
-        model = VirtualChassis
-        fields = ['id', 'master', 'domain']

+ 0 - 23
netbox/dcim/api/views.py

@@ -52,7 +52,6 @@ class DCIMFieldChoicesViewSet(FieldChoicesViewSet):
 class RegionViewSet(ModelViewSet):
 class RegionViewSet(ModelViewSet):
     queryset = Region.objects.all()
     queryset = Region.objects.all()
     serializer_class = serializers.RegionSerializer
     serializer_class = serializers.RegionSerializer
-    write_serializer_class = serializers.WritableRegionSerializer
     filter_class = filters.RegionFilter
     filter_class = filters.RegionFilter
 
 
 
 
@@ -63,7 +62,6 @@ class RegionViewSet(ModelViewSet):
 class SiteViewSet(CustomFieldModelViewSet):
 class SiteViewSet(CustomFieldModelViewSet):
     queryset = Site.objects.select_related('region', 'tenant')
     queryset = Site.objects.select_related('region', 'tenant')
     serializer_class = serializers.SiteSerializer
     serializer_class = serializers.SiteSerializer
-    write_serializer_class = serializers.WritableSiteSerializer
     filter_class = filters.SiteFilter
     filter_class = filters.SiteFilter
 
 
     @detail_route()
     @detail_route()
@@ -84,7 +82,6 @@ class SiteViewSet(CustomFieldModelViewSet):
 class RackGroupViewSet(ModelViewSet):
 class RackGroupViewSet(ModelViewSet):
     queryset = RackGroup.objects.select_related('site')
     queryset = RackGroup.objects.select_related('site')
     serializer_class = serializers.RackGroupSerializer
     serializer_class = serializers.RackGroupSerializer
-    write_serializer_class = serializers.WritableRackGroupSerializer
     filter_class = filters.RackGroupFilter
     filter_class = filters.RackGroupFilter
 
 
 
 
@@ -105,7 +102,6 @@ class RackRoleViewSet(ModelViewSet):
 class RackViewSet(CustomFieldModelViewSet):
 class RackViewSet(CustomFieldModelViewSet):
     queryset = Rack.objects.select_related('site', 'group__site', 'tenant')
     queryset = Rack.objects.select_related('site', 'group__site', 'tenant')
     serializer_class = serializers.RackSerializer
     serializer_class = serializers.RackSerializer
-    write_serializer_class = serializers.WritableRackSerializer
     filter_class = filters.RackFilter
     filter_class = filters.RackFilter
 
 
     @detail_route()
     @detail_route()
@@ -136,7 +132,6 @@ class RackViewSet(CustomFieldModelViewSet):
 class RackReservationViewSet(ModelViewSet):
 class RackReservationViewSet(ModelViewSet):
     queryset = RackReservation.objects.select_related('rack', 'user', 'tenant')
     queryset = RackReservation.objects.select_related('rack', 'user', 'tenant')
     serializer_class = serializers.RackReservationSerializer
     serializer_class = serializers.RackReservationSerializer
-    write_serializer_class = serializers.WritableRackReservationSerializer
     filter_class = filters.RackReservationFilter
     filter_class = filters.RackReservationFilter
 
 
     # Assign user from request
     # Assign user from request
@@ -161,7 +156,6 @@ class ManufacturerViewSet(ModelViewSet):
 class DeviceTypeViewSet(CustomFieldModelViewSet):
 class DeviceTypeViewSet(CustomFieldModelViewSet):
     queryset = DeviceType.objects.select_related('manufacturer')
     queryset = DeviceType.objects.select_related('manufacturer')
     serializer_class = serializers.DeviceTypeSerializer
     serializer_class = serializers.DeviceTypeSerializer
-    write_serializer_class = serializers.WritableDeviceTypeSerializer
     filter_class = filters.DeviceTypeFilter
     filter_class = filters.DeviceTypeFilter
 
 
 
 
@@ -172,42 +166,36 @@ class DeviceTypeViewSet(CustomFieldModelViewSet):
 class ConsolePortTemplateViewSet(ModelViewSet):
 class ConsolePortTemplateViewSet(ModelViewSet):
     queryset = ConsolePortTemplate.objects.select_related('device_type__manufacturer')
     queryset = ConsolePortTemplate.objects.select_related('device_type__manufacturer')
     serializer_class = serializers.ConsolePortTemplateSerializer
     serializer_class = serializers.ConsolePortTemplateSerializer
-    write_serializer_class = serializers.WritableConsolePortTemplateSerializer
     filter_class = filters.ConsolePortTemplateFilter
     filter_class = filters.ConsolePortTemplateFilter
 
 
 
 
 class ConsoleServerPortTemplateViewSet(ModelViewSet):
 class ConsoleServerPortTemplateViewSet(ModelViewSet):
     queryset = ConsoleServerPortTemplate.objects.select_related('device_type__manufacturer')
     queryset = ConsoleServerPortTemplate.objects.select_related('device_type__manufacturer')
     serializer_class = serializers.ConsoleServerPortTemplateSerializer
     serializer_class = serializers.ConsoleServerPortTemplateSerializer
-    write_serializer_class = serializers.WritableConsoleServerPortTemplateSerializer
     filter_class = filters.ConsoleServerPortTemplateFilter
     filter_class = filters.ConsoleServerPortTemplateFilter
 
 
 
 
 class PowerPortTemplateViewSet(ModelViewSet):
 class PowerPortTemplateViewSet(ModelViewSet):
     queryset = PowerPortTemplate.objects.select_related('device_type__manufacturer')
     queryset = PowerPortTemplate.objects.select_related('device_type__manufacturer')
     serializer_class = serializers.PowerPortTemplateSerializer
     serializer_class = serializers.PowerPortTemplateSerializer
-    write_serializer_class = serializers.WritablePowerPortTemplateSerializer
     filter_class = filters.PowerPortTemplateFilter
     filter_class = filters.PowerPortTemplateFilter
 
 
 
 
 class PowerOutletTemplateViewSet(ModelViewSet):
 class PowerOutletTemplateViewSet(ModelViewSet):
     queryset = PowerOutletTemplate.objects.select_related('device_type__manufacturer')
     queryset = PowerOutletTemplate.objects.select_related('device_type__manufacturer')
     serializer_class = serializers.PowerOutletTemplateSerializer
     serializer_class = serializers.PowerOutletTemplateSerializer
-    write_serializer_class = serializers.WritablePowerOutletTemplateSerializer
     filter_class = filters.PowerOutletTemplateFilter
     filter_class = filters.PowerOutletTemplateFilter
 
 
 
 
 class InterfaceTemplateViewSet(ModelViewSet):
 class InterfaceTemplateViewSet(ModelViewSet):
     queryset = InterfaceTemplate.objects.select_related('device_type__manufacturer')
     queryset = InterfaceTemplate.objects.select_related('device_type__manufacturer')
     serializer_class = serializers.InterfaceTemplateSerializer
     serializer_class = serializers.InterfaceTemplateSerializer
-    write_serializer_class = serializers.WritableInterfaceTemplateSerializer
     filter_class = filters.InterfaceTemplateFilter
     filter_class = filters.InterfaceTemplateFilter
 
 
 
 
 class DeviceBayTemplateViewSet(ModelViewSet):
 class DeviceBayTemplateViewSet(ModelViewSet):
     queryset = DeviceBayTemplate.objects.select_related('device_type__manufacturer')
     queryset = DeviceBayTemplate.objects.select_related('device_type__manufacturer')
     serializer_class = serializers.DeviceBayTemplateSerializer
     serializer_class = serializers.DeviceBayTemplateSerializer
-    write_serializer_class = serializers.WritableDeviceBayTemplateSerializer
     filter_class = filters.DeviceBayTemplateFilter
     filter_class = filters.DeviceBayTemplateFilter
 
 
 
 
@@ -228,7 +216,6 @@ class DeviceRoleViewSet(ModelViewSet):
 class PlatformViewSet(ModelViewSet):
 class PlatformViewSet(ModelViewSet):
     queryset = Platform.objects.all()
     queryset = Platform.objects.all()
     serializer_class = serializers.PlatformSerializer
     serializer_class = serializers.PlatformSerializer
-    write_serializer_class = serializers.WritablePlatformSerializer
     filter_class = filters.PlatformFilter
     filter_class = filters.PlatformFilter
 
 
 
 
@@ -244,7 +231,6 @@ class DeviceViewSet(CustomFieldModelViewSet):
         'primary_ip4__nat_outside', 'primary_ip6__nat_outside',
         'primary_ip4__nat_outside', 'primary_ip6__nat_outside',
     )
     )
     serializer_class = serializers.DeviceSerializer
     serializer_class = serializers.DeviceSerializer
-    write_serializer_class = serializers.WritableDeviceSerializer
     filter_class = filters.DeviceFilter
     filter_class = filters.DeviceFilter
 
 
     @detail_route(url_path='napalm')
     @detail_route(url_path='napalm')
@@ -318,35 +304,30 @@ class DeviceViewSet(CustomFieldModelViewSet):
 class ConsolePortViewSet(ModelViewSet):
 class ConsolePortViewSet(ModelViewSet):
     queryset = ConsolePort.objects.select_related('device', 'cs_port__device')
     queryset = ConsolePort.objects.select_related('device', 'cs_port__device')
     serializer_class = serializers.ConsolePortSerializer
     serializer_class = serializers.ConsolePortSerializer
-    write_serializer_class = serializers.WritableConsolePortSerializer
     filter_class = filters.ConsolePortFilter
     filter_class = filters.ConsolePortFilter
 
 
 
 
 class ConsoleServerPortViewSet(ModelViewSet):
 class ConsoleServerPortViewSet(ModelViewSet):
     queryset = ConsoleServerPort.objects.select_related('device', 'connected_console__device')
     queryset = ConsoleServerPort.objects.select_related('device', 'connected_console__device')
     serializer_class = serializers.ConsoleServerPortSerializer
     serializer_class = serializers.ConsoleServerPortSerializer
-    write_serializer_class = serializers.WritableConsoleServerPortSerializer
     filter_class = filters.ConsoleServerPortFilter
     filter_class = filters.ConsoleServerPortFilter
 
 
 
 
 class PowerPortViewSet(ModelViewSet):
 class PowerPortViewSet(ModelViewSet):
     queryset = PowerPort.objects.select_related('device', 'power_outlet__device')
     queryset = PowerPort.objects.select_related('device', 'power_outlet__device')
     serializer_class = serializers.PowerPortSerializer
     serializer_class = serializers.PowerPortSerializer
-    write_serializer_class = serializers.WritablePowerPortSerializer
     filter_class = filters.PowerPortFilter
     filter_class = filters.PowerPortFilter
 
 
 
 
 class PowerOutletViewSet(ModelViewSet):
 class PowerOutletViewSet(ModelViewSet):
     queryset = PowerOutlet.objects.select_related('device', 'connected_port__device')
     queryset = PowerOutlet.objects.select_related('device', 'connected_port__device')
     serializer_class = serializers.PowerOutletSerializer
     serializer_class = serializers.PowerOutletSerializer
-    write_serializer_class = serializers.WritablePowerOutletSerializer
     filter_class = filters.PowerOutletFilter
     filter_class = filters.PowerOutletFilter
 
 
 
 
 class InterfaceViewSet(ModelViewSet):
 class InterfaceViewSet(ModelViewSet):
     queryset = Interface.objects.select_related('device')
     queryset = Interface.objects.select_related('device')
     serializer_class = serializers.InterfaceSerializer
     serializer_class = serializers.InterfaceSerializer
-    write_serializer_class = serializers.WritableInterfaceSerializer
     filter_class = filters.InterfaceFilter
     filter_class = filters.InterfaceFilter
 
 
     @detail_route()
     @detail_route()
@@ -363,14 +344,12 @@ class InterfaceViewSet(ModelViewSet):
 class DeviceBayViewSet(ModelViewSet):
 class DeviceBayViewSet(ModelViewSet):
     queryset = DeviceBay.objects.select_related('installed_device')
     queryset = DeviceBay.objects.select_related('installed_device')
     serializer_class = serializers.DeviceBaySerializer
     serializer_class = serializers.DeviceBaySerializer
-    write_serializer_class = serializers.WritableDeviceBaySerializer
     filter_class = filters.DeviceBayFilter
     filter_class = filters.DeviceBayFilter
 
 
 
 
 class InventoryItemViewSet(ModelViewSet):
 class InventoryItemViewSet(ModelViewSet):
     queryset = InventoryItem.objects.select_related('device', 'manufacturer')
     queryset = InventoryItem.objects.select_related('device', 'manufacturer')
     serializer_class = serializers.InventoryItemSerializer
     serializer_class = serializers.InventoryItemSerializer
-    write_serializer_class = serializers.WritableInventoryItemSerializer
     filter_class = filters.InventoryItemFilter
     filter_class = filters.InventoryItemFilter
 
 
 
 
@@ -393,7 +372,6 @@ class PowerConnectionViewSet(ListModelMixin, GenericViewSet):
 class InterfaceConnectionViewSet(ModelViewSet):
 class InterfaceConnectionViewSet(ModelViewSet):
     queryset = InterfaceConnection.objects.select_related('interface_a__device', 'interface_b__device')
     queryset = InterfaceConnection.objects.select_related('interface_a__device', 'interface_b__device')
     serializer_class = serializers.InterfaceConnectionSerializer
     serializer_class = serializers.InterfaceConnectionSerializer
-    write_serializer_class = serializers.WritableInterfaceConnectionSerializer
     filter_class = filters.InterfaceConnectionFilter
     filter_class = filters.InterfaceConnectionFilter
 
 
 
 
@@ -404,7 +382,6 @@ class InterfaceConnectionViewSet(ModelViewSet):
 class VirtualChassisViewSet(ModelViewSet):
 class VirtualChassisViewSet(ModelViewSet):
     queryset = VirtualChassis.objects.all()
     queryset = VirtualChassis.objects.all()
     serializer_class = serializers.VirtualChassisSerializer
     serializer_class = serializers.VirtualChassisSerializer
-    write_serializer_class = serializers.WritableVirtualChassisSerializer
 
 
 
 
 #
 #

+ 18 - 27
netbox/dcim/tests/test_api.py

@@ -2327,8 +2327,8 @@ class InterfaceTest(HttpStatusMixin, APITestCase):
             'device': self.device.pk,
             'device': self.device.pk,
             'name': 'Test Interface 4',
             'name': 'Test Interface 4',
             'mode': IFACE_MODE_TAGGED,
             'mode': IFACE_MODE_TAGGED,
+            'untagged_vlan': self.vlan3.id,
             'tagged_vlans': [self.vlan1.id, self.vlan2.id],
             'tagged_vlans': [self.vlan1.id, self.vlan2.id],
-            'untagged_vlan': self.vlan3.id
         }
         }
 
 
         url = reverse('dcim-api:interface-list')
         url = reverse('dcim-api:interface-list')
@@ -2336,11 +2336,10 @@ class InterfaceTest(HttpStatusMixin, APITestCase):
 
 
         self.assertHttpStatus(response, status.HTTP_201_CREATED)
         self.assertHttpStatus(response, status.HTTP_201_CREATED)
         self.assertEqual(Interface.objects.count(), 4)
         self.assertEqual(Interface.objects.count(), 4)
-        interface5 = Interface.objects.get(pk=response.data['id'])
-        self.assertEqual(interface5.device_id, data['device'])
-        self.assertEqual(interface5.name, data['name'])
-        self.assertEqual(interface5.tagged_vlans.count(), 2)
-        self.assertEqual(interface5.untagged_vlan.id, data['untagged_vlan'])
+        self.assertEqual(response.data['device']['id'], data['device'])
+        self.assertEqual(response.data['name'], data['name'])
+        self.assertEqual(response.data['untagged_vlan']['id'], data['untagged_vlan'])
+        self.assertEqual([v['id'] for v in response.data['tagged_vlans']], data['tagged_vlans'])
 
 
     def test_create_interface_bulk(self):
     def test_create_interface_bulk(self):
 
 
@@ -2375,22 +2374,22 @@ class InterfaceTest(HttpStatusMixin, APITestCase):
                 'device': self.device.pk,
                 'device': self.device.pk,
                 'name': 'Test Interface 4',
                 'name': 'Test Interface 4',
                 'mode': IFACE_MODE_TAGGED,
                 'mode': IFACE_MODE_TAGGED,
-                'tagged_vlans': [self.vlan1.id],
                 'untagged_vlan': self.vlan2.id,
                 'untagged_vlan': self.vlan2.id,
+                'tagged_vlans': [self.vlan1.id],
             },
             },
             {
             {
                 'device': self.device.pk,
                 'device': self.device.pk,
                 'name': 'Test Interface 5',
                 'name': 'Test Interface 5',
                 'mode': IFACE_MODE_TAGGED,
                 'mode': IFACE_MODE_TAGGED,
-                'tagged_vlans': [self.vlan1.id],
                 'untagged_vlan': self.vlan2.id,
                 'untagged_vlan': self.vlan2.id,
+                'tagged_vlans': [self.vlan1.id],
             },
             },
             {
             {
                 'device': self.device.pk,
                 'device': self.device.pk,
                 'name': 'Test Interface 6',
                 'name': 'Test Interface 6',
                 'mode': IFACE_MODE_TAGGED,
                 'mode': IFACE_MODE_TAGGED,
-                'tagged_vlans': [self.vlan1.id],
                 'untagged_vlan': self.vlan2.id,
                 'untagged_vlan': self.vlan2.id,
+                'tagged_vlans': [self.vlan1.id],
             },
             },
         ]
         ]
 
 
@@ -2399,15 +2398,10 @@ class InterfaceTest(HttpStatusMixin, APITestCase):
 
 
         self.assertHttpStatus(response, status.HTTP_201_CREATED)
         self.assertHttpStatus(response, status.HTTP_201_CREATED)
         self.assertEqual(Interface.objects.count(), 6)
         self.assertEqual(Interface.objects.count(), 6)
-        self.assertEqual(response.data[0]['name'], data[0]['name'])
-        self.assertEqual(response.data[1]['name'], data[1]['name'])
-        self.assertEqual(response.data[2]['name'], data[2]['name'])
-        self.assertEqual(len(response.data[0]['tagged_vlans']), 1)
-        self.assertEqual(len(response.data[1]['tagged_vlans']), 1)
-        self.assertEqual(len(response.data[2]['tagged_vlans']), 1)
-        self.assertEqual(response.data[0]['untagged_vlan'], self.vlan2.id)
-        self.assertEqual(response.data[1]['untagged_vlan'], self.vlan2.id)
-        self.assertEqual(response.data[2]['untagged_vlan'], self.vlan2.id)
+        for i in range(0, 3):
+            self.assertEqual(response.data[i]['name'], data[i]['name'])
+            self.assertEqual([v['id'] for v in response.data[i]['tagged_vlans']], data[i]['tagged_vlans'])
+            self.assertEqual(response.data[i]['untagged_vlan']['id'], data[i]['untagged_vlan'])
 
 
     def test_update_interface(self):
     def test_update_interface(self):
 
 
@@ -2852,9 +2846,9 @@ class InterfaceConnectionTest(HttpStatusMixin, APITestCase):
 
 
         self.assertHttpStatus(response, status.HTTP_201_CREATED)
         self.assertHttpStatus(response, status.HTTP_201_CREATED)
         self.assertEqual(InterfaceConnection.objects.count(), 6)
         self.assertEqual(InterfaceConnection.objects.count(), 6)
-        self.assertEqual(response.data[0]['interface_a'], data[0]['interface_a'])
-        self.assertEqual(response.data[1]['interface_a'], data[1]['interface_a'])
-        self.assertEqual(response.data[2]['interface_a'], data[2]['interface_a'])
+        for i in range(0, 3):
+            self.assertEqual(response.data[i]['interface_a']['id'], data[i]['interface_a'])
+            self.assertEqual(response.data[i]['interface_b']['id'], data[i]['interface_b'])
 
 
     def test_update_interfaceconnection(self):
     def test_update_interfaceconnection(self):
 
 
@@ -3052,12 +3046,9 @@ class VirtualChassisTest(HttpStatusMixin, APITestCase):
         response = self.client.post(url, data, format='json', **self.header)
         response = self.client.post(url, data, format='json', **self.header)
         self.assertHttpStatus(response, status.HTTP_201_CREATED)
         self.assertHttpStatus(response, status.HTTP_201_CREATED)
         self.assertEqual(VirtualChassis.objects.count(), 5)
         self.assertEqual(VirtualChassis.objects.count(), 5)
-        self.assertEqual(response.data[0]['master'], data[0]['master'])
-        self.assertEqual(response.data[0]['domain'], data[0]['domain'])
-        self.assertEqual(response.data[1]['master'], data[1]['master'])
-        self.assertEqual(response.data[1]['domain'], data[1]['domain'])
-        self.assertEqual(response.data[2]['master'], data[2]['master'])
-        self.assertEqual(response.data[2]['domain'], data[2]['domain'])
+        for i in range(0, 3):
+            self.assertEqual(response.data[i]['master']['id'], data[i]['master'])
+            self.assertEqual(response.data[i]['domain'], data[i]['domain'])
 
 
     def test_update_virtualchassis(self):
     def test_update_virtualchassis(self):
 
 

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

@@ -15,7 +15,7 @@ from utilities.api import ChoiceFieldSerializer, ContentTypeFieldSerializer, Val
 # Graphs
 # Graphs
 #
 #
 
 
-class GraphSerializer(serializers.ModelSerializer):
+class GraphSerializer(ValidatedModelSerializer):
     type = ChoiceFieldSerializer(choices=GRAPH_TYPE_CHOICES)
     type = ChoiceFieldSerializer(choices=GRAPH_TYPE_CHOICES)
 
 
     class Meta:
     class Meta:
@@ -23,13 +23,6 @@ class GraphSerializer(serializers.ModelSerializer):
         fields = ['id', 'type', 'weight', 'name', 'source', 'link']
         fields = ['id', 'type', 'weight', 'name', 'source', 'link']
 
 
 
 
-class WritableGraphSerializer(serializers.ModelSerializer):
-
-    class Meta:
-        model = Graph
-        fields = ['id', 'type', 'weight', 'name', 'source', 'link']
-
-
 class RenderedGraphSerializer(serializers.ModelSerializer):
 class RenderedGraphSerializer(serializers.ModelSerializer):
     embed_url = serializers.SerializerMethodField()
     embed_url = serializers.SerializerMethodField()
     embed_link = serializers.SerializerMethodField()
     embed_link = serializers.SerializerMethodField()
@@ -50,7 +43,7 @@ class RenderedGraphSerializer(serializers.ModelSerializer):
 # Export templates
 # Export templates
 #
 #
 
 
-class ExportTemplateSerializer(serializers.ModelSerializer):
+class ExportTemplateSerializer(ValidatedModelSerializer):
 
 
     class Meta:
     class Meta:
         model = ExportTemplate
         model = ExportTemplate
@@ -61,7 +54,7 @@ class ExportTemplateSerializer(serializers.ModelSerializer):
 # Topology maps
 # Topology maps
 #
 #
 
 
-class TopologyMapSerializer(serializers.ModelSerializer):
+class TopologyMapSerializer(ValidatedModelSerializer):
     site = NestedSiteSerializer()
     site = NestedSiteSerializer()
 
 
     class Meta:
     class Meta:
@@ -69,45 +62,19 @@ class TopologyMapSerializer(serializers.ModelSerializer):
         fields = ['id', 'name', 'slug', 'site', 'device_patterns', 'description']
         fields = ['id', 'name', 'slug', 'site', 'device_patterns', 'description']
 
 
 
 
-class WritableTopologyMapSerializer(serializers.ModelSerializer):
-
-    class Meta:
-        model = TopologyMap
-        fields = ['id', 'name', 'slug', 'site', 'device_patterns', 'description']
-
-
 #
 #
 # Image attachments
 # Image attachments
 #
 #
 
 
-class ImageAttachmentSerializer(serializers.ModelSerializer):
-    parent = serializers.SerializerMethodField()
-
-    class Meta:
-        model = ImageAttachment
-        fields = ['id', 'parent', 'name', 'image', 'image_height', 'image_width', 'created']
-
-    def get_parent(self, obj):
-
-        # Static mapping of models to their nested serializers
-        if isinstance(obj.parent, Device):
-            serializer = NestedDeviceSerializer
-        elif isinstance(obj.parent, Rack):
-            serializer = NestedRackSerializer
-        elif isinstance(obj.parent, Site):
-            serializer = NestedSiteSerializer
-        else:
-            raise Exception("Unexpected type of parent object for ImageAttachment")
-
-        return serializer(obj.parent, context={'request': self.context['request']}).data
-
-
-class WritableImageAttachmentSerializer(ValidatedModelSerializer):
+class ImageAttachmentSerializer(ValidatedModelSerializer):
     content_type = ContentTypeFieldSerializer()
     content_type = ContentTypeFieldSerializer()
+    parent = serializers.SerializerMethodField(read_only=True)
 
 
     class Meta:
     class Meta:
         model = ImageAttachment
         model = ImageAttachment
-        fields = ['id', 'content_type', 'object_id', 'name', 'image']
+        fields = [
+            'id', 'content_type', 'object_id', 'parent', 'name', 'image', 'image_height', 'image_width', 'created',
+        ]
 
 
     def validate(self, data):
     def validate(self, data):
 
 
@@ -120,10 +87,24 @@ class WritableImageAttachmentSerializer(ValidatedModelSerializer):
             )
             )
 
 
         # Enforce model validation
         # Enforce model validation
-        super(WritableImageAttachmentSerializer, self).validate(data)
+        super(ImageAttachmentSerializer, self).validate(data)
 
 
         return data
         return data
 
 
+    def get_parent(self, obj):
+
+        # Static mapping of models to their nested serializers
+        if isinstance(obj.parent, Device):
+            serializer = NestedDeviceSerializer
+        elif isinstance(obj.parent, Rack):
+            serializer = NestedRackSerializer
+        elif isinstance(obj.parent, Site):
+            serializer = NestedSiteSerializer
+        else:
+            raise Exception("Unexpected type of parent object for ImageAttachment")
+
+        return serializer(obj.parent, context={'request': self.context['request']}).data
+
 
 
 #
 #
 # Reports
 # Reports

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

@@ -67,7 +67,6 @@ class CustomFieldModelViewSet(ModelViewSet):
 class GraphViewSet(ModelViewSet):
 class GraphViewSet(ModelViewSet):
     queryset = Graph.objects.all()
     queryset = Graph.objects.all()
     serializer_class = serializers.GraphSerializer
     serializer_class = serializers.GraphSerializer
-    write_serializer_class = serializers.WritableGraphSerializer
     filter_class = filters.GraphFilter
     filter_class = filters.GraphFilter
 
 
 
 
@@ -88,7 +87,6 @@ class ExportTemplateViewSet(ModelViewSet):
 class TopologyMapViewSet(ModelViewSet):
 class TopologyMapViewSet(ModelViewSet):
     queryset = TopologyMap.objects.select_related('site')
     queryset = TopologyMap.objects.select_related('site')
     serializer_class = serializers.TopologyMapSerializer
     serializer_class = serializers.TopologyMapSerializer
-    write_serializer_class = serializers.WritableTopologyMapSerializer
     filter_class = filters.TopologyMapFilter
     filter_class = filters.TopologyMapFilter
 
 
     @detail_route()
     @detail_route()
@@ -118,7 +116,6 @@ class TopologyMapViewSet(ModelViewSet):
 class ImageAttachmentViewSet(ModelViewSet):
 class ImageAttachmentViewSet(ModelViewSet):
     queryset = ImageAttachment.objects.all()
     queryset = ImageAttachment.objects.all()
     serializer_class = serializers.ImageAttachmentSerializer
     serializer_class = serializers.ImageAttachmentSerializer
-    write_serializer_class = serializers.WritableImageAttachmentSerializer
 
 
 
 
 #
 #

+ 58 - 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 ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
 from tenancy.api.serializers import NestedTenantSerializer
 from tenancy.api.serializers import NestedTenantSerializer
-from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer
+from utilities.api import ChoiceFieldSerializer, SerializedPKRelatedField, ValidatedModelSerializer, WritableNestedSerializer
 from virtualization.api.serializers import NestedVirtualMachineSerializer
 from virtualization.api.serializers import NestedVirtualMachineSerializer
 
 
 
 
@@ -23,7 +23,7 @@ from virtualization.api.serializers import NestedVirtualMachineSerializer
 #
 #
 
 
 class VRFSerializer(CustomFieldModelSerializer):
 class VRFSerializer(CustomFieldModelSerializer):
-    tenant = NestedTenantSerializer()
+    tenant = NestedTenantSerializer(required=False, allow_null=True)
 
 
     class Meta:
     class Meta:
         model = VRF
         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')
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vrf-detail')
 
 
     class Meta:
     class Meta:
@@ -41,15 +41,6 @@ class NestedVRFSerializer(serializers.ModelSerializer):
         fields = ['id', 'url', 'name', 'rd']
         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
 # Roles
 #
 #
@@ -61,7 +52,7 @@ class RoleSerializer(ValidatedModelSerializer):
         fields = ['id', 'name', 'slug', 'weight']
         fields = ['id', 'name', 'slug', 'weight']
 
 
 
 
-class NestedRoleSerializer(serializers.ModelSerializer):
+class NestedRoleSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:role-detail')
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:role-detail')
 
 
     class Meta:
     class Meta:
@@ -80,7 +71,7 @@ class RIRSerializer(ValidatedModelSerializer):
         fields = ['id', 'name', 'slug', 'is_private']
         fields = ['id', 'name', 'slug', 'is_private']
 
 
 
 
-class NestedRIRSerializer(serializers.ModelSerializer):
+class NestedRIRSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail')
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail')
 
 
     class Meta:
     class Meta:
@@ -100,9 +91,10 @@ class AggregateSerializer(CustomFieldModelSerializer):
         fields = [
         fields = [
             'id', 'family', 'prefix', 'rir', 'date_added', 'description', 'custom_fields', 'created', 'last_updated',
             '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')
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:aggregate-detail')
 
 
     class Meta(AggregateSerializer.Meta):
     class Meta(AggregateSerializer.Meta):
@@ -110,34 +102,12 @@ class NestedAggregateSerializer(serializers.ModelSerializer):
         fields = ['id', 'url', 'family', 'prefix']
         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
 # 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:
     class Meta:
         model = VLANGroup
         model = VLANGroup
@@ -154,21 +124,29 @@ class WritableVLANGroupSerializer(serializers.ModelSerializer):
                 validator(data)
                 validator(data)
 
 
         # Enforce model validation
         # Enforce model validation
-        super(WritableVLANGroupSerializer, self).validate(data)
+        super(VLANGroupSerializer, self).validate(data)
 
 
         return 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
 # VLANs
 #
 #
 
 
 class VLANSerializer(CustomFieldModelSerializer):
 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:
     class Meta:
         model = VLAN
         model = VLAN
@@ -176,24 +154,6 @@ class VLANSerializer(CustomFieldModelSerializer):
             'id', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', 'display_name',
             'id', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', 'display_name',
             'custom_fields', 'created', 'last_updated',
             '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 = []
         validators = []
 
 
     def validate(self, data):
     def validate(self, data):
@@ -206,22 +166,30 @@ class WritableVLANSerializer(CustomFieldModelSerializer):
                 validator(data)
                 validator(data)
 
 
         # Enforce model validation
         # Enforce model validation
-        super(WritableVLANSerializer, self).validate(data)
+        super(VLANSerializer, self).validate(data)
 
 
         return 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
 # Prefixes
 #
 #
 
 
 class PrefixSerializer(CustomFieldModelSerializer):
 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:
     class Meta:
         model = Prefix
         model = Prefix
@@ -229,9 +197,10 @@ class PrefixSerializer(CustomFieldModelSerializer):
             'id', 'family', 'prefix', 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', 'description',
             'id', 'family', 'prefix', 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', 'description',
             'custom_fields', 'created', 'last_updated',
             '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')
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:prefix-detail')
 
 
     class Meta:
     class Meta:
@@ -239,16 +208,6 @@ class NestedPrefixSerializer(serializers.ModelSerializer):
         fields = ['id', 'url', 'family', 'prefix']
         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):
 class AvailablePrefixSerializer(serializers.Serializer):
 
 
     def to_representation(self, instance):
     def to_representation(self, instance):
@@ -288,11 +247,11 @@ class IPAddressInterfaceSerializer(serializers.ModelSerializer):
 
 
 
 
 class IPAddressSerializer(CustomFieldModelSerializer):
 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:
     class Meta:
         model = IPAddress
         model = IPAddress
@@ -300,9 +259,10 @@ class IPAddressSerializer(CustomFieldModelSerializer):
             'id', 'family', 'address', 'vrf', 'tenant', 'status', 'role', 'interface', 'description', 'nat_inside',
             'id', 'family', 'address', 'vrf', 'tenant', 'status', 'role', 'interface', 'description', 'nat_inside',
             'nat_outside', 'custom_fields', 'created', 'last_updated',
             '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')
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail')
 
 
     class Meta:
     class Meta:
@@ -310,18 +270,8 @@ class NestedIPAddressSerializer(serializers.ModelSerializer):
         fields = ['id', 'url', 'family', 'address']
         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):
 class AvailableIPSerializer(serializers.Serializer):
@@ -342,22 +292,16 @@ class AvailableIPSerializer(serializers.Serializer):
 # Services
 # 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)
     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 = SerializedPKRelatedField(
+        queryset=IPAddress.objects.all(),
+        serializer=NestedIPAddressSerializer,
+        required=False,
+        many=True
+    )
 
 
     class Meta:
     class Meta:
         model = Service
         model = Service

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

@@ -35,7 +35,6 @@ class IPAMFieldChoicesViewSet(FieldChoicesViewSet):
 class VRFViewSet(CustomFieldModelViewSet):
 class VRFViewSet(CustomFieldModelViewSet):
     queryset = VRF.objects.select_related('tenant')
     queryset = VRF.objects.select_related('tenant')
     serializer_class = serializers.VRFSerializer
     serializer_class = serializers.VRFSerializer
-    write_serializer_class = serializers.WritableVRFSerializer
     filter_class = filters.VRFFilter
     filter_class = filters.VRFFilter
 
 
 
 
@@ -56,7 +55,6 @@ class RIRViewSet(ModelViewSet):
 class AggregateViewSet(CustomFieldModelViewSet):
 class AggregateViewSet(CustomFieldModelViewSet):
     queryset = Aggregate.objects.select_related('rir')
     queryset = Aggregate.objects.select_related('rir')
     serializer_class = serializers.AggregateSerializer
     serializer_class = serializers.AggregateSerializer
-    write_serializer_class = serializers.WritableAggregateSerializer
     filter_class = filters.AggregateFilter
     filter_class = filters.AggregateFilter
 
 
 
 
@@ -77,7 +75,6 @@ class RoleViewSet(ModelViewSet):
 class PrefixViewSet(CustomFieldModelViewSet):
 class PrefixViewSet(CustomFieldModelViewSet):
     queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
     queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
     serializer_class = serializers.PrefixSerializer
     serializer_class = serializers.PrefixSerializer
-    write_serializer_class = serializers.WritablePrefixSerializer
     filter_class = filters.PrefixFilter
     filter_class = filters.PrefixFilter
 
 
     @detail_route(url_path='available-prefixes', methods=['get', 'post'])
     @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
             # Initialize the serializer with a list or a single object depending on what was requested
             if isinstance(request.data, list):
             if isinstance(request.data, list):
-                serializer = serializers.WritablePrefixSerializer(data=requested_prefixes, many=True)
+                serializer = serializers.PrefixSerializer(data=requested_prefixes, many=True)
             else:
             else:
-                serializer = serializers.WritablePrefixSerializer(data=requested_prefixes[0])
+                serializer = serializers.PrefixSerializer(data=requested_prefixes[0])
 
 
             # Create the new Prefix(es)
             # Create the new Prefix(es)
             if serializer.is_valid():
             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
             # Initialize the serializer with a list or a single object depending on what was requested
             if isinstance(request.data, list):
             if isinstance(request.data, list):
-                serializer = serializers.WritableIPAddressSerializer(data=requested_ips, many=True)
+                serializer = serializers.IPAddressSerializer(data=requested_ips, many=True)
             else:
             else:
-                serializer = serializers.WritableIPAddressSerializer(data=requested_ips[0])
+                serializer = serializers.IPAddressSerializer(data=requested_ips[0])
 
 
             # Create the new IP address(es)
             # Create the new IP address(es)
             if serializer.is_valid():
             if serializer.is_valid():
@@ -223,7 +220,6 @@ class IPAddressViewSet(CustomFieldModelViewSet):
         'nat_outside'
         'nat_outside'
     )
     )
     serializer_class = serializers.IPAddressSerializer
     serializer_class = serializers.IPAddressSerializer
-    write_serializer_class = serializers.WritableIPAddressSerializer
     filter_class = filters.IPAddressFilter
     filter_class = filters.IPAddressFilter
 
 
 
 
@@ -234,7 +230,6 @@ class IPAddressViewSet(CustomFieldModelViewSet):
 class VLANGroupViewSet(ModelViewSet):
 class VLANGroupViewSet(ModelViewSet):
     queryset = VLANGroup.objects.select_related('site')
     queryset = VLANGroup.objects.select_related('site')
     serializer_class = serializers.VLANGroupSerializer
     serializer_class = serializers.VLANGroupSerializer
-    write_serializer_class = serializers.WritableVLANGroupSerializer
     filter_class = filters.VLANGroupFilter
     filter_class = filters.VLANGroupFilter
 
 
 
 
@@ -245,7 +240,6 @@ class VLANGroupViewSet(ModelViewSet):
 class VLANViewSet(CustomFieldModelViewSet):
 class VLANViewSet(CustomFieldModelViewSet):
     queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role')
     queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role')
     serializer_class = serializers.VLANSerializer
     serializer_class = serializers.VLANSerializer
-    write_serializer_class = serializers.WritableVLANSerializer
     filter_class = filters.VLANFilter
     filter_class = filters.VLANFilter
 
 
 
 
@@ -256,5 +250,4 @@ class VLANViewSet(CustomFieldModelViewSet):
 class ServiceViewSet(ModelViewSet):
 class ServiceViewSet(ModelViewSet):
     queryset = Service.objects.select_related('device')
     queryset = Service.objects.select_related('device')
     serializer_class = serializers.ServiceSerializer
     serializer_class = serializers.ServiceSerializer
-    write_serializer_class = serializers.WritableServiceSerializer
     filter_class = filters.ServiceFilter
     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 dcim.api.serializers import NestedDeviceSerializer
 from secrets.models import Secret, SecretRole
 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']
         fields = ['id', 'name', 'slug']
 
 
 
 
-class NestedSecretRoleSerializer(serializers.ModelSerializer):
+class NestedSecretRoleSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='secrets-api:secretrole-detail')
     url = serializers.HyperlinkedIdentityField(view_name='secrets-api:secretrole-detail')
 
 
     class Meta:
     class Meta:
@@ -31,16 +31,9 @@ class NestedSecretRoleSerializer(serializers.ModelSerializer):
 # Secrets
 # Secrets
 #
 #
 
 
-class SecretSerializer(serializers.ModelSerializer):
+class SecretSerializer(ValidatedModelSerializer):
     device = NestedDeviceSerializer()
     device = NestedDeviceSerializer()
     role = NestedSecretRoleSerializer()
     role = NestedSecretRoleSerializer()
-
-    class Meta:
-        model = Secret
-        fields = ['id', 'device', 'role', 'name', 'plaintext', 'hash', 'created', 'last_updated']
-
-
-class WritableSecretSerializer(serializers.ModelSerializer):
     plaintext = serializers.CharField()
     plaintext = serializers.CharField()
 
 
     class Meta:
     class Meta:
@@ -64,6 +57,6 @@ class WritableSecretSerializer(serializers.ModelSerializer):
             validator(data)
             validator(data)
 
 
         # Enforce model validation
         # Enforce model validation
-        super(WritableSecretSerializer, self).validate(data)
+        super(SecretSerializer, self).validate(data)
 
 
         return data
         return data

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

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

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

@@ -4,7 +4,7 @@ from rest_framework import serializers
 
 
 from extras.api.customfields import CustomFieldModelSerializer
 from extras.api.customfields import CustomFieldModelSerializer
 from tenancy.models import Tenant, TenantGroup
 from tenancy.models import Tenant, TenantGroup
-from utilities.api import ValidatedModelSerializer
+from utilities.api import ValidatedModelSerializer, WritableNestedSerializer
 
 
 
 
 #
 #
@@ -18,7 +18,7 @@ class TenantGroupSerializer(ValidatedModelSerializer):
         fields = ['id', 'name', 'slug']
         fields = ['id', 'name', 'slug']
 
 
 
 
-class NestedTenantGroupSerializer(serializers.ModelSerializer):
+class NestedTenantGroupSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenantgroup-detail')
     url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenantgroup-detail')
 
 
     class Meta:
     class Meta:
@@ -31,23 +31,16 @@ class NestedTenantGroupSerializer(serializers.ModelSerializer):
 #
 #
 
 
 class TenantSerializer(CustomFieldModelSerializer):
 class TenantSerializer(CustomFieldModelSerializer):
-    group = NestedTenantGroupSerializer()
+    group = NestedTenantGroupSerializer(required=False)
 
 
     class Meta:
     class Meta:
         model = Tenant
         model = Tenant
         fields = ['id', 'name', 'slug', 'group', 'description', 'comments', 'custom_fields', 'created', 'last_updated']
         fields = ['id', 'name', 'slug', 'group', 'description', 'comments', 'custom_fields', 'created', 'last_updated']
 
 
 
 
-class NestedTenantSerializer(serializers.ModelSerializer):
+class NestedTenantSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenant-detail')
     url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenant-detail')
 
 
     class Meta:
     class Meta:
         model = Tenant
         model = Tenant
         fields = ['id', 'url', 'name', 'slug']
         fields = ['id', 'url', 'name', 'slug']
-
-
-class WritableTenantSerializer(CustomFieldModelSerializer):
-
-    class Meta:
-        model = Tenant
-        fields = ['id', 'name', 'slug', 'group', 'description', 'comments', 'custom_fields', 'created', 'last_updated']

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

@@ -32,5 +32,4 @@ class TenantGroupViewSet(ModelViewSet):
 class TenantViewSet(CustomFieldModelViewSet):
 class TenantViewSet(CustomFieldModelViewSet):
     queryset = Tenant.objects.select_related('group')
     queryset = Tenant.objects.select_related('group')
     serializer_class = serializers.TenantSerializer
     serializer_class = serializers.TenantSerializer
-    write_serializer_class = serializers.WritableTenantSerializer
     filter_class = filters.TenantFilter
     filter_class = filters.TenantFilter

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

@@ -1,10 +1,11 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
 from django.contrib.auth.models import User
 from django.contrib.auth.models import User
-from rest_framework import serializers
 
 
+from utilities.api import WritableNestedSerializer
 
 
-class NestedUserSerializer(serializers.ModelSerializer):
+
+class NestedUserSerializer(WritableNestedSerializer):
 
 
     class Meta:
     class Meta:
         model = User
         model = User

+ 63 - 39
netbox/utilities/api.py

@@ -5,11 +5,13 @@ import pytz
 
 
 from django.conf import settings
 from django.conf import settings
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
+from django.core.exceptions import ObjectDoesNotExist
 from django.db.models import ManyToManyField
 from django.db.models import ManyToManyField
 from django.http import Http404
 from django.http import Http404
 from rest_framework import mixins
 from rest_framework import mixins
 from rest_framework.exceptions import APIException
 from rest_framework.exceptions import APIException
 from rest_framework.permissions import BasePermission
 from rest_framework.permissions import BasePermission
+from rest_framework.relations import PrimaryKeyRelatedField
 from rest_framework.response import Response
 from rest_framework.response import Response
 from rest_framework.serializers import Field, ModelSerializer, ValidationError
 from rest_framework.serializers import Field, ModelSerializer, ValidationError
 from rest_framework.viewsets import GenericViewSet, ViewSet
 from rest_framework.viewsets import GenericViewSet, ViewSet
@@ -37,36 +39,9 @@ class IsAuthenticatedOrLoginNotRequired(BasePermission):
 
 
 
 
 #
 #
-# Serializers
+# Fields
 #
 #
 
 
-class ValidatedModelSerializer(ModelSerializer):
-    """
-    Extends the built-in ModelSerializer to enforce calling clean() on the associated model during validation.
-    """
-    def validate(self, data):
-
-        # Remove custom field data (if any) prior to model validation
-        attrs = data.copy()
-        attrs.pop('custom_fields', None)
-
-        # Run clean() on an instance of the model
-        if self.instance is None:
-            model = self.Meta.model
-            # Ignore ManyToManyFields for new instances (a PK is needed for validation)
-            for field in model._meta.get_fields():
-                if isinstance(field, ManyToManyField) and field.name in attrs:
-                    attrs.pop(field.name)
-            instance = self.Meta.model(**attrs)
-        else:
-            instance = self.instance
-            for k, v in attrs.items():
-                setattr(instance, k, v)
-        instance.clean()
-
-        return data
-
-
 class ChoiceFieldSerializer(Field):
 class ChoiceFieldSerializer(Field):
     """
     """
     Represent a ChoiceField as {'value': <DB value>, 'label': <string>}.
     Represent a ChoiceField as {'value': <DB value>, 'label': <string>}.
@@ -86,7 +61,7 @@ class ChoiceFieldSerializer(Field):
         return {'value': obj, 'label': self._choices[obj]}
         return {'value': obj, 'label': self._choices[obj]}
 
 
     def to_internal_value(self, data):
     def to_internal_value(self, data):
-        return self._choices.get(data)
+        return data
 
 
 
 
 class ContentTypeFieldSerializer(Field):
 class ContentTypeFieldSerializer(Field):
@@ -108,7 +83,6 @@ class TimeZoneField(Field):
     """
     """
     Represent a pytz time zone.
     Represent a pytz time zone.
     """
     """
-
     def to_representation(self, obj):
     def to_representation(self, obj):
         return obj.zone if obj else None
         return obj.zone if obj else None
 
 
@@ -121,6 +95,64 @@ class TimeZoneField(Field):
             raise ValidationError('Invalid time zone "{}"'.format(data))
             raise ValidationError('Invalid time zone "{}"'.format(data))
 
 
 
 
+class SerializedPKRelatedField(PrimaryKeyRelatedField):
+    """
+    Extends PrimaryKeyRelatedField to return a serialized object on read. This is useful for representing related
+    objects in a ManyToManyField while still allowing a set of primary keys to be written.
+    """
+    def __init__(self, serializer, **kwargs):
+        self.serializer = serializer
+        self.pk_field = kwargs.pop('pk_field', None)
+        super(SerializedPKRelatedField, self).__init__(**kwargs)
+
+    def to_representation(self, value):
+        return self.serializer(value, context={'request': self.context['request']}).data
+
+
+#
+# Serializers
+#
+
+class ValidatedModelSerializer(ModelSerializer):
+    """
+    Extends the built-in ModelSerializer to enforce calling clean() on the associated model during validation.
+    """
+    def validate(self, data):
+
+        # Remove custom field data (if any) prior to model validation
+        attrs = data.copy()
+        attrs.pop('custom_fields', None)
+
+        # Run clean() on an instance of the model
+        if self.instance is None:
+            model = self.Meta.model
+            # Ignore ManyToManyFields for new instances (a PK is needed for validation)
+            for field in model._meta.get_fields():
+                if isinstance(field, ManyToManyField) and field.name in attrs:
+                    attrs.pop(field.name)
+            instance = self.Meta.model(**attrs)
+        else:
+            instance = self.instance
+            for k, v in attrs.items():
+                setattr(instance, k, v)
+        instance.clean()
+
+        return data
+
+
+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:
+            raise ValidationError("Invalid ID")
+
+
 #
 #
 # Viewsets
 # Viewsets
 #
 #
@@ -132,16 +164,8 @@ class ModelViewSet(mixins.CreateModelMixin,
                    mixins.ListModelMixin,
                    mixins.ListModelMixin,
                    GenericViewSet):
                    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):
     def get_serializer(self, *args, **kwargs):
         # If a list of objects has been provided, initialize the serializer with many=True
         # If a list of objects has been provided, initialize the serializer with many=True
         if isinstance(kwargs.get('data', {}), list):
         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 extras.api.customfields import CustomFieldModelSerializer
 from ipam.models import IPAddress
 from ipam.models import IPAddress
 from tenancy.api.serializers import NestedTenantSerializer
 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.constants import VM_STATUS_CHOICES
 from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine
 from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine
 
 
@@ -24,7 +24,7 @@ class ClusterTypeSerializer(ValidatedModelSerializer):
         fields = ['id', 'name', 'slug']
         fields = ['id', 'name', 'slug']
 
 
 
 
-class NestedClusterTypeSerializer(serializers.ModelSerializer):
+class NestedClusterTypeSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustertype-detail')
     url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustertype-detail')
 
 
     class Meta:
     class Meta:
@@ -43,7 +43,7 @@ class ClusterGroupSerializer(ValidatedModelSerializer):
         fields = ['id', 'name', 'slug']
         fields = ['id', 'name', 'slug']
 
 
 
 
-class NestedClusterGroupSerializer(serializers.ModelSerializer):
+class NestedClusterGroupSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustergroup-detail')
     url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustergroup-detail')
 
 
     class Meta:
     class Meta:
@@ -57,15 +57,15 @@ class NestedClusterGroupSerializer(serializers.ModelSerializer):
 
 
 class ClusterSerializer(CustomFieldModelSerializer):
 class ClusterSerializer(CustomFieldModelSerializer):
     type = NestedClusterTypeSerializer()
     type = NestedClusterTypeSerializer()
-    group = NestedClusterGroupSerializer()
-    site = NestedSiteSerializer()
+    group = NestedClusterGroupSerializer(required=False, allow_null=True)
+    site = NestedSiteSerializer(required=False, allow_null=True)
 
 
     class Meta:
     class Meta:
         model = Cluster
         model = Cluster
         fields = ['id', 'name', 'type', 'group', 'site', 'comments', 'custom_fields', 'created', 'last_updated']
         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')
     url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:cluster-detail')
 
 
     class Meta:
     class Meta:
@@ -73,13 +73,6 @@ class NestedClusterSerializer(serializers.ModelSerializer):
         fields = ['id', 'url', 'name']
         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
 # Virtual machines
 #
 #
@@ -94,14 +87,14 @@ class VirtualMachineIPAddressSerializer(serializers.ModelSerializer):
 
 
 
 
 class VirtualMachineSerializer(CustomFieldModelSerializer):
 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:
     class Meta:
         model = VirtualMachine
         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')
     url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:virtualmachine-detail')
 
 
     class Meta:
     class Meta:
@@ -119,22 +112,13 @@ class NestedVirtualMachineSerializer(serializers.ModelSerializer):
         fields = ['id', 'url', 'name']
         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
 # VM interfaces
 #
 #
 
 
-class InterfaceSerializer(serializers.ModelSerializer):
+class InterfaceSerializer(ValidatedModelSerializer):
     virtual_machine = NestedVirtualMachineSerializer()
     virtual_machine = NestedVirtualMachineSerializer()
+    form_factor = serializers.IntegerField(default=IFACE_FF_VIRTUAL)
 
 
     class Meta:
     class Meta:
         model = Interface
         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')
     url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:interface-detail')
 
 
     class Meta:
     class Meta:
         model = Interface
         model = Interface
         fields = ['id', 'url', 'name']
         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):
 class ClusterViewSet(CustomFieldModelViewSet):
     queryset = Cluster.objects.select_related('type', 'group')
     queryset = Cluster.objects.select_related('type', 'group')
     serializer_class = serializers.ClusterSerializer
     serializer_class = serializers.ClusterSerializer
-    write_serializer_class = serializers.WritableClusterSerializer
     filter_class = filters.ClusterFilter
     filter_class = filters.ClusterFilter
 
 
 
 
@@ -48,12 +47,10 @@ class ClusterViewSet(CustomFieldModelViewSet):
 class VirtualMachineViewSet(CustomFieldModelViewSet):
 class VirtualMachineViewSet(CustomFieldModelViewSet):
     queryset = VirtualMachine.objects.all()
     queryset = VirtualMachine.objects.all()
     serializer_class = serializers.VirtualMachineSerializer
     serializer_class = serializers.VirtualMachineSerializer
-    write_serializer_class = serializers.WritableVirtualMachineSerializer
     filter_class = filters.VirtualMachineFilter
     filter_class = filters.VirtualMachineFilter
 
 
 
 
 class InterfaceViewSet(ModelViewSet):
 class InterfaceViewSet(ModelViewSet):
     queryset = Interface.objects.filter(virtual_machine__isnull=False).select_related('virtual_machine')
     queryset = Interface.objects.filter(virtual_machine__isnull=False).select_related('virtual_machine')
     serializer_class = serializers.InterfaceSerializer
     serializer_class = serializers.InterfaceSerializer
-    write_serializer_class = serializers.WritableInterfaceSerializer
     filter_class = filters.InterfaceFilter
     filter_class = filters.InterfaceFilter

+ 1 - 1
requirements.txt

@@ -5,7 +5,7 @@ django-filter>=1.1.0
 django-mptt>=0.9.0
 django-mptt>=0.9.0
 django-tables2>=1.19.0
 django-tables2>=1.19.0
 django-timezone-field>=2.0
 django-timezone-field>=2.0
-djangorestframework>=3.7.7
+djangorestframework>=3.7.7,<3.8.2
 drf-yasg[validation]>=1.4.4
 drf-yasg[validation]>=1.4.4
 graphviz>=0.8.2
 graphviz>=0.8.2
 Markdown>=2.6.11
 Markdown>=2.6.11