From 9e89d62d24f9f79de05157a408d97ac392c27d8a Mon Sep 17 00:00:00 2001 From: Val Neekman Date: Wed, 19 Mar 2014 12:31:34 -0700 Subject: [PATCH 1/5] added non-native fields support --- rest_framework/serializers.py | 18 ++++++- .../tests/test_non_native_fields.py | 51 +++++++++++++++++++ 2 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 rest_framework/tests/test_non_native_fields.py diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 5c726dfcd..b17f71af8 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -346,7 +346,13 @@ 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: + if field_name in self.opts.non_native_fields: + continue + else: + raise method = getattr(self, 'transform_%s' % field_name, None) if callable(method): value = method(obj, value) @@ -386,6 +392,9 @@ class BaseSerializer(WritableField): Override default so that the serializer can be used as a nested field across relationships. """ + if field_name in self.opts.non_native_fields: + return None + if self.write_only: return None @@ -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 @@ -923,6 +933,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 100644 index 000000000..917b11213 --- /dev/null +++ b/rest_framework/tests/test_non_native_fields.py @@ -0,0 +1,51 @@ +from django.db import models +from django.test import TestCase +from rest_framework import serializers + + +class ExampleModel(models.Model): + email = models.EmailField(max_length=100) + password = models.CharField(max_length=100) + + +class ExampleSerializer(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 = ExampleModel + fields = ('email', 'password', 'password_confirmation',) + write_only_fields = ('password',) + non_native_fields = ('password_confirmation',) + + +class NonNativeFieldTests(TestCase): + def test_non_native_fields(self): + data = { + 'email': 'foo@example.com', + 'password': '123', + 'password_confirmation': '123', + } + serializer = ExampleSerializer(data=data) + self.assertTrue(serializer.is_valid()) + self.assertTrue(isinstance(serializer.object, ExampleModel)) + 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 = ExampleSerializer(data=data) + self.assertFalse(serializer.is_valid()) + self.assertEquals(len(serializer.errors), 1) + self.assertEquals(serializer.errors['password_confirmation'], + ['Password confirmation mismatch']) From d31c12c21d7fadfd707ae8c4934d99259bf7c12b Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Thu, 20 Mar 2014 11:26:32 +0100 Subject: [PATCH 2/5] fix non_native_fields behaviour non_native_fields assumed to be only in ModelSerializer --- rest_framework/serializers.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) mode change 100644 => 100755 rest_framework/serializers.py diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py old mode 100644 new mode 100755 index b17f71af8..e6cdb0dc2 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -346,13 +346,16 @@ class BaseSerializer(WritableField): continue field.initialize(parent=self, field_name=field_name) key = self.get_field_key(field_name) + try: value = field.field_to_native(obj, field_name) - except AttributeError: - if field_name in self.opts.non_native_fields: + 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 + raise e + method = getattr(self, 'transform_%s' % field_name, None) if callable(method): value = method(obj, value) @@ -392,9 +395,6 @@ class BaseSerializer(WritableField): Override default so that the serializer can be used as a nested field across relationships. """ - if field_name in self.opts.non_native_fields: - return None - if self.write_only: return None @@ -907,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): """ From 7637847fa22961158695c3338de5ebbb7ff8af41 Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Thu, 20 Mar 2014 12:06:30 +0100 Subject: [PATCH 3/5] non_native_fields: test additional field shown in browsable API --- .../tests/test_non_native_fields.py | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) mode change 100644 => 100755 rest_framework/tests/test_non_native_fields.py diff --git a/rest_framework/tests/test_non_native_fields.py b/rest_framework/tests/test_non_native_fields.py old mode 100644 new mode 100755 index 917b11213..ef8e002b9 --- a/rest_framework/tests/test_non_native_fields.py +++ b/rest_framework/tests/test_non_native_fields.py @@ -1,6 +1,9 @@ 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 ExampleModel(models.Model): @@ -10,6 +13,7 @@ class ExampleModel(models.Model): class ExampleSerializer(serializers.ModelSerializer): password_confirmation = serializers.CharField() + def validate_password_confirmation(self, attrs, source): password_confirmation = attrs[source] password = attrs['password'] @@ -17,6 +21,7 @@ class ExampleSerializer(serializers.ModelSerializer): raise serializers.ValidationError('Password confirmation mismatch') attrs.pop(source) return attrs + class Meta: model = ExampleModel fields = ('email', 'password', 'password_confirmation',) @@ -24,7 +29,24 @@ class ExampleSerializer(serializers.ModelSerializer): non_native_fields = ('password_confirmation',) +class ExampleView(generics.ListCreateAPIView): + """ + ExampleView + """ + model = ExampleModel + serializer_class = ExampleSerializer + +example_view = ExampleView.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', @@ -49,3 +71,11 @@ class NonNativeFieldTests(TestCase): 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"') From ccb00fb967286461dbf62bfeac4193d828973b78 Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Thu, 20 Mar 2014 14:17:50 +0100 Subject: [PATCH 4/5] non_native_fields docs --- docs/api-guide/serializers.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) mode change 100644 => 100755 docs/api-guide/serializers.md 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. From 3a11802032ecda4954e3095210dda0953993f376 Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Thu, 20 Mar 2014 14:36:14 +0100 Subject: [PATCH 5/5] non_native_fields tests fix on django 1.7a2 --- .../tests/test_non_native_fields.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/rest_framework/tests/test_non_native_fields.py b/rest_framework/tests/test_non_native_fields.py index ef8e002b9..bfd2b8169 100755 --- a/rest_framework/tests/test_non_native_fields.py +++ b/rest_framework/tests/test_non_native_fields.py @@ -6,12 +6,12 @@ from rest_framework import generics from rest_framework.compat import patterns, url -class ExampleModel(models.Model): +class NonNativeExampleModel(models.Model): email = models.EmailField(max_length=100) password = models.CharField(max_length=100) -class ExampleSerializer(serializers.ModelSerializer): +class NonNativeExampleSerializer(serializers.ModelSerializer): password_confirmation = serializers.CharField() def validate_password_confirmation(self, attrs, source): @@ -23,20 +23,20 @@ class ExampleSerializer(serializers.ModelSerializer): return attrs class Meta: - model = ExampleModel + model = NonNativeExampleModel fields = ('email', 'password', 'password_confirmation',) write_only_fields = ('password',) non_native_fields = ('password_confirmation',) -class ExampleView(generics.ListCreateAPIView): +class NonNativeExampleView(generics.ListCreateAPIView): """ - ExampleView + NonNativeExampleView """ - model = ExampleModel - serializer_class = ExampleSerializer + model = NonNativeExampleModel + serializer_class = NonNativeExampleSerializer -example_view = ExampleView.as_view() +example_view = NonNativeExampleView.as_view() urlpatterns = patterns('', @@ -53,9 +53,9 @@ class NonNativeFieldTests(TestCase): 'password': '123', 'password_confirmation': '123', } - serializer = ExampleSerializer(data=data) + serializer = NonNativeExampleSerializer(data=data) self.assertTrue(serializer.is_valid()) - self.assertTrue(isinstance(serializer.object, ExampleModel)) + 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'}) @@ -66,7 +66,7 @@ class NonNativeFieldTests(TestCase): 'password': '123', 'password_confirmation': 'abc', } - serializer = ExampleSerializer(data=data) + serializer = NonNativeExampleSerializer(data=data) self.assertFalse(serializer.is_valid()) self.assertEquals(len(serializer.errors), 1) self.assertEquals(serializer.errors['password_confirmation'],