Fixes #1421: Improved model validation logic for API serializers

This commit is contained in:
Jeremy Stretch 2017-08-15 13:54:04 -04:00
parent 04c300b8e2
commit c394985b1b
8 changed files with 53 additions and 58 deletions

View File

@ -6,7 +6,7 @@ from circuits.models import Provider, Circuit, CircuitTermination, CircuitType
from dcim.api.serializers import NestedSiteSerializer, InterfaceSerializer from dcim.api.serializers import NestedSiteSerializer, InterfaceSerializer
from extras.api.customfields import CustomFieldModelSerializer from extras.api.customfields import CustomFieldModelSerializer
from tenancy.api.serializers import NestedTenantSerializer from tenancy.api.serializers import NestedTenantSerializer
from utilities.api import ModelValidationMixin from utilities.api import ValidatedModelSerializer
# #
@ -45,7 +45,7 @@ class WritableProviderSerializer(CustomFieldModelSerializer):
# Circuit types # Circuit types
# #
class CircuitTypeSerializer(ModelValidationMixin, serializers.ModelSerializer): class CircuitTypeSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = CircuitType model = CircuitType
@ -111,7 +111,7 @@ class CircuitTerminationSerializer(serializers.ModelSerializer):
] ]
class WritableCircuitTerminationSerializer(ModelValidationMixin, serializers.ModelSerializer): class WritableCircuitTerminationSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = CircuitTermination model = CircuitTermination

View File

@ -15,7 +15,7 @@ from dcim.models import (
) )
from extras.api.customfields import CustomFieldModelSerializer from extras.api.customfields import CustomFieldModelSerializer
from tenancy.api.serializers import NestedTenantSerializer from tenancy.api.serializers import NestedTenantSerializer
from utilities.api import ChoiceFieldSerializer, ModelValidationMixin from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer
# #
@ -38,7 +38,7 @@ class RegionSerializer(serializers.ModelSerializer):
fields = ['id', 'name', 'slug', 'parent'] fields = ['id', 'name', 'slug', 'parent']
class WritableRegionSerializer(ModelValidationMixin, serializers.ModelSerializer): class WritableRegionSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = Region model = Region
@ -100,7 +100,7 @@ class NestedRackGroupSerializer(serializers.ModelSerializer):
fields = ['id', 'url', 'name', 'slug'] fields = ['id', 'url', 'name', 'slug']
class WritableRackGroupSerializer(ModelValidationMixin, serializers.ModelSerializer): class WritableRackGroupSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = RackGroup model = RackGroup
@ -111,7 +111,7 @@ class WritableRackGroupSerializer(ModelValidationMixin, serializers.ModelSeriali
# Rack roles # Rack roles
# #
class RackRoleSerializer(ModelValidationMixin, serializers.ModelSerializer): class RackRoleSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = RackRole model = RackRole
@ -216,7 +216,7 @@ class RackReservationSerializer(serializers.ModelSerializer):
fields = ['id', 'rack', 'units', 'created', 'user', 'description'] fields = ['id', 'rack', 'units', 'created', 'user', 'description']
class WritableRackReservationSerializer(ModelValidationMixin, serializers.ModelSerializer): class WritableRackReservationSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = RackReservation model = RackReservation
@ -227,7 +227,7 @@ class WritableRackReservationSerializer(ModelValidationMixin, serializers.ModelS
# Manufacturers # Manufacturers
# #
class ManufacturerSerializer(ModelValidationMixin, serializers.ModelSerializer): class ManufacturerSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = Manufacturer model = Manufacturer
@ -292,7 +292,7 @@ class ConsolePortTemplateSerializer(serializers.ModelSerializer):
fields = ['id', 'device_type', 'name'] fields = ['id', 'device_type', 'name']
class WritableConsolePortTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer): class WritableConsolePortTemplateSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = ConsolePortTemplate model = ConsolePortTemplate
@ -311,7 +311,7 @@ class ConsoleServerPortTemplateSerializer(serializers.ModelSerializer):
fields = ['id', 'device_type', 'name'] fields = ['id', 'device_type', 'name']
class WritableConsoleServerPortTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer): class WritableConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = ConsoleServerPortTemplate model = ConsoleServerPortTemplate
@ -330,7 +330,7 @@ class PowerPortTemplateSerializer(serializers.ModelSerializer):
fields = ['id', 'device_type', 'name'] fields = ['id', 'device_type', 'name']
class WritablePowerPortTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer): class WritablePowerPortTemplateSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = PowerPortTemplate model = PowerPortTemplate
@ -349,7 +349,7 @@ class PowerOutletTemplateSerializer(serializers.ModelSerializer):
fields = ['id', 'device_type', 'name'] fields = ['id', 'device_type', 'name']
class WritablePowerOutletTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer): class WritablePowerOutletTemplateSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = PowerOutletTemplate model = PowerOutletTemplate
@ -369,7 +369,7 @@ class InterfaceTemplateSerializer(serializers.ModelSerializer):
fields = ['id', 'device_type', 'name', 'form_factor', 'mgmt_only'] fields = ['id', 'device_type', 'name', 'form_factor', 'mgmt_only']
class WritableInterfaceTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer): class WritableInterfaceTemplateSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = InterfaceTemplate model = InterfaceTemplate
@ -388,7 +388,7 @@ class DeviceBayTemplateSerializer(serializers.ModelSerializer):
fields = ['id', 'device_type', 'name'] fields = ['id', 'device_type', 'name']
class WritableDeviceBayTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer): class WritableDeviceBayTemplateSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = DeviceBayTemplate model = DeviceBayTemplate
@ -399,7 +399,7 @@ class WritableDeviceBayTemplateSerializer(ModelValidationMixin, serializers.Mode
# Device roles # Device roles
# #
class DeviceRoleSerializer(ModelValidationMixin, serializers.ModelSerializer): class DeviceRoleSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = DeviceRole model = DeviceRole
@ -418,7 +418,7 @@ class NestedDeviceRoleSerializer(serializers.ModelSerializer):
# Platforms # Platforms
# #
class PlatformSerializer(ModelValidationMixin, serializers.ModelSerializer): class PlatformSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = Platform model = Platform
@ -516,7 +516,7 @@ class ConsoleServerPortSerializer(serializers.ModelSerializer):
read_only_fields = ['connected_console'] read_only_fields = ['connected_console']
class WritableConsoleServerPortSerializer(ModelValidationMixin, serializers.ModelSerializer): class WritableConsoleServerPortSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = ConsoleServerPort model = ConsoleServerPort
@ -536,7 +536,7 @@ class ConsolePortSerializer(serializers.ModelSerializer):
fields = ['id', 'device', 'name', 'cs_port', 'connection_status'] fields = ['id', 'device', 'name', 'cs_port', 'connection_status']
class WritableConsolePortSerializer(ModelValidationMixin, serializers.ModelSerializer): class WritableConsolePortSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = ConsolePort model = ConsolePort
@ -556,7 +556,7 @@ class PowerOutletSerializer(serializers.ModelSerializer):
read_only_fields = ['connected_port'] read_only_fields = ['connected_port']
class WritablePowerOutletSerializer(ModelValidationMixin, serializers.ModelSerializer): class WritablePowerOutletSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = PowerOutlet model = PowerOutlet
@ -576,7 +576,7 @@ class PowerPortSerializer(serializers.ModelSerializer):
fields = ['id', 'device', 'name', 'power_outlet', 'connection_status'] fields = ['id', 'device', 'name', 'power_outlet', 'connection_status']
class WritablePowerPortSerializer(ModelValidationMixin, serializers.ModelSerializer): class WritablePowerPortSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = PowerPort model = PowerPort
@ -664,7 +664,7 @@ class PeerInterfaceSerializer(serializers.ModelSerializer):
] ]
class WritableInterfaceSerializer(ModelValidationMixin, serializers.ModelSerializer): class WritableInterfaceSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = Interface model = Interface
@ -694,7 +694,7 @@ class NestedDeviceBaySerializer(serializers.ModelSerializer):
fields = ['id', 'url', 'name'] fields = ['id', 'url', 'name']
class WritableDeviceBaySerializer(ModelValidationMixin, serializers.ModelSerializer): class WritableDeviceBaySerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = DeviceBay model = DeviceBay
@ -717,7 +717,7 @@ class InventoryItemSerializer(serializers.ModelSerializer):
] ]
class WritableInventoryItemSerializer(ModelValidationMixin, serializers.ModelSerializer): class WritableInventoryItemSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = InventoryItem model = InventoryItem
@ -749,7 +749,7 @@ class NestedInterfaceConnectionSerializer(serializers.ModelSerializer):
fields = ['id', 'url', 'connection_status'] fields = ['id', 'url', 'connection_status']
class WritableInterfaceConnectionSerializer(ModelValidationMixin, serializers.ModelSerializer): class WritableInterfaceConnectionSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = InterfaceConnection model = InterfaceConnection

View File

@ -10,6 +10,7 @@ from django.db import transaction
from extras.models import ( from extras.models import (
CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_SELECT, CustomField, CustomFieldChoice, CustomFieldValue, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_SELECT, CustomField, CustomFieldChoice, CustomFieldValue,
) )
from utilities.api import ValidatedModelSerializer
# #
@ -68,7 +69,7 @@ class CustomFieldsSerializer(serializers.BaseSerializer):
return data return data
class CustomFieldModelSerializer(serializers.ModelSerializer): class CustomFieldModelSerializer(ValidatedModelSerializer):
""" """
Extends ModelSerializer to render any CustomFields and their values associated with an object. Extends ModelSerializer to render any CustomFields and their values associated with an object.
""" """
@ -111,16 +112,6 @@ class CustomFieldModelSerializer(serializers.ModelSerializer):
defaults={'serialized_value': custom_field.serialize_value(value)}, defaults={'serialized_value': custom_field.serialize_value(value)},
) )
def validate(self, data):
"""
Enforce model validation (see utilities.api.ModelValidationMixin)
"""
model_data = data.copy()
model_data.pop('custom_fields', None)
instance = self.Meta.model(**model_data)
instance.clean()
return data
def create(self, validated_data): def create(self, validated_data):
custom_fields = validated_data.pop('custom_fields', None) custom_fields = validated_data.pop('custom_fields', None)

View File

@ -10,7 +10,7 @@ from extras.models import (
ACTION_CHOICES, ExportTemplate, Graph, GRAPH_TYPE_CHOICES, ImageAttachment, TopologyMap, UserAction, ACTION_CHOICES, ExportTemplate, Graph, GRAPH_TYPE_CHOICES, ImageAttachment, TopologyMap, UserAction,
) )
from users.api.serializers import NestedUserSerializer from users.api.serializers import NestedUserSerializer
from utilities.api import ChoiceFieldSerializer, ContentTypeFieldSerializer, ModelValidationMixin from utilities.api import ChoiceFieldSerializer, ContentTypeFieldSerializer, ValidatedModelSerializer
# #
@ -104,7 +104,7 @@ class ImageAttachmentSerializer(serializers.ModelSerializer):
return serializer(obj.parent, context={'request': self.context['request']}).data return serializer(obj.parent, context={'request': self.context['request']}).data
class WritableImageAttachmentSerializer(ModelValidationMixin, serializers.ModelSerializer): class WritableImageAttachmentSerializer(ValidatedModelSerializer):
content_type = ContentTypeFieldSerializer() content_type = ContentTypeFieldSerializer()
class Meta: class Meta:

View File

@ -11,7 +11,7 @@ from ipam.models import (
PREFIX_STATUS_CHOICES, RIR, Role, Service, VLAN, VLAN_STATUS_CHOICES, VLANGroup, VRF, PREFIX_STATUS_CHOICES, RIR, Role, Service, VLAN, VLAN_STATUS_CHOICES, VLANGroup, VRF,
) )
from tenancy.api.serializers import NestedTenantSerializer from tenancy.api.serializers import NestedTenantSerializer
from utilities.api import ChoiceFieldSerializer, ModelValidationMixin from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer
# #
@ -45,7 +45,7 @@ class WritableVRFSerializer(CustomFieldModelSerializer):
# Roles # Roles
# #
class RoleSerializer(ModelValidationMixin, serializers.ModelSerializer): class RoleSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = Role model = Role
@ -64,7 +64,7 @@ class NestedRoleSerializer(serializers.ModelSerializer):
# RIRs # RIRs
# #
class RIRSerializer(ModelValidationMixin, serializers.ModelSerializer): class RIRSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = RIR model = RIR
@ -303,7 +303,7 @@ class ServiceSerializer(serializers.ModelSerializer):
fields = ['id', 'device', 'name', 'port', 'protocol', 'ipaddresses', 'description'] fields = ['id', 'device', 'name', 'port', 'protocol', 'ipaddresses', 'description']
# TODO: Figure out how to use ModelValidationMixin with ManyToManyFields. Calling clean() yields a ValueError. # TODO: Figure out how to use model validation with ManyToManyFields. Calling clean() yields a ValueError.
class WritableServiceSerializer(serializers.ModelSerializer): class WritableServiceSerializer(serializers.ModelSerializer):
class Meta: class Meta:

View File

@ -5,14 +5,14 @@ from rest_framework.validators import UniqueTogetherValidator
from dcim.api.serializers import NestedDeviceSerializer from dcim.api.serializers import NestedDeviceSerializer
from secrets.models import Secret, SecretRole from secrets.models import Secret, SecretRole
from utilities.api import ModelValidationMixin from utilities.api import ValidatedModelSerializer
# #
# SecretRoles # SecretRoles
# #
class SecretRoleSerializer(ModelValidationMixin, serializers.ModelSerializer): class SecretRoleSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = SecretRole model = SecretRole

View File

@ -4,14 +4,14 @@ from rest_framework import serializers
from extras.api.customfields import CustomFieldModelSerializer from extras.api.customfields import CustomFieldModelSerializer
from tenancy.models import Tenant, TenantGroup from tenancy.models import Tenant, TenantGroup
from utilities.api import ModelValidationMixin from utilities.api import ValidatedModelSerializer
# #
# Tenant groups # Tenant groups
# #
class TenantGroupSerializer(ModelValidationMixin, serializers.ModelSerializer): class TenantGroupSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = TenantGroup model = TenantGroup

View File

@ -8,7 +8,7 @@ from rest_framework.compat import is_authenticated
from rest_framework.exceptions import APIException from rest_framework.exceptions import APIException
from rest_framework.pagination import LimitOffsetPagination from rest_framework.pagination import LimitOffsetPagination
from rest_framework.permissions import BasePermission, DjangoModelPermissions, SAFE_METHODS from rest_framework.permissions import BasePermission, DjangoModelPermissions, SAFE_METHODS
from rest_framework.serializers import Field, ValidationError from rest_framework.serializers import Field, ModelSerializer, ValidationError
from users.models import Token from users.models import Token
@ -80,6 +80,21 @@ class IsAuthenticatedOrLoginNotRequired(BasePermission):
# Serializers # Serializers
# #
class ValidatedModelSerializer(ModelSerializer):
"""
Extends the built-in ModelSerializer to enforce calling clean() on the associated model during validation.
"""
def validate(self, attrs):
if self.instance is None:
instance = self.Meta.model(**attrs)
else:
instance = self.instance
for k, v in attrs.items():
setattr(instance, k, v)
instance.clean()
return attrs
class ChoiceFieldSerializer(Field): class ChoiceFieldSerializer(Field):
""" """
Represent a ChoiceField as {'value': <DB value>, 'label': <string>}. Represent a ChoiceField as {'value': <DB value>, 'label': <string>}.
@ -121,17 +136,6 @@ class ContentTypeFieldSerializer(Field):
# Mixins # Mixins
# #
class ModelValidationMixin(object):
"""
Enforce a model's validation through clean() when validating serializer data. This is necessary to ensure we're
employing the same validation logic via both forms and the API.
"""
def validate(self, attrs):
instance = self.Meta.model(**attrs)
instance.clean()
return attrs
class WritableSerializerMixin(object): class WritableSerializerMixin(object):
""" """
Allow for the use of an alternate, writable serializer class for write operations (e.g. POST, PUT). Allow for the use of an alternate, writable serializer class for write operations (e.g. POST, PUT).