From db3cbaf83bc5938ee7b513091e5664e96378d957 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 4 Apr 2018 15:39:14 -0400 Subject: [PATCH 1/6] Introduced WritableNestedSerializer --- netbox/utilities/api.py | 112 +++++++++++++++++++++++----------------- 1 file changed, 64 insertions(+), 48 deletions(-) diff --git a/netbox/utilities/api.py b/netbox/utilities/api.py index c54379dff..3f01da7a9 100644 --- a/netbox/utilities/api.py +++ b/netbox/utilities/api.py @@ -5,6 +5,7 @@ import pytz from django.conf import settings from django.contrib.contenttypes.models import ContentType +from django.core.exceptions import ObjectDoesNotExist from django.db.models import ManyToManyField from django.http import Http404 from rest_framework import mixins @@ -36,6 +37,64 @@ class IsAuthenticatedOrLoginNotRequired(BasePermission): return request.user.is_authenticated +# +# Fields +# + +class ChoiceFieldSerializer(Field): + """ + Represent a ChoiceField as {'value': , 'label': }. + """ + def __init__(self, choices, **kwargs): + self._choices = dict() + for k, v in choices: + # Unpack grouped choices + if type(v) in [list, tuple]: + for k2, v2 in v: + self._choices[k2] = v2 + else: + self._choices[k] = v + super(ChoiceFieldSerializer, self).__init__(**kwargs) + + def to_representation(self, obj): + return {'value': obj, 'label': self._choices[obj]} + + def to_internal_value(self, data): + return data + + +class ContentTypeFieldSerializer(Field): + """ + Represent a ContentType as '.' + """ + def to_representation(self, obj): + return "{}.{}".format(obj.app_label, obj.model) + + def to_internal_value(self, data): + app_label, model = data.split('.') + try: + return ContentType.objects.get_by_natural_key(app_label=app_label, model=model) + except ContentType.DoesNotExist: + raise ValidationError("Invalid content type") + + +class TimeZoneField(Field): + """ + Represent a pytz time zone. + """ + + def to_representation(self, obj): + return obj.zone if obj else None + + def to_internal_value(self, data): + if not data: + return "" + try: + return pytz.timezone(str(data)) + except pytz.exceptions.UnknownTimeZoneError: + raise ValidationError('Invalid time zone "{}"'.format(data)) + + # # Serializers # @@ -67,58 +126,15 @@ class ValidatedModelSerializer(ModelSerializer): return data -class ChoiceFieldSerializer(Field): +class WritableNestedSerializer(ModelSerializer): """ - Represent a ChoiceField as {'value': , 'label': }. + Returns a nested representation of an object on read, but accepts only a primary key on write. """ - def __init__(self, choices, **kwargs): - self._choices = dict() - for k, v in choices: - # Unpack grouped choices - if type(v) in [list, tuple]: - for k2, v2 in v: - self._choices[k2] = v2 - else: - self._choices[k] = v - super(ChoiceFieldSerializer, self).__init__(**kwargs) - - def to_representation(self, obj): - return {'value': obj, 'label': self._choices[obj]} - def to_internal_value(self, data): - return self._choices.get(data) - - -class ContentTypeFieldSerializer(Field): - """ - Represent a ContentType as '.' - """ - def to_representation(self, obj): - return "{}.{}".format(obj.app_label, obj.model) - - def to_internal_value(self, data): - app_label, model = data.split('.') try: - return ContentType.objects.get_by_natural_key(app_label=app_label, model=model) - except ContentType.DoesNotExist: - raise ValidationError("Invalid content type") - - -class TimeZoneField(Field): - """ - Represent a pytz time zone. - """ - - def to_representation(self, obj): - return obj.zone if obj else None - - def to_internal_value(self, data): - if not data: - return "" - try: - return pytz.timezone(str(data)) - except pytz.exceptions.UnknownTimeZoneError: - raise ValidationError('Invalid time zone "{}"'.format(data)) + return self.Meta.model.objects.get(pk=data) + except ObjectDoesNotExist: + raise ValidationError("Invalid ID") # From 7241783249eb3b751d73de836d6266209ceaf462 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 4 Apr 2018 17:01:24 -0400 Subject: [PATCH 2/6] Started merging writable serializers (WIP) --- netbox/circuits/api/serializers.py | 45 +-- netbox/circuits/api/views.py | 3 - netbox/dcim/api/serializers.py | 441 +++++++++-------------------- netbox/dcim/api/views.py | 23 -- netbox/dcim/tests/test_api.py | 18 +- netbox/extras/api/serializers.py | 67 ++--- netbox/extras/api/views.py | 3 - netbox/tenancy/api/serializers.py | 15 +- netbox/tenancy/api/views.py | 1 - netbox/users/api/serializers.py | 5 +- 10 files changed, 187 insertions(+), 434 deletions(-) diff --git a/netbox/circuits/api/serializers.py b/netbox/circuits/api/serializers.py index db550a63b..af56aef47 100644 --- a/netbox/circuits/api/serializers.py +++ b/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 diff --git a/netbox/circuits/api/views.py b/netbox/circuits/api/views.py index 9b75bc184..d70a0596c 100644 --- a/netbox/circuits/api/views.py +++ b/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 diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index d458bc646..f791a83de 100644 --- a/netbox/dcim/api/serializers.py +++ b/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,16 +419,26 @@ 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 + # # 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,51 +562,6 @@ class InterfaceSerializer(serializers.ModelSerializer): 'is_connected', 'interface_connection', 'circuit_termination', 'mode', 'untagged_vlan', 'tagged_vlans', ] - def get_is_connected(self, obj): - """ - Return True if the interface has a connected interface or circuit termination. - """ - if obj.connection: - return True - try: - circuit_termination = obj.circuit_termination - return True - except CircuitTermination.DoesNotExist: - pass - return False - - def get_interface_connection(self, obj): - if obj.connection: - return OrderedDict(( - ('interface', PeerInterfaceSerializer(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. @@ -746,23 +579,58 @@ class WritableInterfaceSerializer(ValidatedModelSerializer): "be global.".format(vlan) }) - return super(WritableInterfaceSerializer, self).validate(data) + return super(InterfaceSerializer, self).validate(data) + + def get_is_connected(self, obj): + """ + Return True if the interface has a connected interface or circuit termination. + """ + if obj.connection: + return True + try: + circuit_termination = obj.circuit_termination + return True + except CircuitTermination.DoesNotExist: + pass + return False + + def get_interface_connection(self, obj): + if obj.connection: + return OrderedDict(( + ('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', +# ] # # 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'] diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 13f68639f..5ef4b1de7 100644 --- a/netbox/dcim/api/views.py +++ b/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 # diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index b32d7e7a0..6642be440 100644 --- a/netbox/dcim/tests/test_api.py +++ b/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): diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index 6c3cdd409..8678d42a2 100644 --- a/netbox/extras/api/serializers.py +++ b/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,23 +62,34 @@ 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 ImageAttachmentSerializer(ValidatedModelSerializer): + content_type = ContentTypeFieldSerializer() + parent = serializers.SerializerMethodField(read_only=True) class Meta: model = ImageAttachment - fields = ['id', 'parent', 'name', 'image', 'image_height', 'image_width', 'created'] + fields = [ + 'id', 'content_type', 'object_id', 'parent', 'name', 'image', 'image_height', 'image_width', 'created', + ] + + def validate(self, data): + + # Validate that the parent object exists + try: + data['content_type'].get_object_for_this_type(id=data['object_id']) + except ObjectDoesNotExist: + raise serializers.ValidationError( + "Invalid parent object: {} ID {}".format(data['content_type'], data['object_id']) + ) + + # Enforce model validation + super(ImageAttachmentSerializer, self).validate(data) + + return data def get_parent(self, obj): @@ -102,29 +106,6 @@ class ImageAttachmentSerializer(serializers.ModelSerializer): return serializer(obj.parent, context={'request': self.context['request']}).data -class WritableImageAttachmentSerializer(ValidatedModelSerializer): - content_type = ContentTypeFieldSerializer() - - class Meta: - model = ImageAttachment - fields = ['id', 'content_type', 'object_id', 'name', 'image'] - - def validate(self, data): - - # Validate that the parent object exists - try: - data['content_type'].get_object_for_this_type(id=data['object_id']) - except ObjectDoesNotExist: - raise serializers.ValidationError( - "Invalid parent object: {} ID {}".format(data['content_type'], data['object_id']) - ) - - # Enforce model validation - super(WritableImageAttachmentSerializer, self).validate(data) - - return data - - # # Reports # diff --git a/netbox/extras/api/views.py b/netbox/extras/api/views.py index 252c2d12c..047abcb44 100644 --- a/netbox/extras/api/views.py +++ b/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 # diff --git a/netbox/tenancy/api/serializers.py b/netbox/tenancy/api/serializers.py index 454e41c52..3a6e1fb4b 100644 --- a/netbox/tenancy/api/serializers.py +++ b/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'] diff --git a/netbox/tenancy/api/views.py b/netbox/tenancy/api/views.py index 26f9bc71e..1ebd95500 100644 --- a/netbox/tenancy/api/views.py +++ b/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 diff --git a/netbox/users/api/serializers.py b/netbox/users/api/serializers.py index 80f79516c..861bdade9 100644 --- a/netbox/users/api/serializers.py +++ b/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 From 821fb1e01e040e646f1980ff0abfa8c3160e0ae9 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 5 Apr 2018 14:12:43 -0400 Subject: [PATCH 3/6] Finished merging writable serializers --- netbox/circuits/api/serializers.py | 4 +- netbox/dcim/api/serializers.py | 56 +++----- netbox/ipam/api/serializers.py | 167 +++++++---------------- netbox/ipam/api/views.py | 15 +- netbox/secrets/api/serializers.py | 15 +- netbox/secrets/api/views.py | 1 - netbox/utilities/api.py | 12 +- netbox/virtualization/api/serializers.py | 62 +++------ netbox/virtualization/api/views.py | 3 - 9 files changed, 105 insertions(+), 230 deletions(-) diff --git a/netbox/circuits/api/serializers.py b/netbox/circuits/api/serializers.py index af56aef47..ded67c934 100644 --- a/netbox/circuits/api/serializers.py +++ b/netbox/circuits/api/serializers.py @@ -59,7 +59,7 @@ class CircuitSerializer(CustomFieldModelSerializer): provider = NestedProviderSerializer() status = ChoiceFieldSerializer(choices=CIRCUIT_STATUS_CHOICES, required=False) type = NestedCircuitTypeSerializer() - tenant = NestedTenantSerializer(required=False) + tenant = NestedTenantSerializer(required=False, allow_null=True) class Meta: model = Circuit @@ -84,7 +84,7 @@ class NestedCircuitSerializer(WritableNestedSerializer): class CircuitTerminationSerializer(ValidatedModelSerializer): circuit = NestedCircuitSerializer() site = NestedSiteSerializer() - interface = InterfaceSerializer(required=False) + interface = InterfaceSerializer(required=False, allow_null=True) class Meta: model = CircuitTermination diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index f791a83de..7c5191477 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -37,7 +37,7 @@ class NestedRegionSerializer(WritableNestedSerializer): class RegionSerializer(serializers.ModelSerializer): - parent = NestedRegionSerializer(required=False) + parent = NestedRegionSerializer(required=False, allow_null=True) class Meta: model = Region @@ -50,8 +50,8 @@ class RegionSerializer(serializers.ModelSerializer): class SiteSerializer(CustomFieldModelSerializer): status = ChoiceFieldSerializer(choices=SITE_STATUS_CHOICES, required=False) - region = NestedRegionSerializer(required=False) - tenant = NestedTenantSerializer(required=False) + region = NestedRegionSerializer(required=False, allow_null=True) + tenant = NestedTenantSerializer(required=False, allow_null=True) time_zone = TimeZoneField(required=False) class Meta: @@ -117,9 +117,9 @@ class NestedRackRoleSerializer(WritableNestedSerializer): class RackSerializer(CustomFieldModelSerializer): site = NestedSiteSerializer() - group = NestedRackGroupSerializer(required=False) - tenant = NestedTenantSerializer(required=False) - role = NestedRackRoleSerializer(required=False) + group = NestedRackGroupSerializer(required=False, allow_null=True) + tenant = NestedTenantSerializer(required=False, allow_null=True) + role = NestedRackRoleSerializer(required=False, allow_null=True) type = ChoiceFieldSerializer(choices=RACK_TYPE_CHOICES, required=False) width = ChoiceFieldSerializer(choices=RACK_WIDTH_CHOICES, required=False) @@ -186,7 +186,7 @@ class RackUnitSerializer(serializers.Serializer): class RackReservationSerializer(ValidatedModelSerializer): rack = NestedRackSerializer() user = NestedUserSerializer() - tenant = NestedTenantSerializer(required=False) + tenant = NestedTenantSerializer(required=False, allow_null=True) class Meta: model = RackReservation @@ -337,7 +337,7 @@ class NestedDeviceRoleSerializer(WritableNestedSerializer): # class PlatformSerializer(ValidatedModelSerializer): - manufacturer = NestedManufacturerSerializer(required=False) + manufacturer = NestedManufacturerSerializer(required=False, allow_null=True) class Meta: model = Platform @@ -387,18 +387,18 @@ class DeviceVirtualChassisSerializer(serializers.ModelSerializer): class DeviceSerializer(CustomFieldModelSerializer): device_type = NestedDeviceTypeSerializer() device_role = NestedDeviceRoleSerializer() - tenant = NestedTenantSerializer(required=False) - platform = NestedPlatformSerializer(required=False) + tenant = NestedTenantSerializer(required=False, allow_null=True) + platform = NestedPlatformSerializer(required=False, allow_null=True) site = NestedSiteSerializer() - rack = NestedRackSerializer(required=False) + rack = NestedRackSerializer(required=False, allow_null=True) face = ChoiceFieldSerializer(choices=RACK_FACE_CHOICES, required=False) status = ChoiceFieldSerializer(choices=DEVICE_STATUS_CHOICES, required=False) primary_ip = DeviceIPAddressSerializer(read_only=True) - primary_ip4 = DeviceIPAddressSerializer(required=False) - primary_ip6 = DeviceIPAddressSerializer(required=False) + primary_ip4 = DeviceIPAddressSerializer(required=False, allow_null=True) + primary_ip6 = DeviceIPAddressSerializer(required=False, allow_null=True) parent_device = serializers.SerializerMethodField() - cluster = NestedClusterSerializer(required=False) - virtual_chassis = DeviceVirtualChassisSerializer(required=False) + cluster = NestedClusterSerializer(required=False, allow_null=True) + virtual_chassis = DeviceVirtualChassisSerializer(required=False, allow_null=True) class Meta: model = Device @@ -462,7 +462,7 @@ class NestedConsoleServerPortSerializer(WritableNestedSerializer): class ConsolePortSerializer(ValidatedModelSerializer): device = NestedDeviceSerializer() - cs_port = NestedConsoleServerPortSerializer(required=False) + cs_port = NestedConsoleServerPortSerializer(required=False, allow_null=True) class Meta: model = ConsolePort @@ -497,7 +497,7 @@ class NestedPowerOutletSerializer(WritableNestedSerializer): class PowerPortSerializer(ValidatedModelSerializer): device = NestedDeviceSerializer() - power_outlet = NestedPowerOutletSerializer(required=False) + power_outlet = NestedPowerOutletSerializer(required=False, allow_null=True) class Meta: model = PowerPort @@ -547,11 +547,11 @@ class InterfaceVLANSerializer(WritableNestedSerializer): class InterfaceSerializer(ValidatedModelSerializer): device = NestedDeviceSerializer() form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES, required=False) - lag = NestedInterfaceSerializer(required=False) + lag = NestedInterfaceSerializer(required=False, allow_null=True) is_connected = serializers.SerializerMethodField(read_only=True) interface_connection = serializers.SerializerMethodField(read_only=True) - circuit_termination = InterfaceCircuitTerminationSerializer(required=False) - untagged_vlan = InterfaceVLANSerializer(required=False) + circuit_termination = InterfaceCircuitTerminationSerializer(read_only=True) + untagged_vlan = InterfaceVLANSerializer(required=False, allow_null=True) mode = ChoiceFieldSerializer(choices=IFACE_MODE_CHOICES, required=False) tagged_vlans = InterfaceVLANSerializer(many=True, required=False) @@ -603,27 +603,13 @@ class InterfaceSerializer(ValidatedModelSerializer): return None -# class PeerInterfaceSerializer(serializers.ModelSerializer): -# url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail') -# device = NestedDeviceSerializer() -# form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES) -# lag = NestedInterfaceSerializer() -# -# class Meta: -# model = Interface -# fields = [ -# 'id', 'url', 'device', 'name', 'form_factor', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', -# 'description', -# ] - - # # Device bays # class DeviceBaySerializer(ValidatedModelSerializer): device = NestedDeviceSerializer() - installed_device = NestedDeviceSerializer(required=False) + installed_device = NestedDeviceSerializer(required=False, allow_null=True) class Meta: model = DeviceBay diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index 2eca51895..02680bd69 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -14,7 +14,7 @@ from ipam.constants import ( ) from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF from tenancy.api.serializers import NestedTenantSerializer -from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer +from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer, WritableNestedSerializer from virtualization.api.serializers import NestedVirtualMachineSerializer @@ -23,7 +23,7 @@ from virtualization.api.serializers import NestedVirtualMachineSerializer # class VRFSerializer(CustomFieldModelSerializer): - tenant = NestedTenantSerializer() + tenant = NestedTenantSerializer(required=False, allow_null=True) class Meta: model = VRF @@ -33,7 +33,7 @@ class VRFSerializer(CustomFieldModelSerializer): ] -class NestedVRFSerializer(serializers.ModelSerializer): +class NestedVRFSerializer(WritableNestedSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vrf-detail') class Meta: @@ -41,15 +41,6 @@ class NestedVRFSerializer(serializers.ModelSerializer): fields = ['id', 'url', 'name', 'rd'] -class WritableVRFSerializer(CustomFieldModelSerializer): - - class Meta: - model = VRF - fields = [ - 'id', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'custom_fields', 'created', 'last_updated', - ] - - # # Roles # @@ -61,7 +52,7 @@ class RoleSerializer(ValidatedModelSerializer): fields = ['id', 'name', 'slug', 'weight'] -class NestedRoleSerializer(serializers.ModelSerializer): +class NestedRoleSerializer(WritableNestedSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:role-detail') class Meta: @@ -80,7 +71,7 @@ class RIRSerializer(ValidatedModelSerializer): fields = ['id', 'name', 'slug', 'is_private'] -class NestedRIRSerializer(serializers.ModelSerializer): +class NestedRIRSerializer(WritableNestedSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail') class Meta: @@ -100,9 +91,10 @@ class AggregateSerializer(CustomFieldModelSerializer): fields = [ 'id', 'family', 'prefix', 'rir', 'date_added', 'description', 'custom_fields', 'created', 'last_updated', ] + read_only_fields = ['family'] -class NestedAggregateSerializer(serializers.ModelSerializer): +class NestedAggregateSerializer(WritableNestedSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:aggregate-detail') class Meta(AggregateSerializer.Meta): @@ -110,34 +102,12 @@ class NestedAggregateSerializer(serializers.ModelSerializer): fields = ['id', 'url', 'family', 'prefix'] -class WritableAggregateSerializer(CustomFieldModelSerializer): - - class Meta: - model = Aggregate - fields = ['id', 'prefix', 'rir', 'date_added', 'description', 'custom_fields', 'created', 'last_updated'] - - # # VLAN groups # -class VLANGroupSerializer(serializers.ModelSerializer): - site = NestedSiteSerializer() - - class Meta: - model = VLANGroup - fields = ['id', 'name', 'slug', 'site'] - - -class NestedVLANGroupSerializer(serializers.ModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail') - - class Meta: - model = VLANGroup - fields = ['id', 'url', 'name', 'slug'] - - -class WritableVLANGroupSerializer(serializers.ModelSerializer): +class VLANGroupSerializer(ValidatedModelSerializer): + site = NestedSiteSerializer(required=False, allow_null=True) class Meta: model = VLANGroup @@ -154,21 +124,29 @@ class WritableVLANGroupSerializer(serializers.ModelSerializer): validator(data) # Enforce model validation - super(WritableVLANGroupSerializer, self).validate(data) + super(VLANGroupSerializer, self).validate(data) return data +class NestedVLANGroupSerializer(WritableNestedSerializer): + url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail') + + class Meta: + model = VLANGroup + fields = ['id', 'url', 'name', 'slug'] + + # # VLANs # class VLANSerializer(CustomFieldModelSerializer): - site = NestedSiteSerializer() - group = NestedVLANGroupSerializer() - tenant = NestedTenantSerializer() - status = ChoiceFieldSerializer(choices=VLAN_STATUS_CHOICES) - role = NestedRoleSerializer() + site = NestedSiteSerializer(required=False, allow_null=True) + group = NestedVLANGroupSerializer(required=False, allow_null=True) + tenant = NestedTenantSerializer(required=False, allow_null=True) + status = ChoiceFieldSerializer(choices=VLAN_STATUS_CHOICES, required=False) + role = NestedRoleSerializer(required=False, allow_null=True) class Meta: model = VLAN @@ -176,24 +154,6 @@ class VLANSerializer(CustomFieldModelSerializer): 'id', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', 'display_name', 'custom_fields', 'created', 'last_updated', ] - - -class NestedVLANSerializer(serializers.ModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail') - - class Meta: - model = VLAN - fields = ['id', 'url', 'vid', 'name', 'display_name'] - - -class WritableVLANSerializer(CustomFieldModelSerializer): - - class Meta: - model = VLAN - fields = [ - 'id', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', 'custom_fields', 'created', - 'last_updated', - ] validators = [] def validate(self, data): @@ -206,22 +166,30 @@ class WritableVLANSerializer(CustomFieldModelSerializer): validator(data) # Enforce model validation - super(WritableVLANSerializer, self).validate(data) + super(VLANSerializer, self).validate(data) return data +class NestedVLANSerializer(WritableNestedSerializer): + url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail') + + class Meta: + model = VLAN + fields = ['id', 'url', 'vid', 'name', 'display_name'] + + # # Prefixes # class PrefixSerializer(CustomFieldModelSerializer): - site = NestedSiteSerializer() - vrf = NestedVRFSerializer() - tenant = NestedTenantSerializer() - vlan = NestedVLANSerializer() - status = ChoiceFieldSerializer(choices=PREFIX_STATUS_CHOICES) - role = NestedRoleSerializer() + site = NestedSiteSerializer(required=False, allow_null=True) + vrf = NestedVRFSerializer(required=False, allow_null=True) + tenant = NestedTenantSerializer(required=False, allow_null=True) + vlan = NestedVLANSerializer(required=False, allow_null=True) + status = ChoiceFieldSerializer(choices=PREFIX_STATUS_CHOICES, required=False) + role = NestedRoleSerializer(required=False, allow_null=True) class Meta: model = Prefix @@ -229,9 +197,10 @@ class PrefixSerializer(CustomFieldModelSerializer): 'id', 'family', 'prefix', 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', 'description', 'custom_fields', 'created', 'last_updated', ] + read_only_fields = ['family'] -class NestedPrefixSerializer(serializers.ModelSerializer): +class NestedPrefixSerializer(WritableNestedSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:prefix-detail') class Meta: @@ -239,16 +208,6 @@ class NestedPrefixSerializer(serializers.ModelSerializer): fields = ['id', 'url', 'family', 'prefix'] -class WritablePrefixSerializer(CustomFieldModelSerializer): - - class Meta: - model = Prefix - fields = [ - 'id', 'prefix', 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', 'description', - 'custom_fields', 'created', 'last_updated', - ] - - class AvailablePrefixSerializer(serializers.Serializer): def to_representation(self, instance): @@ -288,11 +247,11 @@ class IPAddressInterfaceSerializer(serializers.ModelSerializer): class IPAddressSerializer(CustomFieldModelSerializer): - vrf = NestedVRFSerializer() - tenant = NestedTenantSerializer() - status = ChoiceFieldSerializer(choices=IPADDRESS_STATUS_CHOICES) - role = ChoiceFieldSerializer(choices=IPADDRESS_ROLE_CHOICES) - interface = IPAddressInterfaceSerializer() + vrf = NestedVRFSerializer(required=False, allow_null=True) + tenant = NestedTenantSerializer(required=False, allow_null=True) + status = ChoiceFieldSerializer(choices=IPADDRESS_STATUS_CHOICES, required=False) + role = ChoiceFieldSerializer(choices=IPADDRESS_ROLE_CHOICES, required=False) + interface = IPAddressInterfaceSerializer(required=False, allow_null=True) class Meta: model = IPAddress @@ -300,9 +259,10 @@ class IPAddressSerializer(CustomFieldModelSerializer): 'id', 'family', 'address', 'vrf', 'tenant', 'status', 'role', 'interface', 'description', 'nat_inside', 'nat_outside', 'custom_fields', 'created', 'last_updated', ] + read_only_fields = ['family'] -class NestedIPAddressSerializer(serializers.ModelSerializer): +class NestedIPAddressSerializer(WritableNestedSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail') class Meta: @@ -310,18 +270,8 @@ class NestedIPAddressSerializer(serializers.ModelSerializer): fields = ['id', 'url', 'family', 'address'] -IPAddressSerializer._declared_fields['nat_inside'] = NestedIPAddressSerializer() -IPAddressSerializer._declared_fields['nat_outside'] = NestedIPAddressSerializer() - - -class WritableIPAddressSerializer(CustomFieldModelSerializer): - - class Meta: - model = IPAddress - fields = [ - 'id', 'address', 'vrf', 'tenant', 'status', 'role', 'interface', 'description', 'nat_inside', - 'custom_fields', 'created', 'last_updated', - ] +IPAddressSerializer._declared_fields['nat_inside'] = NestedIPAddressSerializer(required=False, allow_null=True) +IPAddressSerializer._declared_fields['nat_outside'] = NestedIPAddressSerializer(read_only=True) class AvailableIPSerializer(serializers.Serializer): @@ -342,22 +292,11 @@ class AvailableIPSerializer(serializers.Serializer): # Services # -class ServiceSerializer(serializers.ModelSerializer): - device = NestedDeviceSerializer() - virtual_machine = NestedVirtualMachineSerializer() +class ServiceSerializer(ValidatedModelSerializer): + device = NestedDeviceSerializer(required=False, allow_null=True) + virtual_machine = NestedVirtualMachineSerializer(required=False, allow_null=True) protocol = ChoiceFieldSerializer(choices=IP_PROTOCOL_CHOICES) - ipaddresses = NestedIPAddressSerializer(many=True) - - class Meta: - model = Service - fields = [ - 'id', 'device', 'virtual_machine', 'name', 'port', 'protocol', 'ipaddresses', 'description', 'created', - 'last_updated', - ] - - -# TODO: Figure out how to use model validation with ManyToManyFields. Calling clean() yields a ValueError. -class WritableServiceSerializer(serializers.ModelSerializer): + ipaddresses = NestedIPAddressSerializer(many=True, required=False) class Meta: model = Service diff --git a/netbox/ipam/api/views.py b/netbox/ipam/api/views.py index f6a55b618..abbe6e2b1 100644 --- a/netbox/ipam/api/views.py +++ b/netbox/ipam/api/views.py @@ -35,7 +35,6 @@ class IPAMFieldChoicesViewSet(FieldChoicesViewSet): class VRFViewSet(CustomFieldModelViewSet): queryset = VRF.objects.select_related('tenant') serializer_class = serializers.VRFSerializer - write_serializer_class = serializers.WritableVRFSerializer filter_class = filters.VRFFilter @@ -56,7 +55,6 @@ class RIRViewSet(ModelViewSet): class AggregateViewSet(CustomFieldModelViewSet): queryset = Aggregate.objects.select_related('rir') serializer_class = serializers.AggregateSerializer - write_serializer_class = serializers.WritableAggregateSerializer filter_class = filters.AggregateFilter @@ -77,7 +75,6 @@ class RoleViewSet(ModelViewSet): class PrefixViewSet(CustomFieldModelViewSet): queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role') serializer_class = serializers.PrefixSerializer - write_serializer_class = serializers.WritablePrefixSerializer filter_class = filters.PrefixFilter @detail_route(url_path='available-prefixes', methods=['get', 'post']) @@ -120,9 +117,9 @@ class PrefixViewSet(CustomFieldModelViewSet): # Initialize the serializer with a list or a single object depending on what was requested if isinstance(request.data, list): - serializer = serializers.WritablePrefixSerializer(data=requested_prefixes, many=True) + serializer = serializers.PrefixSerializer(data=requested_prefixes, many=True) else: - serializer = serializers.WritablePrefixSerializer(data=requested_prefixes[0]) + serializer = serializers.PrefixSerializer(data=requested_prefixes[0]) # Create the new Prefix(es) if serializer.is_valid(): @@ -177,9 +174,9 @@ class PrefixViewSet(CustomFieldModelViewSet): # Initialize the serializer with a list or a single object depending on what was requested if isinstance(request.data, list): - serializer = serializers.WritableIPAddressSerializer(data=requested_ips, many=True) + serializer = serializers.IPAddressSerializer(data=requested_ips, many=True) else: - serializer = serializers.WritableIPAddressSerializer(data=requested_ips[0]) + serializer = serializers.IPAddressSerializer(data=requested_ips[0]) # Create the new IP address(es) if serializer.is_valid(): @@ -223,7 +220,6 @@ class IPAddressViewSet(CustomFieldModelViewSet): 'nat_outside' ) serializer_class = serializers.IPAddressSerializer - write_serializer_class = serializers.WritableIPAddressSerializer filter_class = filters.IPAddressFilter @@ -234,7 +230,6 @@ class IPAddressViewSet(CustomFieldModelViewSet): class VLANGroupViewSet(ModelViewSet): queryset = VLANGroup.objects.select_related('site') serializer_class = serializers.VLANGroupSerializer - write_serializer_class = serializers.WritableVLANGroupSerializer filter_class = filters.VLANGroupFilter @@ -245,7 +240,6 @@ class VLANGroupViewSet(ModelViewSet): class VLANViewSet(CustomFieldModelViewSet): queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role') serializer_class = serializers.VLANSerializer - write_serializer_class = serializers.WritableVLANSerializer filter_class = filters.VLANFilter @@ -256,5 +250,4 @@ class VLANViewSet(CustomFieldModelViewSet): class ServiceViewSet(ModelViewSet): queryset = Service.objects.select_related('device') serializer_class = serializers.ServiceSerializer - write_serializer_class = serializers.WritableServiceSerializer filter_class = filters.ServiceFilter diff --git a/netbox/secrets/api/serializers.py b/netbox/secrets/api/serializers.py index a4e61a018..aca91920a 100644 --- a/netbox/secrets/api/serializers.py +++ b/netbox/secrets/api/serializers.py @@ -5,7 +5,7 @@ from rest_framework.validators import UniqueTogetherValidator from dcim.api.serializers import NestedDeviceSerializer from secrets.models import Secret, SecretRole -from utilities.api import ValidatedModelSerializer +from utilities.api import ValidatedModelSerializer, WritableNestedSerializer # @@ -19,7 +19,7 @@ class SecretRoleSerializer(ValidatedModelSerializer): fields = ['id', 'name', 'slug'] -class NestedSecretRoleSerializer(serializers.ModelSerializer): +class NestedSecretRoleSerializer(WritableNestedSerializer): url = serializers.HyperlinkedIdentityField(view_name='secrets-api:secretrole-detail') class Meta: @@ -31,16 +31,9 @@ class NestedSecretRoleSerializer(serializers.ModelSerializer): # Secrets # -class SecretSerializer(serializers.ModelSerializer): +class SecretSerializer(ValidatedModelSerializer): device = NestedDeviceSerializer() role = NestedSecretRoleSerializer() - - class Meta: - model = Secret - fields = ['id', 'device', 'role', 'name', 'plaintext', 'hash', 'created', 'last_updated'] - - -class WritableSecretSerializer(serializers.ModelSerializer): plaintext = serializers.CharField() class Meta: @@ -64,6 +57,6 @@ class WritableSecretSerializer(serializers.ModelSerializer): validator(data) # Enforce model validation - super(WritableSecretSerializer, self).validate(data) + super(SecretSerializer, self).validate(data) return data diff --git a/netbox/secrets/api/views.py b/netbox/secrets/api/views.py index 807a87b42..9bc52f9f0 100644 --- a/netbox/secrets/api/views.py +++ b/netbox/secrets/api/views.py @@ -51,7 +51,6 @@ class SecretViewSet(ModelViewSet): 'role__users', 'role__groups', ) serializer_class = serializers.SecretSerializer - write_serializer_class = serializers.WritableSecretSerializer filter_class = filters.SecretFilter master_key = None diff --git a/netbox/utilities/api.py b/netbox/utilities/api.py index 3f01da7a9..63ce23db1 100644 --- a/netbox/utilities/api.py +++ b/netbox/utilities/api.py @@ -131,6 +131,8 @@ class WritableNestedSerializer(ModelSerializer): Returns a nested representation of an object on read, but accepts only a primary key on write. """ def to_internal_value(self, data): + if data is None: + return None try: return self.Meta.model.objects.get(pk=data) except ObjectDoesNotExist: @@ -148,16 +150,8 @@ class ModelViewSet(mixins.CreateModelMixin, mixins.ListModelMixin, GenericViewSet): """ - Substitute DRF's built-in ModelViewSet for our own, which introduces a bit of additional functionality: - 1. Use an alternate serializer (if provided) for write operations - 2. Accept either a single object or a list of objects to create + Accept either a single object or a list of objects to create. """ - def get_serializer_class(self): - # Check for a different serializer to use for write operations - if self.action in WRITE_OPERATIONS and hasattr(self, 'write_serializer_class'): - return self.write_serializer_class - return self.serializer_class - def get_serializer(self, *args, **kwargs): # If a list of objects has been provided, initialize the serializer with many=True if isinstance(kwargs.get('data', {}), list): diff --git a/netbox/virtualization/api/serializers.py b/netbox/virtualization/api/serializers.py index 7e2ec1690..267526fe0 100644 --- a/netbox/virtualization/api/serializers.py +++ b/netbox/virtualization/api/serializers.py @@ -8,7 +8,7 @@ from dcim.models import Interface from extras.api.customfields import CustomFieldModelSerializer from ipam.models import IPAddress from tenancy.api.serializers import NestedTenantSerializer -from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer +from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer, WritableNestedSerializer from virtualization.constants import VM_STATUS_CHOICES from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine @@ -24,7 +24,7 @@ class ClusterTypeSerializer(ValidatedModelSerializer): fields = ['id', 'name', 'slug'] -class NestedClusterTypeSerializer(serializers.ModelSerializer): +class NestedClusterTypeSerializer(WritableNestedSerializer): url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustertype-detail') class Meta: @@ -43,7 +43,7 @@ class ClusterGroupSerializer(ValidatedModelSerializer): fields = ['id', 'name', 'slug'] -class NestedClusterGroupSerializer(serializers.ModelSerializer): +class NestedClusterGroupSerializer(WritableNestedSerializer): url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustergroup-detail') class Meta: @@ -57,15 +57,15 @@ class NestedClusterGroupSerializer(serializers.ModelSerializer): class ClusterSerializer(CustomFieldModelSerializer): type = NestedClusterTypeSerializer() - group = NestedClusterGroupSerializer() - site = NestedSiteSerializer() + group = NestedClusterGroupSerializer(required=False, allow_null=True) + site = NestedSiteSerializer(required=False, allow_null=True) class Meta: model = Cluster fields = ['id', 'name', 'type', 'group', 'site', 'comments', 'custom_fields', 'created', 'last_updated'] -class NestedClusterSerializer(serializers.ModelSerializer): +class NestedClusterSerializer(WritableNestedSerializer): url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:cluster-detail') class Meta: @@ -73,13 +73,6 @@ class NestedClusterSerializer(serializers.ModelSerializer): fields = ['id', 'url', 'name'] -class WritableClusterSerializer(CustomFieldModelSerializer): - - class Meta: - model = Cluster - fields = ['id', 'name', 'type', 'group', 'site', 'comments', 'custom_fields', 'created', 'last_updated'] - - # # Virtual machines # @@ -94,14 +87,14 @@ class VirtualMachineIPAddressSerializer(serializers.ModelSerializer): class VirtualMachineSerializer(CustomFieldModelSerializer): - status = ChoiceFieldSerializer(choices=VM_STATUS_CHOICES) - cluster = NestedClusterSerializer() - role = NestedDeviceRoleSerializer() - tenant = NestedTenantSerializer() - platform = NestedPlatformSerializer() - primary_ip = VirtualMachineIPAddressSerializer() - primary_ip4 = VirtualMachineIPAddressSerializer() - primary_ip6 = VirtualMachineIPAddressSerializer() + status = ChoiceFieldSerializer(choices=VM_STATUS_CHOICES, required=False) + cluster = NestedClusterSerializer(required=False, allow_null=True) + role = NestedDeviceRoleSerializer(required=False, allow_null=True) + tenant = NestedTenantSerializer(required=False, allow_null=True) + platform = NestedPlatformSerializer(required=False, allow_null=True) + primary_ip = VirtualMachineIPAddressSerializer(read_only=True) + primary_ip4 = VirtualMachineIPAddressSerializer(required=False, allow_null=True) + primary_ip6 = VirtualMachineIPAddressSerializer(required=False, allow_null=True) class Meta: model = VirtualMachine @@ -111,7 +104,7 @@ class VirtualMachineSerializer(CustomFieldModelSerializer): ] -class NestedVirtualMachineSerializer(serializers.ModelSerializer): +class NestedVirtualMachineSerializer(WritableNestedSerializer): url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:virtualmachine-detail') class Meta: @@ -119,22 +112,13 @@ class NestedVirtualMachineSerializer(serializers.ModelSerializer): fields = ['id', 'url', 'name'] -class WritableVirtualMachineSerializer(CustomFieldModelSerializer): - - class Meta: - model = VirtualMachine - fields = [ - 'id', 'name', 'status', 'cluster', 'role', 'tenant', 'platform', 'primary_ip4', 'primary_ip6', 'vcpus', - 'memory', 'disk', 'comments', 'custom_fields', 'created', 'last_updated', - ] - - # # VM interfaces # -class InterfaceSerializer(serializers.ModelSerializer): +class InterfaceSerializer(ValidatedModelSerializer): virtual_machine = NestedVirtualMachineSerializer() + form_factor = serializers.IntegerField(default=IFACE_FF_VIRTUAL) class Meta: model = Interface @@ -143,19 +127,9 @@ class InterfaceSerializer(serializers.ModelSerializer): ] -class NestedInterfaceSerializer(serializers.ModelSerializer): +class NestedInterfaceSerializer(WritableNestedSerializer): url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:interface-detail') class Meta: model = Interface fields = ['id', 'url', 'name'] - - -class WritableInterfaceSerializer(ValidatedModelSerializer): - form_factor = serializers.IntegerField(default=IFACE_FF_VIRTUAL) - - class Meta: - model = Interface - fields = [ - 'id', 'name', 'virtual_machine', 'form_factor', 'enabled', 'mac_address', 'mtu', 'description', - ] diff --git a/netbox/virtualization/api/views.py b/netbox/virtualization/api/views.py index 149bb3145..fae8b9232 100644 --- a/netbox/virtualization/api/views.py +++ b/netbox/virtualization/api/views.py @@ -37,7 +37,6 @@ class ClusterGroupViewSet(ModelViewSet): class ClusterViewSet(CustomFieldModelViewSet): queryset = Cluster.objects.select_related('type', 'group') serializer_class = serializers.ClusterSerializer - write_serializer_class = serializers.WritableClusterSerializer filter_class = filters.ClusterFilter @@ -48,12 +47,10 @@ class ClusterViewSet(CustomFieldModelViewSet): class VirtualMachineViewSet(CustomFieldModelViewSet): queryset = VirtualMachine.objects.all() serializer_class = serializers.VirtualMachineSerializer - write_serializer_class = serializers.WritableVirtualMachineSerializer filter_class = filters.VirtualMachineFilter class InterfaceViewSet(ModelViewSet): queryset = Interface.objects.filter(virtual_machine__isnull=False).select_related('virtual_machine') serializer_class = serializers.InterfaceSerializer - write_serializer_class = serializers.WritableInterfaceSerializer filter_class = filters.InterfaceFilter From c72d70d114419d33941407fee97c9a0606da17d6 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 5 Apr 2018 16:26:29 -0400 Subject: [PATCH 4/6] Removed nested serializers for ManyToMany relationships temporarily --- netbox/dcim/api/serializers.py | 1 - netbox/dcim/tests/test_api.py | 13 ++++--------- netbox/ipam/api/serializers.py | 1 - 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 7c5191477..249379f4f 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -553,7 +553,6 @@ class InterfaceSerializer(ValidatedModelSerializer): circuit_termination = InterfaceCircuitTerminationSerializer(read_only=True) untagged_vlan = InterfaceVLANSerializer(required=False, allow_null=True) mode = ChoiceFieldSerializer(choices=IFACE_MODE_CHOICES, required=False) - tagged_vlans = InterfaceVLANSerializer(many=True, required=False) class Meta: model = Interface diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index 6642be440..069445774 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -2402,15 +2402,10 @@ class InterfaceTest(HttpStatusMixin, APITestCase): self.assertHttpStatus(response, status.HTTP_201_CREATED) 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(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): diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index 02680bd69..a60c7321a 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -296,7 +296,6 @@ class ServiceSerializer(ValidatedModelSerializer): device = NestedDeviceSerializer(required=False, allow_null=True) virtual_machine = NestedVirtualMachineSerializer(required=False, allow_null=True) protocol = ChoiceFieldSerializer(choices=IP_PROTOCOL_CHOICES) - ipaddresses = NestedIPAddressSerializer(many=True, required=False) class Meta: model = Service From 9de1a8c36311738b4463355766ddb4cf12e0e31a Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 6 Apr 2018 12:42:25 -0400 Subject: [PATCH 5/6] Introduced SerializedPKRelatedField to represent serialized ManyToManyFields --- netbox/dcim/api/serializers.py | 12 ++++++++++-- netbox/dcim/tests/test_api.py | 22 +++++++++------------- netbox/ipam/api/serializers.py | 8 +++++++- netbox/utilities/api.py | 16 +++++++++++++++- 4 files changed, 41 insertions(+), 17 deletions(-) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 249379f4f..45689a397 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -20,7 +20,9 @@ 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, WritableNestedSerializer +from utilities.api import ( + ChoiceFieldSerializer, SerializedPKRelatedField, TimeZoneField, ValidatedModelSerializer, WritableNestedSerializer, +) from virtualization.models import Cluster @@ -551,8 +553,14 @@ class InterfaceSerializer(ValidatedModelSerializer): is_connected = serializers.SerializerMethodField(read_only=True) interface_connection = serializers.SerializerMethodField(read_only=True) circuit_termination = InterfaceCircuitTerminationSerializer(read_only=True) - untagged_vlan = InterfaceVLANSerializer(required=False, allow_null=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: model = Interface diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index 069445774..6614f8068 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -1,7 +1,6 @@ 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 @@ -2322,15 +2321,14 @@ 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 = { 'device': self.device.pk, 'name': 'Test Interface 4', 'mode': IFACE_MODE_TAGGED, + 'untagged_vlan': self.vlan3.id, 'tagged_vlans': [self.vlan1.id, self.vlan2.id], - 'untagged_vlan': self.vlan3.id } url = reverse('dcim-api:interface-list') @@ -2338,11 +2336,10 @@ class InterfaceTest(HttpStatusMixin, APITestCase): self.assertHttpStatus(response, status.HTTP_201_CREATED) 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): @@ -2370,7 +2367,6 @@ 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 = [ @@ -2378,22 +2374,22 @@ class InterfaceTest(HttpStatusMixin, APITestCase): 'device': self.device.pk, 'name': 'Test Interface 4', 'mode': IFACE_MODE_TAGGED, - 'tagged_vlans': [self.vlan1.id], 'untagged_vlan': self.vlan2.id, + 'tagged_vlans': [self.vlan1.id], }, { 'device': self.device.pk, 'name': 'Test Interface 5', 'mode': IFACE_MODE_TAGGED, - 'tagged_vlans': [self.vlan1.id], 'untagged_vlan': self.vlan2.id, + 'tagged_vlans': [self.vlan1.id], }, { 'device': self.device.pk, 'name': 'Test Interface 6', 'mode': IFACE_MODE_TAGGED, - 'tagged_vlans': [self.vlan1.id], 'untagged_vlan': self.vlan2.id, + 'tagged_vlans': [self.vlan1.id], }, ] @@ -2404,7 +2400,7 @@ class InterfaceTest(HttpStatusMixin, APITestCase): self.assertEqual(Interface.objects.count(), 6) for i in range(0, 3): self.assertEqual(response.data[i]['name'], data[i]['name']) - self.assertEqual(response.data[i]['tagged_vlans'], data[i]['tagged_vlans']) + 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): diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index a60c7321a..6fb9d3ba4 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -14,7 +14,7 @@ from ipam.constants import ( ) from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF from tenancy.api.serializers import NestedTenantSerializer -from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer, WritableNestedSerializer +from utilities.api import ChoiceFieldSerializer, SerializedPKRelatedField, ValidatedModelSerializer, WritableNestedSerializer from virtualization.api.serializers import NestedVirtualMachineSerializer @@ -296,6 +296,12 @@ class ServiceSerializer(ValidatedModelSerializer): device = NestedDeviceSerializer(required=False, allow_null=True) virtual_machine = NestedVirtualMachineSerializer(required=False, allow_null=True) protocol = ChoiceFieldSerializer(choices=IP_PROTOCOL_CHOICES) + ipaddresses = SerializedPKRelatedField( + queryset=IPAddress.objects.all(), + serializer=NestedIPAddressSerializer, + required=False, + many=True + ) class Meta: model = Service diff --git a/netbox/utilities/api.py b/netbox/utilities/api.py index 63ce23db1..40d111269 100644 --- a/netbox/utilities/api.py +++ b/netbox/utilities/api.py @@ -11,6 +11,7 @@ from django.http import Http404 from rest_framework import mixins from rest_framework.exceptions import APIException from rest_framework.permissions import BasePermission +from rest_framework.relations import PrimaryKeyRelatedField from rest_framework.response import Response from rest_framework.serializers import Field, ModelSerializer, ValidationError from rest_framework.viewsets import GenericViewSet, ViewSet @@ -82,7 +83,6 @@ class TimeZoneField(Field): """ Represent a pytz time zone. """ - def to_representation(self, obj): return obj.zone if obj else None @@ -95,6 +95,20 @@ class TimeZoneField(Field): 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 # From aeaa47e91df5d287d02bea2a33e9ee4d74b5b56b Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 6 Apr 2018 14:40:16 -0400 Subject: [PATCH 6/6] Avoid a bug in DRF v3.8.2 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 288830b74..1f8aca440 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ django-filter>=1.1.0 django-mptt>=0.9.0 django-tables2>=1.19.0 django-timezone-field>=2.0 -djangorestframework>=3.7.7 +djangorestframework>=3.7.7,<3.8.2 drf-yasg[validation]>=1.4.4 graphviz>=0.8.2 Markdown>=2.6.11