From 9eba60f5b0c59d97fd6526c6deb55c33c7e2e1a0 Mon Sep 17 00:00:00 2001 From: Nadav Samet Date: Tue, 4 Jun 2013 21:29:33 -0700 Subject: [PATCH] Add support for passing callable as `source` argument for a field. Sometimes it is desirable that the serialized form will contain a field that is not directly accessible through a model attribute or method. That may happen, for instance, if you are serializing a third-party model which you cannot modify. This change adds support for passing a callable as a `source` argument for a field. That callable accepts a single argument which is the object being serialized, and in turn, returns the value that goes into the field. --- docs/api-guide/fields.md | 2 +- rest_framework/fields.py | 3 +++ rest_framework/tests/test_serializer.py | 20 ++++++++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 94fff7e21..b97262704 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -20,7 +20,7 @@ Each serializer field class constructor takes at least these arguments. Some Fi ### `source` -The name of the attribute that will be used to populate the field. May be a method that only takes a `self` argument, such as `Field(source='get_absolute_url')`, or may use dotted notation to traverse attributes, such as `Field(source='user.email')`. +The name of the attribute that will be used to populate the field. May be a method that only takes a `self` argument, such as `Field(source='get_absolute_url')`, or may use dotted notation to traverse attributes, such as `Field(source='user.email')`. Alternatively, `source` can be a callable that takes a single argument (the object being serialized) and returns the value that will populate this field. The value `source='*'` has a special meaning, and is used to indicate that the entire object should be passed through to the field. This can be useful for creating nested representations. (See the implementation of the `PaginationSerializer` class for an example.) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 535aa2ac8..d018f659b 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -160,6 +160,9 @@ class Field(object): source = self.source or field_name value = obj + if callable(source): + return source(value) + for component in source.split('.'): value = get_component(value, component) if value is None: diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index 8b87a0847..24b25440e 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -803,6 +803,26 @@ class CallableDefaultValueTests(TestCase): self.assertEqual(instance.text, 'overridden') +class CallableSourceTests(TestCase): + def setUp(self): + class CommentSerializer(serializers.Serializer): + email = serializers.EmailField() + content = serializers.CharField(max_length=1000) + length = serializers.IntegerField( + source=lambda comment: len(comment.content), + read_only=True) + self.serializer_class = CommentSerializer + + def test_callable_source(self): + instance = Comment('user@email.com', 'foobar', None) + serializer = self.serializer_class(instance=instance) + self.assertEquals(serializer.data, { + 'email': 'user@email.com', + 'content': 'foobar', + 'length': 6 + }) + + class ManyRelatedTests(TestCase): def test_reverse_relations(self): post = BlogPost.objects.create(title="Test blog post")