Explorar el Código

Started merging writable serializers (WIP)

Jeremy Stretch hace 7 años
padre
commit
7241783249

+ 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 extras.api.customfields import CustomFieldModelSerializer
 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')
 
     class Meta:
@@ -32,16 +32,6 @@ class NestedProviderSerializer(serializers.ModelSerializer):
         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
 #
@@ -53,7 +43,7 @@ class CircuitTypeSerializer(ValidatedModelSerializer):
         fields = ['id', 'name', 'slug']
 
 
-class NestedCircuitTypeSerializer(serializers.ModelSerializer):
+class NestedCircuitTypeSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
 
     class Meta:
@@ -67,9 +57,9 @@ class NestedCircuitTypeSerializer(serializers.ModelSerializer):
 
 class CircuitSerializer(CustomFieldModelSerializer):
     provider = NestedProviderSerializer()
-    status = ChoiceFieldSerializer(choices=CIRCUIT_STATUS_CHOICES)
+    status = ChoiceFieldSerializer(choices=CIRCUIT_STATUS_CHOICES, required=False)
     type = NestedCircuitTypeSerializer()
-    tenant = NestedTenantSerializer()
+    tenant = NestedTenantSerializer(required=False)
 
     class Meta:
         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')
 
     class Meta:
@@ -87,33 +77,14 @@ class NestedCircuitSerializer(serializers.ModelSerializer):
         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
 #
 
-class CircuitTerminationSerializer(serializers.ModelSerializer):
+class CircuitTerminationSerializer(ValidatedModelSerializer):
     circuit = NestedCircuitSerializer()
     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)
 
     class Meta:
         model = CircuitTermination

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

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

+ 135 - 298
netbox/dcim/api/serializers.py

@@ -20,7 +20,7 @@ from extras.api.customfields import CustomFieldModelSerializer
 from ipam.models import IPAddress, VLAN
 from tenancy.api.serializers import NestedTenantSerializer
 from users.api.serializers import NestedUserSerializer
-from utilities.api import ChoiceFieldSerializer, TimeZoneField, ValidatedModelSerializer
+from utilities.api import ChoiceFieldSerializer, TimeZoneField, ValidatedModelSerializer, WritableNestedSerializer
 from virtualization.models import Cluster
 
 
@@ -28,7 +28,7 @@ from virtualization.models import Cluster
 # Regions
 #
 
-class NestedRegionSerializer(serializers.ModelSerializer):
+class NestedRegionSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail')
 
     class Meta:
@@ -37,14 +37,7 @@ class NestedRegionSerializer(serializers.ModelSerializer):
 
 
 class RegionSerializer(serializers.ModelSerializer):
-    parent = NestedRegionSerializer()
-
-    class Meta:
-        model = Region
-        fields = ['id', 'name', 'slug', 'parent']
-
-
-class WritableRegionSerializer(ValidatedModelSerializer):
+    parent = NestedRegionSerializer(required=False)
 
     class Meta:
         model = Region
@@ -56,9 +49,9 @@ class WritableRegionSerializer(ValidatedModelSerializer):
 #
 
 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)
+    tenant = NestedTenantSerializer(required=False)
     time_zone = TimeZoneField(required=False)
 
     class Meta:
@@ -71,7 +64,7 @@ class SiteSerializer(CustomFieldModelSerializer):
         ]
 
 
-class NestedSiteSerializer(serializers.ModelSerializer):
+class NestedSiteSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:site-detail')
 
     class Meta:
@@ -79,23 +72,11 @@ class NestedSiteSerializer(serializers.ModelSerializer):
         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
 #
 
-class RackGroupSerializer(serializers.ModelSerializer):
+class RackGroupSerializer(ValidatedModelSerializer):
     site = NestedSiteSerializer()
 
     class Meta:
@@ -103,7 +84,7 @@ class RackGroupSerializer(serializers.ModelSerializer):
         fields = ['id', 'name', 'slug', 'site']
 
 
-class NestedRackGroupSerializer(serializers.ModelSerializer):
+class NestedRackGroupSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackgroup-detail')
 
     class Meta:
@@ -111,13 +92,6 @@ class NestedRackGroupSerializer(serializers.ModelSerializer):
         fields = ['id', 'url', 'name', 'slug']
 
 
-class WritableRackGroupSerializer(ValidatedModelSerializer):
-
-    class Meta:
-        model = RackGroup
-        fields = ['id', 'name', 'slug', 'site']
-
-
 #
 # Rack roles
 #
@@ -129,7 +103,7 @@ class RackRoleSerializer(ValidatedModelSerializer):
         fields = ['id', 'name', 'slug', 'color']
 
 
-class NestedRackRoleSerializer(serializers.ModelSerializer):
+class NestedRackRoleSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
 
     class Meta:
@@ -143,11 +117,11 @@ class NestedRackRoleSerializer(serializers.ModelSerializer):
 
 class RackSerializer(CustomFieldModelSerializer):
     site = NestedSiteSerializer()
-    group = NestedRackGroupSerializer()
-    tenant = NestedTenantSerializer()
-    role = NestedRackRoleSerializer()
-    type = ChoiceFieldSerializer(choices=RACK_TYPE_CHOICES)
-    width = ChoiceFieldSerializer(choices=RACK_WIDTH_CHOICES)
+    group = NestedRackGroupSerializer(required=False)
+    tenant = NestedTenantSerializer(required=False)
+    role = NestedRackRoleSerializer(required=False)
+    type = ChoiceFieldSerializer(choices=RACK_TYPE_CHOICES, required=False)
+    width = ChoiceFieldSerializer(choices=RACK_WIDTH_CHOICES, required=False)
 
     class Meta:
         model = Rack
@@ -155,24 +129,6 @@ class RackSerializer(CustomFieldModelSerializer):
             'id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'role', 'serial', 'type', 'width',
             '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
         # prevents facility_id from being interpreted as a required field.
         validators = [
@@ -188,16 +144,24 @@ class WritableRackSerializer(CustomFieldModelSerializer):
             validator(data)
 
         # Enforce model validation
-        super(WritableRackSerializer, self).validate(data)
+        super(RackSerializer, self).validate(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
 #
 
-class NestedDeviceSerializer(serializers.ModelSerializer):
+class NestedDeviceSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail')
 
     class Meta:
@@ -219,23 +183,16 @@ class RackUnitSerializer(serializers.Serializer):
 # Rack reservations
 #
 
-class RackReservationSerializer(serializers.ModelSerializer):
+class RackReservationSerializer(ValidatedModelSerializer):
     rack = NestedRackSerializer()
     user = NestedUserSerializer()
-    tenant = NestedTenantSerializer()
+    tenant = NestedTenantSerializer(required=False)
 
     class Meta:
         model = RackReservation
         fields = ['id', 'rack', 'units', 'created', 'user', 'tenant', 'description']
 
 
-class WritableRackReservationSerializer(ValidatedModelSerializer):
-
-    class Meta:
-        model = RackReservation
-        fields = ['id', 'rack', 'units', 'user', 'description']
-
-
 #
 # Manufacturers
 #
@@ -247,7 +204,7 @@ class ManufacturerSerializer(ValidatedModelSerializer):
         fields = ['id', 'name', 'slug']
 
 
-class NestedManufacturerSerializer(serializers.ModelSerializer):
+class NestedManufacturerSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
 
     class Meta:
@@ -261,8 +218,8 @@ class NestedManufacturerSerializer(serializers.ModelSerializer):
 
 class DeviceTypeSerializer(CustomFieldModelSerializer):
     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)
 
     class Meta:
@@ -274,30 +231,20 @@ class DeviceTypeSerializer(CustomFieldModelSerializer):
         ]
 
 
-class NestedDeviceTypeSerializer(serializers.ModelSerializer):
+class NestedDeviceTypeSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail')
-    manufacturer = NestedManufacturerSerializer()
+    manufacturer = NestedManufacturerSerializer(read_only=True)
 
     class Meta:
         model = DeviceType
         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
 #
 
-class ConsolePortTemplateSerializer(serializers.ModelSerializer):
+class ConsolePortTemplateSerializer(ValidatedModelSerializer):
     device_type = NestedDeviceTypeSerializer()
 
     class Meta:
@@ -305,18 +252,11 @@ class ConsolePortTemplateSerializer(serializers.ModelSerializer):
         fields = ['id', 'device_type', 'name']
 
 
-class WritableConsolePortTemplateSerializer(ValidatedModelSerializer):
-
-    class Meta:
-        model = ConsolePortTemplate
-        fields = ['id', 'device_type', 'name']
-
-
 #
 # Console server port templates
 #
 
-class ConsoleServerPortTemplateSerializer(serializers.ModelSerializer):
+class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
     device_type = NestedDeviceTypeSerializer()
 
     class Meta:
@@ -324,18 +264,11 @@ class ConsoleServerPortTemplateSerializer(serializers.ModelSerializer):
         fields = ['id', 'device_type', 'name']
 
 
-class WritableConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
-
-    class Meta:
-        model = ConsoleServerPortTemplate
-        fields = ['id', 'device_type', 'name']
-
-
 #
 # Power port templates
 #
 
-class PowerPortTemplateSerializer(serializers.ModelSerializer):
+class PowerPortTemplateSerializer(ValidatedModelSerializer):
     device_type = NestedDeviceTypeSerializer()
 
     class Meta:
@@ -343,18 +276,11 @@ class PowerPortTemplateSerializer(serializers.ModelSerializer):
         fields = ['id', 'device_type', 'name']
 
 
-class WritablePowerPortTemplateSerializer(ValidatedModelSerializer):
-
-    class Meta:
-        model = PowerPortTemplate
-        fields = ['id', 'device_type', 'name']
-
-
 #
 # Power outlet templates
 #
 
-class PowerOutletTemplateSerializer(serializers.ModelSerializer):
+class PowerOutletTemplateSerializer(ValidatedModelSerializer):
     device_type = NestedDeviceTypeSerializer()
 
     class Meta:
@@ -362,27 +288,13 @@ class PowerOutletTemplateSerializer(serializers.ModelSerializer):
         fields = ['id', 'device_type', 'name']
 
 
-class WritablePowerOutletTemplateSerializer(ValidatedModelSerializer):
-
-    class Meta:
-        model = PowerOutletTemplate
-        fields = ['id', 'device_type', 'name']
-
-
 #
 # Interface templates
 #
 
-class InterfaceTemplateSerializer(serializers.ModelSerializer):
+class InterfaceTemplateSerializer(ValidatedModelSerializer):
     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:
         model = InterfaceTemplate
@@ -393,7 +305,7 @@ class WritableInterfaceTemplateSerializer(ValidatedModelSerializer):
 # Device bay templates
 #
 
-class DeviceBayTemplateSerializer(serializers.ModelSerializer):
+class DeviceBayTemplateSerializer(ValidatedModelSerializer):
     device_type = NestedDeviceTypeSerializer()
 
     class Meta:
@@ -401,13 +313,6 @@ class DeviceBayTemplateSerializer(serializers.ModelSerializer):
         fields = ['id', 'device_type', 'name']
 
 
-class WritableDeviceBayTemplateSerializer(ValidatedModelSerializer):
-
-    class Meta:
-        model = DeviceBayTemplate
-        fields = ['id', 'device_type', 'name']
-
-
 #
 # Device roles
 #
@@ -419,7 +324,7 @@ class DeviceRoleSerializer(ValidatedModelSerializer):
         fields = ['id', 'name', 'slug', 'color', 'vm_role']
 
 
-class NestedDeviceRoleSerializer(serializers.ModelSerializer):
+class NestedDeviceRoleSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
 
     class Meta:
@@ -431,15 +336,15 @@ class NestedDeviceRoleSerializer(serializers.ModelSerializer):
 # Platforms
 #
 
-class PlatformSerializer(serializers.ModelSerializer):
-    manufacturer = NestedManufacturerSerializer()
+class PlatformSerializer(ValidatedModelSerializer):
+    manufacturer = NestedManufacturerSerializer(required=False)
 
     class Meta:
         model = Platform
         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')
 
     class Meta:
@@ -447,13 +352,6 @@ class NestedPlatformSerializer(serializers.ModelSerializer):
         fields = ['id', 'url', 'name', 'slug']
 
 
-class WritablePlatformSerializer(ValidatedModelSerializer):
-
-    class Meta:
-        model = Platform
-        fields = ['id', 'name', 'slug', 'manufacturer', 'napalm_driver', 'rpc_client']
-
-
 #
 # Devices
 #
@@ -489,18 +387,18 @@ class DeviceVirtualChassisSerializer(serializers.ModelSerializer):
 class DeviceSerializer(CustomFieldModelSerializer):
     device_type = NestedDeviceTypeSerializer()
     device_role = NestedDeviceRoleSerializer()
-    tenant = NestedTenantSerializer()
-    platform = NestedPlatformSerializer()
+    tenant = NestedTenantSerializer(required=False)
+    platform = NestedPlatformSerializer(required=False)
     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)
+    face = ChoiceFieldSerializer(choices=RACK_FACE_CHOICES, required=False)
+    status = ChoiceFieldSerializer(choices=DEVICE_STATUS_CHOICES, required=False)
+    primary_ip = DeviceIPAddressSerializer(read_only=True)
+    primary_ip4 = DeviceIPAddressSerializer(required=False)
+    primary_ip6 = DeviceIPAddressSerializer(required=False)
     parent_device = serializers.SerializerMethodField()
-    cluster = NestedClusterSerializer()
-    virtual_chassis = DeviceVirtualChassisSerializer()
+    cluster = NestedClusterSerializer(required=False)
+    virtual_chassis = DeviceVirtualChassisSerializer(required=False)
 
     class Meta:
         model = Device
@@ -510,27 +408,6 @@ class DeviceSerializer(CustomFieldModelSerializer):
             'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'custom_fields', 'created',
             '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 = []
 
     def validate(self, data):
@@ -542,8 +419,18 @@ class WritableDeviceSerializer(CustomFieldModelSerializer):
             validator(data)
 
         # 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
 
 
@@ -551,7 +438,7 @@ class WritableDeviceSerializer(CustomFieldModelSerializer):
 # Console server ports
 #
 
-class ConsoleServerPortSerializer(serializers.ModelSerializer):
+class ConsoleServerPortSerializer(ValidatedModelSerializer):
     device = NestedDeviceSerializer()
 
     class Meta:
@@ -560,27 +447,22 @@ class ConsoleServerPortSerializer(serializers.ModelSerializer):
         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:
         model = ConsoleServerPort
-        fields = ['id', 'device', 'name']
+        fields = ['id', 'url', 'device', 'name']
 
 
 #
 # Console ports
 #
 
-class ConsolePortSerializer(serializers.ModelSerializer):
+class ConsolePortSerializer(ValidatedModelSerializer):
     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)
 
     class Meta:
         model = ConsolePort
@@ -591,7 +473,7 @@ class WritableConsolePortSerializer(ValidatedModelSerializer):
 # Power outlets
 #
 
-class PowerOutletSerializer(serializers.ModelSerializer):
+class PowerOutletSerializer(ValidatedModelSerializer):
     device = NestedDeviceSerializer()
 
     class Meta:
@@ -600,27 +482,22 @@ class PowerOutletSerializer(serializers.ModelSerializer):
         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:
         model = PowerOutlet
-        fields = ['id', 'device', 'name']
+        fields = ['id', 'url', 'device', 'name']
 
 
 #
 # Power ports
 #
 
-class PowerPortSerializer(serializers.ModelSerializer):
+class PowerPortSerializer(ValidatedModelSerializer):
     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)
 
     class Meta:
         model = PowerPort
@@ -631,12 +508,13 @@ class WritablePowerPortSerializer(ValidatedModelSerializer):
 # Interfaces
 #
 
-class NestedInterfaceSerializer(serializers.ModelSerializer):
+class NestedInterfaceSerializer(WritableNestedSerializer):
+    device = NestedDeviceSerializer(read_only=True)
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
 
     class Meta:
         model = Interface
-        fields = ['id', 'url', 'name']
+        fields = ['id', 'url', 'device', 'name']
 
 
 class InterfaceNestedCircuitSerializer(serializers.ModelSerializer):
@@ -647,8 +525,8 @@ class InterfaceNestedCircuitSerializer(serializers.ModelSerializer):
         fields = ['id', 'url', 'cid']
 
 
-class InterfaceCircuitTerminationSerializer(serializers.ModelSerializer):
-    circuit = InterfaceNestedCircuitSerializer()
+class InterfaceCircuitTerminationSerializer(WritableNestedSerializer):
+    circuit = InterfaceNestedCircuitSerializer(read_only=True)
 
     class Meta:
         model = CircuitTermination
@@ -658,7 +536,7 @@ class InterfaceCircuitTerminationSerializer(serializers.ModelSerializer):
 
 
 # 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')
 
     class Meta:
@@ -666,16 +544,16 @@ class InterfaceVLANSerializer(serializers.ModelSerializer):
         fields = ['id', 'url', 'vid', 'name', 'display_name']
 
 
-class InterfaceSerializer(serializers.ModelSerializer):
+class InterfaceSerializer(ValidatedModelSerializer):
     device = NestedDeviceSerializer()
-    form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES)
-    lag = NestedInterfaceSerializer()
+    form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES, required=False)
+    lag = NestedInterfaceSerializer(required=False)
     is_connected = 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(required=False)
+    untagged_vlan = InterfaceVLANSerializer(required=False)
+    mode = ChoiceFieldSerializer(choices=IFACE_MODE_CHOICES, required=False)
+    tagged_vlans = InterfaceVLANSerializer(many=True, required=False)
 
     class Meta:
         model = Interface
@@ -684,6 +562,25 @@ class InterfaceSerializer(serializers.ModelSerializer):
             '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):
         """
         Return True if the interface has a connected interface or circuit termination.
@@ -700,69 +597,40 @@ class InterfaceSerializer(serializers.ModelSerializer):
     def get_interface_connection(self, obj):
         if obj.connection:
             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),
             ))
         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)
+# class PeerInterfaceSerializer(serializers.ModelSerializer):
+#     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
+#     device = NestedDeviceSerializer()
+#     form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES)
+#     lag = NestedInterfaceSerializer()
+#
+#     class Meta:
+#         model = Interface
+#         fields = [
+#             'id', 'url', 'device', 'name', 'form_factor', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only',
+#             'description',
+#         ]
 
 
 #
 # Device bays
 #
 
-class DeviceBaySerializer(serializers.ModelSerializer):
+class DeviceBaySerializer(ValidatedModelSerializer):
     device = NestedDeviceSerializer()
-    installed_device = NestedDeviceSerializer()
+    installed_device = NestedDeviceSerializer(required=False)
 
     class Meta:
         model = DeviceBay
         fields = ['id', 'device', 'name', 'installed_device']
 
 
-class NestedDeviceBaySerializer(serializers.ModelSerializer):
+class NestedDeviceBaySerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail')
 
     class Meta:
@@ -770,32 +638,15 @@ class NestedDeviceBaySerializer(serializers.ModelSerializer):
         fields = ['id', 'url', 'name']
 
 
-class WritableDeviceBaySerializer(ValidatedModelSerializer):
-
-    class Meta:
-        model = DeviceBay
-        fields = ['id', 'device', 'name', 'installed_device']
-
-
 #
 # Inventory items
 #
 
-class InventoryItemSerializer(serializers.ModelSerializer):
+class InventoryItemSerializer(ValidatedModelSerializer):
     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
     parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None)
+    manufacturer = NestedManufacturerSerializer()
 
     class Meta:
         model = InventoryItem
@@ -809,17 +660,17 @@ class WritableInventoryItemSerializer(ValidatedModelSerializer):
 # 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:
         model = InterfaceConnection
         fields = ['id', 'interface_a', 'interface_b', 'connection_status']
 
 
-class NestedInterfaceConnectionSerializer(serializers.ModelSerializer):
+class NestedInterfaceConnectionSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interfaceconnection-detail')
 
     class Meta:
@@ -827,18 +678,11 @@ class NestedInterfaceConnectionSerializer(serializers.ModelSerializer):
         fields = ['id', 'url', 'connection_status']
 
 
-class WritableInterfaceConnectionSerializer(ValidatedModelSerializer):
-
-    class Meta:
-        model = InterfaceConnection
-        fields = ['id', 'interface_a', 'interface_b', 'connection_status']
-
-
 #
 # Virtual chassis
 #
 
-class VirtualChassisSerializer(serializers.ModelSerializer):
+class VirtualChassisSerializer(ValidatedModelSerializer):
     master = NestedDeviceSerializer()
 
     class Meta:
@@ -846,16 +690,9 @@ class VirtualChassisSerializer(serializers.ModelSerializer):
         fields = ['id', 'master', 'domain']
 
 
-class NestedVirtualChassisSerializer(serializers.ModelSerializer):
+class NestedVirtualChassisSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
 
     class Meta:
         model = VirtualChassis
         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):
     queryset = Region.objects.all()
     serializer_class = serializers.RegionSerializer
-    write_serializer_class = serializers.WritableRegionSerializer
     filter_class = filters.RegionFilter
 
 
@@ -63,7 +62,6 @@ class RegionViewSet(ModelViewSet):
 class SiteViewSet(CustomFieldModelViewSet):
     queryset = Site.objects.select_related('region', 'tenant')
     serializer_class = serializers.SiteSerializer
-    write_serializer_class = serializers.WritableSiteSerializer
     filter_class = filters.SiteFilter
 
     @detail_route()
@@ -84,7 +82,6 @@ class SiteViewSet(CustomFieldModelViewSet):
 class RackGroupViewSet(ModelViewSet):
     queryset = RackGroup.objects.select_related('site')
     serializer_class = serializers.RackGroupSerializer
-    write_serializer_class = serializers.WritableRackGroupSerializer
     filter_class = filters.RackGroupFilter
 
 
@@ -105,7 +102,6 @@ class RackRoleViewSet(ModelViewSet):
 class RackViewSet(CustomFieldModelViewSet):
     queryset = Rack.objects.select_related('site', 'group__site', 'tenant')
     serializer_class = serializers.RackSerializer
-    write_serializer_class = serializers.WritableRackSerializer
     filter_class = filters.RackFilter
 
     @detail_route()
@@ -136,7 +132,6 @@ class RackViewSet(CustomFieldModelViewSet):
 class RackReservationViewSet(ModelViewSet):
     queryset = RackReservation.objects.select_related('rack', 'user', 'tenant')
     serializer_class = serializers.RackReservationSerializer
-    write_serializer_class = serializers.WritableRackReservationSerializer
     filter_class = filters.RackReservationFilter
 
     # Assign user from request
@@ -161,7 +156,6 @@ class ManufacturerViewSet(ModelViewSet):
 class DeviceTypeViewSet(CustomFieldModelViewSet):
     queryset = DeviceType.objects.select_related('manufacturer')
     serializer_class = serializers.DeviceTypeSerializer
-    write_serializer_class = serializers.WritableDeviceTypeSerializer
     filter_class = filters.DeviceTypeFilter
 
 
@@ -172,42 +166,36 @@ class DeviceTypeViewSet(CustomFieldModelViewSet):
 class ConsolePortTemplateViewSet(ModelViewSet):
     queryset = ConsolePortTemplate.objects.select_related('device_type__manufacturer')
     serializer_class = serializers.ConsolePortTemplateSerializer
-    write_serializer_class = serializers.WritableConsolePortTemplateSerializer
     filter_class = filters.ConsolePortTemplateFilter
 
 
 class ConsoleServerPortTemplateViewSet(ModelViewSet):
     queryset = ConsoleServerPortTemplate.objects.select_related('device_type__manufacturer')
     serializer_class = serializers.ConsoleServerPortTemplateSerializer
-    write_serializer_class = serializers.WritableConsoleServerPortTemplateSerializer
     filter_class = filters.ConsoleServerPortTemplateFilter
 
 
 class PowerPortTemplateViewSet(ModelViewSet):
     queryset = PowerPortTemplate.objects.select_related('device_type__manufacturer')
     serializer_class = serializers.PowerPortTemplateSerializer
-    write_serializer_class = serializers.WritablePowerPortTemplateSerializer
     filter_class = filters.PowerPortTemplateFilter
 
 
 class PowerOutletTemplateViewSet(ModelViewSet):
     queryset = PowerOutletTemplate.objects.select_related('device_type__manufacturer')
     serializer_class = serializers.PowerOutletTemplateSerializer
-    write_serializer_class = serializers.WritablePowerOutletTemplateSerializer
     filter_class = filters.PowerOutletTemplateFilter
 
 
 class InterfaceTemplateViewSet(ModelViewSet):
     queryset = InterfaceTemplate.objects.select_related('device_type__manufacturer')
     serializer_class = serializers.InterfaceTemplateSerializer
-    write_serializer_class = serializers.WritableInterfaceTemplateSerializer
     filter_class = filters.InterfaceTemplateFilter
 
 
 class DeviceBayTemplateViewSet(ModelViewSet):
     queryset = DeviceBayTemplate.objects.select_related('device_type__manufacturer')
     serializer_class = serializers.DeviceBayTemplateSerializer
-    write_serializer_class = serializers.WritableDeviceBayTemplateSerializer
     filter_class = filters.DeviceBayTemplateFilter
 
 
@@ -228,7 +216,6 @@ class DeviceRoleViewSet(ModelViewSet):
 class PlatformViewSet(ModelViewSet):
     queryset = Platform.objects.all()
     serializer_class = serializers.PlatformSerializer
-    write_serializer_class = serializers.WritablePlatformSerializer
     filter_class = filters.PlatformFilter
 
 
@@ -244,7 +231,6 @@ class DeviceViewSet(CustomFieldModelViewSet):
         'primary_ip4__nat_outside', 'primary_ip6__nat_outside',
     )
     serializer_class = serializers.DeviceSerializer
-    write_serializer_class = serializers.WritableDeviceSerializer
     filter_class = filters.DeviceFilter
 
     @detail_route(url_path='napalm')
@@ -318,35 +304,30 @@ class DeviceViewSet(CustomFieldModelViewSet):
 class ConsolePortViewSet(ModelViewSet):
     queryset = ConsolePort.objects.select_related('device', 'cs_port__device')
     serializer_class = serializers.ConsolePortSerializer
-    write_serializer_class = serializers.WritableConsolePortSerializer
     filter_class = filters.ConsolePortFilter
 
 
 class ConsoleServerPortViewSet(ModelViewSet):
     queryset = ConsoleServerPort.objects.select_related('device', 'connected_console__device')
     serializer_class = serializers.ConsoleServerPortSerializer
-    write_serializer_class = serializers.WritableConsoleServerPortSerializer
     filter_class = filters.ConsoleServerPortFilter
 
 
 class PowerPortViewSet(ModelViewSet):
     queryset = PowerPort.objects.select_related('device', 'power_outlet__device')
     serializer_class = serializers.PowerPortSerializer
-    write_serializer_class = serializers.WritablePowerPortSerializer
     filter_class = filters.PowerPortFilter
 
 
 class PowerOutletViewSet(ModelViewSet):
     queryset = PowerOutlet.objects.select_related('device', 'connected_port__device')
     serializer_class = serializers.PowerOutletSerializer
-    write_serializer_class = serializers.WritablePowerOutletSerializer
     filter_class = filters.PowerOutletFilter
 
 
 class InterfaceViewSet(ModelViewSet):
     queryset = Interface.objects.select_related('device')
     serializer_class = serializers.InterfaceSerializer
-    write_serializer_class = serializers.WritableInterfaceSerializer
     filter_class = filters.InterfaceFilter
 
     @detail_route()
@@ -363,14 +344,12 @@ class InterfaceViewSet(ModelViewSet):
 class DeviceBayViewSet(ModelViewSet):
     queryset = DeviceBay.objects.select_related('installed_device')
     serializer_class = serializers.DeviceBaySerializer
-    write_serializer_class = serializers.WritableDeviceBaySerializer
     filter_class = filters.DeviceBayFilter
 
 
 class InventoryItemViewSet(ModelViewSet):
     queryset = InventoryItem.objects.select_related('device', 'manufacturer')
     serializer_class = serializers.InventoryItemSerializer
-    write_serializer_class = serializers.WritableInventoryItemSerializer
     filter_class = filters.InventoryItemFilter
 
 
@@ -393,7 +372,6 @@ class PowerConnectionViewSet(ListModelMixin, GenericViewSet):
 class InterfaceConnectionViewSet(ModelViewSet):
     queryset = InterfaceConnection.objects.select_related('interface_a__device', 'interface_b__device')
     serializer_class = serializers.InterfaceConnectionSerializer
-    write_serializer_class = serializers.WritableInterfaceConnectionSerializer
     filter_class = filters.InterfaceConnectionFilter
 
 
@@ -404,7 +382,6 @@ class InterfaceConnectionViewSet(ModelViewSet):
 class VirtualChassisViewSet(ModelViewSet):
     queryset = VirtualChassis.objects.all()
     serializer_class = serializers.VirtualChassisSerializer
-    write_serializer_class = serializers.WritableVirtualChassisSerializer
 
 
 #

+ 9 - 9
netbox/dcim/tests/test_api.py

@@ -1,6 +1,7 @@
 from __future__ import unicode_literals
 
 from django.contrib.auth.models import User
+from django.test.utils import override_settings
 from django.urls import reverse
 from rest_framework import status
 from rest_framework.test import APITestCase
@@ -2321,6 +2322,7 @@ class InterfaceTest(HttpStatusMixin, APITestCase):
         self.assertEqual(interface4.device_id, data['device'])
         self.assertEqual(interface4.name, data['name'])
 
+    @override_settings(DEBUG=True)
     def test_create_interface_with_802_1q(self):
 
         data = {
@@ -2368,6 +2370,7 @@ class InterfaceTest(HttpStatusMixin, APITestCase):
         self.assertEqual(response.data[1]['name'], data[1]['name'])
         self.assertEqual(response.data[2]['name'], data[2]['name'])
 
+    @override_settings(DEBUG=True)
     def test_create_interface_802_1q_bulk(self):
 
         data = [
@@ -2852,9 +2855,9 @@ class InterfaceConnectionTest(HttpStatusMixin, APITestCase):
 
         self.assertHttpStatus(response, status.HTTP_201_CREATED)
         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):
 
@@ -3052,12 +3055,9 @@ class VirtualChassisTest(HttpStatusMixin, APITestCase):
         response = self.client.post(url, data, format='json', **self.header)
         self.assertHttpStatus(response, status.HTTP_201_CREATED)
         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):
 

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

@@ -15,7 +15,7 @@ from utilities.api import ChoiceFieldSerializer, ContentTypeFieldSerializer, Val
 # Graphs
 #
 
-class GraphSerializer(serializers.ModelSerializer):
+class GraphSerializer(ValidatedModelSerializer):
     type = ChoiceFieldSerializer(choices=GRAPH_TYPE_CHOICES)
 
     class Meta:
@@ -23,13 +23,6 @@ class GraphSerializer(serializers.ModelSerializer):
         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):
     embed_url = serializers.SerializerMethodField()
     embed_link = serializers.SerializerMethodField()
@@ -50,7 +43,7 @@ class RenderedGraphSerializer(serializers.ModelSerializer):
 # Export templates
 #
 
-class ExportTemplateSerializer(serializers.ModelSerializer):
+class ExportTemplateSerializer(ValidatedModelSerializer):
 
     class Meta:
         model = ExportTemplate
@@ -61,7 +54,7 @@ class ExportTemplateSerializer(serializers.ModelSerializer):
 # Topology maps
 #
 
-class TopologyMapSerializer(serializers.ModelSerializer):
+class TopologyMapSerializer(ValidatedModelSerializer):
     site = NestedSiteSerializer()
 
     class Meta:
@@ -69,45 +62,19 @@ class TopologyMapSerializer(serializers.ModelSerializer):
         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
 #
 
-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()
+    parent = serializers.SerializerMethodField(read_only=True)
 
     class Meta:
         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):
 
@@ -120,10 +87,24 @@ class WritableImageAttachmentSerializer(ValidatedModelSerializer):
             )
 
         # Enforce model validation
-        super(WritableImageAttachmentSerializer, self).validate(data)
+        super(ImageAttachmentSerializer, self).validate(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

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

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

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

@@ -4,7 +4,7 @@ from rest_framework import serializers
 
 from extras.api.customfields import CustomFieldModelSerializer
 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']
 
 
-class NestedTenantGroupSerializer(serializers.ModelSerializer):
+class NestedTenantGroupSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenantgroup-detail')
 
     class Meta:
@@ -31,23 +31,16 @@ class NestedTenantGroupSerializer(serializers.ModelSerializer):
 #
 
 class TenantSerializer(CustomFieldModelSerializer):
-    group = NestedTenantGroupSerializer()
+    group = NestedTenantGroupSerializer(required=False)
 
     class Meta:
         model = Tenant
         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')
 
     class Meta:
         model = Tenant
         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):
     queryset = Tenant.objects.select_related('group')
     serializer_class = serializers.TenantSerializer
-    write_serializer_class = serializers.WritableTenantSerializer
     filter_class = filters.TenantFilter

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

@@ -1,10 +1,11 @@
 from __future__ import unicode_literals
 
 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:
         model = User