diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md old mode 100644 new mode 100755 index 7ee060af4..55d8c74c0 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -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. diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py old mode 100644 new mode 100755 index 5c726dfcd..e6cdb0dc2 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -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() diff --git a/rest_framework/tests/test_non_native_fields.py b/rest_framework/tests/test_non_native_fields.py new file mode 100755 index 000000000..bfd2b8169 --- /dev/null +++ b/rest_framework/tests/test_non_native_fields.py @@ -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"')