mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Fixes #1421: Improved model validation logic for API serializers
This commit is contained in:
@ -6,7 +6,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 ModelValidationMixin
|
||||
from utilities.api import ValidatedModelSerializer
|
||||
|
||||
|
||||
#
|
||||
@ -45,7 +45,7 @@ class WritableProviderSerializer(CustomFieldModelSerializer):
|
||||
# Circuit types
|
||||
#
|
||||
|
||||
class CircuitTypeSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
class CircuitTypeSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
@ -111,7 +111,7 @@ class CircuitTerminationSerializer(serializers.ModelSerializer):
|
||||
]
|
||||
|
||||
|
||||
class WritableCircuitTerminationSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
class WritableCircuitTerminationSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
|
@ -15,7 +15,7 @@ from dcim.models import (
|
||||
)
|
||||
from extras.api.customfields import CustomFieldModelSerializer
|
||||
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']
|
||||
|
||||
|
||||
class WritableRegionSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
class WritableRegionSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Region
|
||||
@ -100,7 +100,7 @@ class NestedRackGroupSerializer(serializers.ModelSerializer):
|
||||
fields = ['id', 'url', 'name', 'slug']
|
||||
|
||||
|
||||
class WritableRackGroupSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
class WritableRackGroupSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = RackGroup
|
||||
@ -111,7 +111,7 @@ class WritableRackGroupSerializer(ModelValidationMixin, serializers.ModelSeriali
|
||||
# Rack roles
|
||||
#
|
||||
|
||||
class RackRoleSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
class RackRoleSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = RackRole
|
||||
@ -216,7 +216,7 @@ class RackReservationSerializer(serializers.ModelSerializer):
|
||||
fields = ['id', 'rack', 'units', 'created', 'user', 'description']
|
||||
|
||||
|
||||
class WritableRackReservationSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
class WritableRackReservationSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = RackReservation
|
||||
@ -227,7 +227,7 @@ class WritableRackReservationSerializer(ModelValidationMixin, serializers.ModelS
|
||||
# Manufacturers
|
||||
#
|
||||
|
||||
class ManufacturerSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
class ManufacturerSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Manufacturer
|
||||
@ -292,7 +292,7 @@ class ConsolePortTemplateSerializer(serializers.ModelSerializer):
|
||||
fields = ['id', 'device_type', 'name']
|
||||
|
||||
|
||||
class WritableConsolePortTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
class WritableConsolePortTemplateSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = ConsolePortTemplate
|
||||
@ -311,7 +311,7 @@ class ConsoleServerPortTemplateSerializer(serializers.ModelSerializer):
|
||||
fields = ['id', 'device_type', 'name']
|
||||
|
||||
|
||||
class WritableConsoleServerPortTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
class WritableConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPortTemplate
|
||||
@ -330,7 +330,7 @@ class PowerPortTemplateSerializer(serializers.ModelSerializer):
|
||||
fields = ['id', 'device_type', 'name']
|
||||
|
||||
|
||||
class WritablePowerPortTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
class WritablePowerPortTemplateSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = PowerPortTemplate
|
||||
@ -349,7 +349,7 @@ class PowerOutletTemplateSerializer(serializers.ModelSerializer):
|
||||
fields = ['id', 'device_type', 'name']
|
||||
|
||||
|
||||
class WritablePowerOutletTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
class WritablePowerOutletTemplateSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = PowerOutletTemplate
|
||||
@ -369,7 +369,7 @@ class InterfaceTemplateSerializer(serializers.ModelSerializer):
|
||||
fields = ['id', 'device_type', 'name', 'form_factor', 'mgmt_only']
|
||||
|
||||
|
||||
class WritableInterfaceTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
class WritableInterfaceTemplateSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = InterfaceTemplate
|
||||
@ -388,7 +388,7 @@ class DeviceBayTemplateSerializer(serializers.ModelSerializer):
|
||||
fields = ['id', 'device_type', 'name']
|
||||
|
||||
|
||||
class WritableDeviceBayTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
class WritableDeviceBayTemplateSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = DeviceBayTemplate
|
||||
@ -399,7 +399,7 @@ class WritableDeviceBayTemplateSerializer(ModelValidationMixin, serializers.Mode
|
||||
# Device roles
|
||||
#
|
||||
|
||||
class DeviceRoleSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
class DeviceRoleSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = DeviceRole
|
||||
@ -418,7 +418,7 @@ class NestedDeviceRoleSerializer(serializers.ModelSerializer):
|
||||
# Platforms
|
||||
#
|
||||
|
||||
class PlatformSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
class PlatformSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Platform
|
||||
@ -516,7 +516,7 @@ class ConsoleServerPortSerializer(serializers.ModelSerializer):
|
||||
read_only_fields = ['connected_console']
|
||||
|
||||
|
||||
class WritableConsoleServerPortSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
class WritableConsoleServerPortSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPort
|
||||
@ -536,7 +536,7 @@ class ConsolePortSerializer(serializers.ModelSerializer):
|
||||
fields = ['id', 'device', 'name', 'cs_port', 'connection_status']
|
||||
|
||||
|
||||
class WritableConsolePortSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
class WritableConsolePortSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = ConsolePort
|
||||
@ -556,7 +556,7 @@ class PowerOutletSerializer(serializers.ModelSerializer):
|
||||
read_only_fields = ['connected_port']
|
||||
|
||||
|
||||
class WritablePowerOutletSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
class WritablePowerOutletSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = PowerOutlet
|
||||
@ -576,7 +576,7 @@ class PowerPortSerializer(serializers.ModelSerializer):
|
||||
fields = ['id', 'device', 'name', 'power_outlet', 'connection_status']
|
||||
|
||||
|
||||
class WritablePowerPortSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
class WritablePowerPortSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = PowerPort
|
||||
@ -664,7 +664,7 @@ class PeerInterfaceSerializer(serializers.ModelSerializer):
|
||||
]
|
||||
|
||||
|
||||
class WritableInterfaceSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
class WritableInterfaceSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
@ -694,7 +694,7 @@ class NestedDeviceBaySerializer(serializers.ModelSerializer):
|
||||
fields = ['id', 'url', 'name']
|
||||
|
||||
|
||||
class WritableDeviceBaySerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
class WritableDeviceBaySerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = DeviceBay
|
||||
@ -717,7 +717,7 @@ class InventoryItemSerializer(serializers.ModelSerializer):
|
||||
]
|
||||
|
||||
|
||||
class WritableInventoryItemSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
class WritableInventoryItemSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = InventoryItem
|
||||
@ -749,7 +749,7 @@ class NestedInterfaceConnectionSerializer(serializers.ModelSerializer):
|
||||
fields = ['id', 'url', 'connection_status']
|
||||
|
||||
|
||||
class WritableInterfaceConnectionSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
class WritableInterfaceConnectionSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = InterfaceConnection
|
||||
|
@ -10,6 +10,7 @@ from django.db import transaction
|
||||
from extras.models import (
|
||||
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
|
||||
|
||||
|
||||
class CustomFieldModelSerializer(serializers.ModelSerializer):
|
||||
class CustomFieldModelSerializer(ValidatedModelSerializer):
|
||||
"""
|
||||
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)},
|
||||
)
|
||||
|
||||
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):
|
||||
|
||||
custom_fields = validated_data.pop('custom_fields', None)
|
||||
|
@ -10,7 +10,7 @@ from extras.models import (
|
||||
ACTION_CHOICES, ExportTemplate, Graph, GRAPH_TYPE_CHOICES, ImageAttachment, TopologyMap, UserAction,
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
class WritableImageAttachmentSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
class WritableImageAttachmentSerializer(ValidatedModelSerializer):
|
||||
content_type = ContentTypeFieldSerializer()
|
||||
|
||||
class Meta:
|
||||
|
@ -11,7 +11,7 @@ from ipam.models import (
|
||||
PREFIX_STATUS_CHOICES, RIR, Role, Service, VLAN, VLAN_STATUS_CHOICES, VLANGroup, VRF,
|
||||
)
|
||||
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
|
||||
#
|
||||
|
||||
class RoleSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
class RoleSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Role
|
||||
@ -64,7 +64,7 @@ class NestedRoleSerializer(serializers.ModelSerializer):
|
||||
# RIRs
|
||||
#
|
||||
|
||||
class RIRSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
class RIRSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = RIR
|
||||
@ -303,7 +303,7 @@ class ServiceSerializer(serializers.ModelSerializer):
|
||||
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 Meta:
|
||||
|
@ -5,14 +5,14 @@ from rest_framework.validators import UniqueTogetherValidator
|
||||
|
||||
from dcim.api.serializers import NestedDeviceSerializer
|
||||
from secrets.models import Secret, SecretRole
|
||||
from utilities.api import ModelValidationMixin
|
||||
from utilities.api import ValidatedModelSerializer
|
||||
|
||||
|
||||
#
|
||||
# SecretRoles
|
||||
#
|
||||
|
||||
class SecretRoleSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
class SecretRoleSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = SecretRole
|
||||
|
@ -4,14 +4,14 @@ from rest_framework import serializers
|
||||
|
||||
from extras.api.customfields import CustomFieldModelSerializer
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.api import ModelValidationMixin
|
||||
from utilities.api import ValidatedModelSerializer
|
||||
|
||||
|
||||
#
|
||||
# Tenant groups
|
||||
#
|
||||
|
||||
class TenantGroupSerializer(ModelValidationMixin, serializers.ModelSerializer):
|
||||
class TenantGroupSerializer(ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = TenantGroup
|
||||
|
@ -8,7 +8,7 @@ from rest_framework.compat import is_authenticated
|
||||
from rest_framework.exceptions import APIException
|
||||
from rest_framework.pagination import LimitOffsetPagination
|
||||
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
|
||||
|
||||
@ -80,6 +80,21 @@ class IsAuthenticatedOrLoginNotRequired(BasePermission):
|
||||
# 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):
|
||||
"""
|
||||
Represent a ChoiceField as {'value': <DB value>, 'label': <string>}.
|
||||
@ -121,17 +136,6 @@ class ContentTypeFieldSerializer(Field):
|
||||
# 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):
|
||||
"""
|
||||
Allow for the use of an alternate, writable serializer class for write operations (e.g. POST, PUT).
|
||||
|
Reference in New Issue
Block a user