mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-08-06 05:20:12 +03:00
Merge 3a11802032
into abe14c06f7
This commit is contained in:
commit
824a347682
30
docs/api-guide/serializers.md
Normal file → Executable file
30
docs/api-guide/serializers.md
Normal file → Executable 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.
|
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
|
## 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.
|
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
27
rest_framework/serializers.py
Normal file → Executable file
|
@ -346,7 +346,16 @@ class BaseSerializer(WritableField):
|
||||||
continue
|
continue
|
||||||
field.initialize(parent=self, field_name=field_name)
|
field.initialize(parent=self, field_name=field_name)
|
||||||
key = self.get_field_key(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)
|
method = getattr(self, 'transform_%s' % field_name, None)
|
||||||
if callable(method):
|
if callable(method):
|
||||||
value = method(obj, value)
|
value = method(obj, value)
|
||||||
|
@ -622,6 +631,7 @@ class ModelSerializerOptions(SerializerOptions):
|
||||||
self.model = getattr(meta, 'model', None)
|
self.model = getattr(meta, 'model', None)
|
||||||
self.read_only_fields = getattr(meta, 'read_only_fields', ())
|
self.read_only_fields = getattr(meta, 'read_only_fields', ())
|
||||||
self.write_only_fields = getattr(meta, 'write_only_fields', ())
|
self.write_only_fields = getattr(meta, 'write_only_fields', ())
|
||||||
|
self.non_native_fields = getattr(meta, 'non_native_fields', ())
|
||||||
|
|
||||||
|
|
||||||
class ModelSerializer(Serializer):
|
class ModelSerializer(Serializer):
|
||||||
|
@ -782,7 +792,7 @@ class ModelSerializer(Serializer):
|
||||||
"Non-existant field '%s' specified in `write_only_fields` "
|
"Non-existant field '%s' specified in `write_only_fields` "
|
||||||
"on serializer '%s'." %
|
"on serializer '%s'." %
|
||||||
(field_name, self.__class__.__name__))
|
(field_name, self.__class__.__name__))
|
||||||
ret[field_name].write_only = True
|
ret[field_name].write_only = True
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
@ -897,6 +907,15 @@ class ModelSerializer(Serializer):
|
||||||
and not isinstance(field, Serializer):
|
and not isinstance(field, Serializer):
|
||||||
exclusions.remove(field_name)
|
exclusions.remove(field_name)
|
||||||
return exclusions
|
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):
|
def full_clean(self, instance):
|
||||||
"""
|
"""
|
||||||
|
@ -923,6 +942,10 @@ class ModelSerializer(Serializer):
|
||||||
nested_forward_relations = {}
|
nested_forward_relations = {}
|
||||||
meta = self.opts.model._meta
|
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
|
# Reverse fk or one-to-one relations
|
||||||
for (obj, model) in meta.get_all_related_objects_with_model():
|
for (obj, model) in meta.get_all_related_objects_with_model():
|
||||||
field_name = obj.get_accessor_name()
|
field_name = obj.get_accessor_name()
|
||||||
|
|
81
rest_framework/tests/test_non_native_fields.py
Executable file
81
rest_framework/tests/test_non_native_fields.py
Executable 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"')
|
Loading…
Reference in New Issue
Block a user