This commit is contained in:
Federico Capoano 2014-03-20 13:36:20 +00:00
commit 824a347682
3 changed files with 136 additions and 2 deletions

30
docs/api-guide/serializers.md Normal file → Executable file
View File

@ -405,6 +405,36 @@ 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 (extra) fields
You can add extra fields to a `ModelSerializer` which do not correspond to any particular property on the model on which you can do custom processing with the `non_native_fields` Meta option.
A common example of this case is to be able to specify a `password_confirmation` field:
class UserSerializer(serializers.ModelSerializer):
password_confirmation = serializers.CharField()
def validate_password_confirmation(self, attrs, source):
password_confirmation = attrs[source]
password = attrs['password']
if password_confirmation != password:
raise serializers.ValidationError('Password confirmation mismatch')
attrs.pop(source)
return attrs
class Meta:
model = User
fields = ('email', 'password', 'password_confirmation',)
write_only_fields = ('password',)
non_native_fields = ('password_confirmation',)
In this case the `password_confirmation` field will be shown in the browsable API form but if a POST request is done
it won't be automatically saved into the database but will be available for custom processing, which in this case is just a simple check to compare the two password fields.
The peculiarity of `non_native_fields` is that the fields specified in that list will be visible in the browsable API form and in the OPTIONS http response of the view.
## 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.

27
rest_framework/serializers.py Normal file → Executable file
View File

@ -346,7 +346,16 @@ class BaseSerializer(WritableField):
continue
field.initialize(parent=self, field_name=field_name)
key = self.get_field_key(field_name)
value = field.field_to_native(obj, field_name)
try:
value = field.field_to_native(obj, field_name)
except AttributeError as e:
# non_native_fields check is done only in ModelSerializer
if field_name in getattr(self.opts, 'non_native_fields', []):
continue
else:
raise e
method = getattr(self, 'transform_%s' % field_name, None)
if callable(method):
value = method(obj, value)
@ -622,6 +631,7 @@ class ModelSerializerOptions(SerializerOptions):
self.model = getattr(meta, 'model', None)
self.read_only_fields = getattr(meta, 'read_only_fields', ())
self.write_only_fields = getattr(meta, 'write_only_fields', ())
self.non_native_fields = getattr(meta, 'non_native_fields', ())
class ModelSerializer(Serializer):
@ -782,7 +792,7 @@ class ModelSerializer(Serializer):
"Non-existant field '%s' specified in `write_only_fields` "
"on serializer '%s'." %
(field_name, self.__class__.__name__))
ret[field_name].write_only = True
ret[field_name].write_only = True
return ret
@ -897,6 +907,15 @@ class ModelSerializer(Serializer):
and not isinstance(field, Serializer):
exclusions.remove(field_name)
return exclusions
def field_to_native(self, obj, field_name):
"""
Add support to non_native_fields
"""
if field_name in self.opts.non_native_fields:
return None
return super(ModelSerializer, self).field_to_native(obj, field_name)
def full_clean(self, instance):
"""
@ -923,6 +942,10 @@ class ModelSerializer(Serializer):
nested_forward_relations = {}
meta = self.opts.model._meta
for field_name in self.opts.non_native_fields:
if field_name in attrs:
attrs.pop(field_name)
# Reverse fk or one-to-one relations
for (obj, model) in meta.get_all_related_objects_with_model():
field_name = obj.get_accessor_name()

View File

@ -0,0 +1,81 @@
from django.db import models
from django.test import TestCase
from rest_framework import serializers
from rest_framework import generics
from rest_framework.compat import patterns, url
class NonNativeExampleModel(models.Model):
email = models.EmailField(max_length=100)
password = models.CharField(max_length=100)
class NonNativeExampleSerializer(serializers.ModelSerializer):
password_confirmation = serializers.CharField()
def validate_password_confirmation(self, attrs, source):
password_confirmation = attrs[source]
password = attrs['password']
if password_confirmation != password:
raise serializers.ValidationError('Password confirmation mismatch')
attrs.pop(source)
return attrs
class Meta:
model = NonNativeExampleModel
fields = ('email', 'password', 'password_confirmation',)
write_only_fields = ('password',)
non_native_fields = ('password_confirmation',)
class NonNativeExampleView(generics.ListCreateAPIView):
"""
NonNativeExampleView
"""
model = NonNativeExampleModel
serializer_class = NonNativeExampleSerializer
example_view = NonNativeExampleView.as_view()
urlpatterns = patterns('',
url(r'^example$', example_view),
)
class NonNativeFieldTests(TestCase):
urls = 'rest_framework.tests.test_non_native_fields'
def test_non_native_fields(self):
data = {
'email': 'foo@example.com',
'password': '123',
'password_confirmation': '123',
}
serializer = NonNativeExampleSerializer(data=data)
self.assertTrue(serializer.is_valid())
self.assertTrue(isinstance(serializer.object, NonNativeExampleModel))
self.assertEquals(serializer.object.email, data['email'])
self.assertEquals(serializer.object.password, data['password'])
self.assertEquals(serializer.data, {'email': 'foo@example.com'})
def test_non_native_fields_validation_error(self):
data = {
'email': 'foo@example.com',
'password': '123',
'password_confirmation': 'abc',
}
serializer = NonNativeExampleSerializer(data=data)
self.assertFalse(serializer.is_valid())
self.assertEquals(len(serializer.errors), 1)
self.assertEquals(serializer.errors['password_confirmation'],
['Password confirmation mismatch'])
def test_non_native_fields_displayed_in_html_version(self):
"""
Ensure password_confirmation field is shown in the browsable API form
"""
response = self.client.get('/example', HTTP_ACCEPT='text/html')
self.assertContains(response, 'for="password"')
self.assertContains(response, 'for="password_confirmation"')