From 082af96d9b4b1efe24e79d170bf89ae06cad7354 Mon Sep 17 00:00:00 2001 From: Val Neekman Date: Sat, 22 Mar 2014 20:38:34 -0400 Subject: [PATCH 1/4] added test case for validation only fields --- .../tests/test_validation_only_fields.py | 149 ++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 rest_framework/tests/test_validation_only_fields.py diff --git a/rest_framework/tests/test_validation_only_fields.py b/rest_framework/tests/test_validation_only_fields.py new file mode 100644 index 000000000..ca07260df --- /dev/null +++ b/rest_framework/tests/test_validation_only_fields.py @@ -0,0 +1,149 @@ +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',) + validation_only_fields = ('password_confirmation', 'accept_our_terms_and_conditions',) + + def restore_object(self, attrs, instance=None): + # Flow: south-bound -- object creation: model instance + for attr in self.Meta.validation_only_fields: + attrs.pop(attr) + return super(ValidationOnlyFieldsExampleSerializer, self).restore_object(attrs, instance) + + def to_native(self, obj): + try: + # Flow: north-bound -- form creation: browser API + return super(ValidationOnlyFieldsExampleSerializer, self).to_native(obj) + except AttributeError as e: + # Flow: south-bound -- object validation: model class + for field in self.Meta.validation_only_fields: + 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) From 0f59d534ce6048de3dacfc2e28481239e747686e Mon Sep 17 00:00:00 2001 From: Val Neekman Date: Sat, 22 Mar 2014 21:44:19 -0400 Subject: [PATCH 2/4] added non-native valiation only fields usage to the serializer docs --- docs/api-guide/serializers.md | 88 ++++++++++++++++++++++++++++------- 1 file changed, 72 insertions(+), 16 deletions(-) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index 7ee060af4..321390e5d 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -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_` (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,62 @@ 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 during the `object attribute` validations. + +**Note:** You should `not` remove the extra `validation only` fields from the `fields` attribute prior to invoking the `to_native()` method on the `parent` serializer class during the `form creation` process if you are using the built-in browsable API. + + # Example of overriding restore_object() and to_native() attributes + + class UserCreationSerializer(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 = ValidationOnlyFieldsExampleModel + fields = ('email', 'password', 'password_confirmation', 'accept_our_terms_and_conditions',) + write_only_fields = ('password',) + validation_only_fields = ('password_confirmation', 'accept_our_terms_and_conditions',) + + def restore_object(self, attrs, instance=None): + # Flow: south-bound -- object creation: model instance + for attr in self.Meta.validation_only_fields: + attrs.pop(attr) + return super(UserCreationSerializer, self).restore_object(attrs, instance) + + def to_native(self, obj): + try: + # Flow: north-bound -- form creation: browser API + return super(UserCreationSerializer, self).to_native(obj) + except AttributeError as e: + # Flow: south-bound -- object validation: model class + for field in self.Meta.validation_only_fields: + self.fields.pop(field) + return super(UserCreationSerializer, 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 +570,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 +596,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` 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`. From 0fd48e03653eca8e8c13e8b5a11582ce263010ac Mon Sep 17 00:00:00 2001 From: Val Neekman Date: Mon, 24 Mar 2014 23:29:31 -0400 Subject: [PATCH 3/4] Update serializer.md Renamed to CreateUserSerializer to match the other examples. --- docs/api-guide/serializers.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index 321390e5d..7512e72cb 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -417,7 +417,7 @@ You can add extra non-native `validation only` fields to a `ModelSerializer` pro # Example of overriding restore_object() and to_native() attributes - class UserCreationSerializer(serializers.ModelSerializer): + class CreateUserSerializer(serializers.ModelSerializer): password_confirmation = serializers.CharField() accept_our_terms_and_conditions = serializers.BooleanField() @@ -440,8 +440,8 @@ You can add extra non-native `validation only` fields to a `ModelSerializer` pro return attrs class Meta: - model = ValidationOnlyFieldsExampleModel - fields = ('email', 'password', 'password_confirmation', 'accept_our_terms_and_conditions',) + model = User + fields = ('username', 'email', 'password', 'password_confirmation', 'accept_our_terms_and_conditions',) write_only_fields = ('password',) validation_only_fields = ('password_confirmation', 'accept_our_terms_and_conditions',) @@ -449,17 +449,17 @@ You can add extra non-native `validation only` fields to a `ModelSerializer` pro # Flow: south-bound -- object creation: model instance for attr in self.Meta.validation_only_fields: attrs.pop(attr) - return super(UserCreationSerializer, self).restore_object(attrs, instance) + return super(CreateUserSerializer, self).restore_object(attrs, instance) def to_native(self, obj): try: # Flow: north-bound -- form creation: browser API - return super(UserCreationSerializer, self).to_native(obj) + return super(CreateUserSerializer, self).to_native(obj) except AttributeError as e: # Flow: south-bound -- object validation: model class for field in self.Meta.validation_only_fields: self.fields.pop(field) - return super(UserCreationSerializer, self).to_native(obj) + return super(CreateUserSerializer, self).to_native(obj) ## Relational fields From f5d223f78213650dce8284ea268f02f55b3cfe29 Mon Sep 17 00:00:00 2001 From: Val Neekman Date: Tue, 25 Mar 2014 00:06:58 -0400 Subject: [PATCH 4/4] simplify the handling of the non-native, validation only fields --- docs/api-guide/serializers.md | 16 ++++------------ .../tests/test_validation_only_fields.py | 12 +++--------- 2 files changed, 7 insertions(+), 21 deletions(-) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index 7512e72cb..d2553f7f4 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -411,9 +411,7 @@ You can add extra non-native `validation only` fields to a `ModelSerializer` pro **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 during the `object attribute` validations. - -**Note:** You should `not` remove the extra `validation only` fields from the `fields` attribute prior to invoking the `to_native()` method on the `parent` serializer class during the `form creation` process if you are using the built-in browsable API. +**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 @@ -443,21 +441,15 @@ You can add extra non-native `validation only` fields to a `ModelSerializer` pro model = User fields = ('username', 'email', 'password', 'password_confirmation', 'accept_our_terms_and_conditions',) write_only_fields = ('password',) - validation_only_fields = ('password_confirmation', 'accept_our_terms_and_conditions',) def restore_object(self, attrs, instance=None): - # Flow: south-bound -- object creation: model instance - for attr in self.Meta.validation_only_fields: + 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): - try: - # Flow: north-bound -- form creation: browser API - return super(CreateUserSerializer, self).to_native(obj) - except AttributeError as e: - # Flow: south-bound -- object validation: model class - for field in self.Meta.validation_only_fields: + 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) diff --git a/rest_framework/tests/test_validation_only_fields.py b/rest_framework/tests/test_validation_only_fields.py index ca07260df..ab49cd3ce 100644 --- a/rest_framework/tests/test_validation_only_fields.py +++ b/rest_framework/tests/test_validation_only_fields.py @@ -40,21 +40,15 @@ class ValidationOnlyFieldsExampleSerializer(serializers.ModelSerializer): model = ValidationOnlyFieldsExampleModel fields = ('email', 'password', 'password_confirmation', 'accept_our_terms_and_conditions',) write_only_fields = ('password',) - validation_only_fields = ('password_confirmation', 'accept_our_terms_and_conditions',) def restore_object(self, attrs, instance=None): - # Flow: south-bound -- object creation: model instance - for attr in self.Meta.validation_only_fields: + 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): - try: - # Flow: north-bound -- form creation: browser API - return super(ValidationOnlyFieldsExampleSerializer, self).to_native(obj) - except AttributeError as e: - # Flow: south-bound -- object validation: model class - for field in self.Meta.validation_only_fields: + 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)