This commit is contained in:
Val Neekman 2014-03-25 04:07:09 +00:00
commit e0b1d68c7b
2 changed files with 207 additions and 16 deletions

View File

@ -21,7 +21,7 @@ Let's start by creating a simple object we can use for example purposes:
self.email = email
self.content = content
self.created = created or datetime.datetime.now()
comment = Comment(email='leila@example.com', content='foo bar')
We'll declare a serializer that we can use to serialize and deserialize `Comment` objects.
@ -45,7 +45,7 @@ Declaring a serializer looks very similar to declaring a form:
instance.content = attrs.get('content', instance.content)
instance.created = attrs.get('created', instance.created)
return instance
return Comment(**attrs)
return Comment(**attrs)
The first part of serializer class defines the fields that get serialized/deserialized. The `restore_object` method defines how fully fledged instances get created when deserializing data.
@ -83,8 +83,8 @@ If you need to customize the serialized value of a particular field, you can do
These methods are essentially the reverse of `validate_<fieldname>` (see *Validation* below.)
## Deserializing objects
Deserialization is similar. First we parse a stream into Python native datatypes...
Deserialization is similar. First we parse a stream into Python native datatypes...
from StringIO import StringIO
from rest_framework.parsers import JSONParser
@ -174,7 +174,7 @@ To save the deserialized objects created by a serializer, call the `.save()` met
The default behavior of the method is to simply call `.save()` on the deserialized object instance. You can override the default save behaviour by overriding the `.save_object(obj)` method on the serializer class.
The generic views provided by REST framework call the `.save()` method when updating or creating entities.
The generic views provided by REST framework call the `.save()` method when updating or creating entities.
## Dealing with nested objects
@ -288,12 +288,12 @@ By default the serializer class will use the `id` key on the incoming data to de
slug = serializers.CharField(max_length=100)
created = serializers.DateTimeField()
... # Various other fields
def get_identity(self, data):
"""
This hook is required for bulk update.
We need to override the default, to use the slug as the identity.
Note that the data has not yet been validated at this point,
so we need to deal gracefully with incorrect datatypes.
"""
@ -361,7 +361,7 @@ The `depth` option should be set to an integer value that indicates the depth of
If you want to customize the way the serialization is done (e.g. using `allow_add_remove`) you'll need to define the field yourself.
## Specifying which fields should be read-only
## Specifying which fields should be read-only
You may wish to specify multiple fields as read-only. Instead of adding each field explicitly with the `read_only=True` attribute, you may use the `read_only_fields` Meta option, like so:
@ -371,9 +371,9 @@ You may wish to specify multiple fields as read-only. Instead of adding each fi
fields = ('id', 'account_name', 'users', 'created')
read_only_fields = ('account_name',)
Model fields which have `editable=False` set, and `AutoField` fields will be set to read-only by default, and do not need to be added to the `read_only_fields` option.
Model fields which have `editable=False` set, and `AutoField` fields will be set to read-only by default, and do not need to be added to the `read_only_fields` option.
## Specifying which fields should be write-only
## Specifying which fields should be write-only
You may wish to specify multiple fields as write-only. Instead of adding each field explicitly with the `write_only=True` attribute, you may use the `write_only_fields` Meta option, like so:
@ -387,12 +387,12 @@ You may wish to specify multiple fields as write-only. Instead of adding each f
"""
Instantiate a new User instance.
"""
assert instance is None, 'Cannot update users with CreateUserSerializer'
assert instance is None, 'Cannot update users with CreateUserSerializer'
user = User(email=attrs['email'], username=attrs['username'])
user.set_password(attrs['password'])
return user
## Specifying fields explicitly
## Specifying fields explicitly
You can add extra fields to a `ModelSerializer` or override the default fields by declaring fields on the class, just as you would for a `Serializer` class.
@ -405,6 +405,54 @@ You can add extra fields to a `ModelSerializer` or override the default fields b
Extra fields can correspond to any property or callable on the model.
## Specifying non-native validation only fields
You can add extra non-native `validation only` fields to a `ModelSerializer` provided that you meet the following two conditions:
**A)** The extra `validation only` fields are removed from the `attrs` parameter prior to invoking the `restore_object()` method on the `parent` serializer class.
**B)** The extra `validation only` fields are removed from the `fields` attribute prior to invoking the `to_native()` method on the `parent` serializer class when `obj` is None.
# Example of overriding restore_object() and to_native() attributes
class CreateUserSerializer(serializers.ModelSerializer):
password_confirmation = serializers.CharField()
accept_our_terms_and_conditions = serializers.BooleanField()
custom_messages = {
'password_mismatch': 'The two password fields did not match.',
'terms_conditions': 'You must accept our terms and conditions.',
}
def validate_password_confirmation(self, attrs, source):
password_confirmation = attrs[source]
password = attrs['password']
if password_confirmation != password:
raise serializers.ValidationError(self.custom_messages['password_mismatch'])
return attrs
def validate_accept_our_terms_and_conditions(self, attrs, source):
accept_our_terms_and_conditions = attrs[source]
if not accept_our_terms_and_conditions:
raise serializers.ValidationError(self.custom_messages['terms_conditions'])
return attrs
class Meta:
model = User
fields = ('username', 'email', 'password', 'password_confirmation', 'accept_our_terms_and_conditions',)
write_only_fields = ('password',)
def restore_object(self, attrs, instance=None):
for attr in ('password_confirmation', 'accept_our_terms_and_conditions'):
attrs.pop(attr)
return super(CreateUserSerializer, self).restore_object(attrs, instance)
def to_native(self, obj):
if obj is not None:
for field in ('password_confirmation', 'accept_our_terms_and_conditions'):
self.fields.pop(field)
return super(CreateUserSerializer, self).to_native(obj)
## Relational fields
When serializing model instances, there are a number of different ways you might choose to represent relationships. The default representation for `ModelSerializer` is to use the primary keys of the related instances.
@ -514,10 +562,10 @@ For example, if you wanted to be able to set which fields should be used by a se
def __init__(self, *args, **kwargs):
# Don't pass the 'fields' arg up to the superclass
fields = kwargs.pop('fields', None)
# Instantiate the superclass normally
super(DynamicFieldsModelSerializer, self).__init__(*args, **kwargs)
if fields:
# Drop any fields that are not specified in the `fields` argument.
allowed = set(fields)
@ -540,7 +588,7 @@ This would then allow you to do the following:
## Customising the default fields
The `field_mapping` attribute is a dictionary that maps model classes to serializer classes. Overriding the attribute will let you set a different set of default serializer classes.
The `field_mapping` attribute is a dictionary that maps model classes to serializer classes. Overriding the attribute will let you set a different set of default serializer classes.
For more advanced customization than simply changing the default serializer class you can override various `get_<field_type>_field` methods. Doing so will allow you to customize the arguments that each serializer field is initialized with. Each of these methods may either return a field or serializer instance, or `None`.

View File

@ -0,0 +1,143 @@
from django.db import models
from django.test import TestCase
from django.core.urlresolvers import reverse
from rest_framework import serializers
from rest_framework import generics
from rest_framework import status
from rest_framework.compat import patterns
from rest_framework.compat import url
class ValidationOnlyFieldsExampleModel(models.Model):
email = models.EmailField(max_length=100)
password = models.CharField(max_length=100)
class ValidationOnlyFieldsExampleSerializer(serializers.ModelSerializer):
password_confirmation = serializers.CharField()
accept_our_terms_and_conditions = serializers.BooleanField()
custom_messages = {
'password_mismatch': 'Password confirmation failed.',
'terms_condition': 'You must accept our terms and conditions.',
}
def validate_password_confirmation(self, attrs, source):
password_confirmation = attrs[source]
password = attrs['password']
if password_confirmation != password:
raise serializers.ValidationError(self.custom_messages['password_mismatch'])
return attrs
def validate_accept_our_terms_and_conditions(self, attrs, source):
accept_our_terms_and_conditions = attrs[source]
if not accept_our_terms_and_conditions:
raise serializers.ValidationError(self.custom_messages['terms_condition'])
return attrs
class Meta:
model = ValidationOnlyFieldsExampleModel
fields = ('email', 'password', 'password_confirmation', 'accept_our_terms_and_conditions',)
write_only_fields = ('password',)
def restore_object(self, attrs, instance=None):
for attr in ('password_confirmation', 'accept_our_terms_and_conditions'):
attrs.pop(attr)
return super(ValidationOnlyFieldsExampleSerializer, self).restore_object(attrs, instance)
def to_native(self, obj):
if obj is not None:
for field in ('password_confirmation', 'accept_our_terms_and_conditions'):
self.fields.pop(field)
return super(ValidationOnlyFieldsExampleSerializer, self).to_native(obj)
class ValidationOnlyFieldsExampleView(generics.ListCreateAPIView):
"""
ValidationOnlyFieldsExampleView
"""
model = ValidationOnlyFieldsExampleModel
serializer_class = ValidationOnlyFieldsExampleSerializer
validation_only_fields_test_view = ValidationOnlyFieldsExampleView.as_view()
urlpatterns = patterns('',
url(
r'^validation/only/fields/test$',
validation_only_fields_test_view,
name='validation_only_fields_test'
),
)
class ValidationOnlyFieldsTests(TestCase):
urls = 'rest_framework.tests.test_validation_only_fields'
def test_validation_fields_only_not_included_in_data(self):
data = {
'email': 'foo@example.com',
'password': '1234',
'password_confirmation': '1234',
'accept_our_terms_and_conditions': True,
}
serializer = ValidationOnlyFieldsExampleSerializer(data=data)
self.assertTrue(serializer.is_valid())
self.assertTrue(isinstance(serializer.object, ValidationOnlyFieldsExampleModel))
self.assertEquals(serializer.object.email, data['email'])
self.assertEquals(serializer.object.password, data['password'])
self.assertFalse(hasattr(serializer.object, 'password_confirmation'))
self.assertEquals(serializer.data.get('email'), data['email'])
self.assertEquals(serializer.data.get('password'), None)
self.assertEquals(serializer.data.get('password_confirmation'), None)
def test_validation_only_raises_proper_validation_error(self):
data = {
'email': 'foo@example.com',
'password': '1234',
'password_confirmation': 'ABCD', # wrong password
'accept_our_terms_and_conditions': True,
}
serializer = ValidationOnlyFieldsExampleSerializer(data=data)
self.assertFalse(serializer.is_valid())
self.assertEquals(len(serializer.errors), 1)
self.assertEquals(serializer.errors['password_confirmation'][0],
ValidationOnlyFieldsExampleSerializer.custom_messages['password_mismatch'])
data = {
'email': 'foo@example.com',
'password': '1234',
'password_confirmation': 'ABCD', # wrong password
'accept_our_terms_and_conditions': False,
}
serializer = ValidationOnlyFieldsExampleSerializer(data=data)
self.assertFalse(serializer.is_valid())
self.assertEquals(len(serializer.errors), 2)
self.assertEquals(serializer.errors['password_confirmation'][0],
ValidationOnlyFieldsExampleSerializer.custom_messages['password_mismatch'])
self.assertEquals(serializer.errors['accept_our_terms_and_conditions'][0],
ValidationOnlyFieldsExampleSerializer.custom_messages['terms_condition'])
def test_validation_only_fields_included_in_browser_api_forms(self):
url = reverse('validation_only_fields_test')
resp = self.client.get(url, HTTP_ACCEPT='text/html')
self.assertContains(resp, 'for="email"')
self.assertContains(resp, 'for="password"')
self.assertContains(resp, 'for="password_confirmation"')
self.assertContains(resp, 'for="accept_our_terms_and_conditions"')
def test_validation_only_fields_not_included_in_reponse(self):
url = reverse('validation_only_fields_test')
data = {
'email': 'foo@example.com',
'password': '1234',
'password_confirmation': '1234',
'accept_our_terms_and_conditions': True,
}
resp = self.client.post(url, data=data)
self.assertEqual(resp.status_code, status.HTTP_201_CREATED)
self.assertEquals(resp.data.get('email'), data['email'])
self.assertEquals(resp.data.get('password'), None)
self.assertEquals(resp.data.get('password_confirmation'), None)
self.assertEquals(resp.data.get('accept_our_terms_and_conditions'), None)