diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index 487502e9a..855a0f9f6 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -237,6 +237,16 @@ The `RelatedField` class may be subclassed to create a custom representation of All the relational fields may be used for any relationship or reverse relationship on a model. +## Reverse relational fields + +By default reverse relational fields are not displayed when ModelSerializer is used. You can control this behavior by using `DEFAULT_INCLUDE_REVERSE_RELATIONS` setting. + +Besides global setting you can also use model specific setting: + + class BlogPostSerializer(serializer.ModelSerializer): + class Meta: + include_reverse_relations = True + ## Specifying which fields should be included If you only want a subset of the default fields to be used in a model serializer, you can do so using `fields` or `exclude` options, just as you would with a `ModelForm`. diff --git a/docs/api-guide/settings.md b/docs/api-guide/settings.md index a422e5f61..984c00d35 100644 --- a/docs/api-guide/settings.md +++ b/docs/api-guide/settings.md @@ -96,6 +96,12 @@ Default: `rest_framework.serializers.ModelSerializer` Default: `rest_framework.pagination.PaginationSerializer` +## DEFAULT_INCLUDE_REVERSE_RELATIONS + +If set to `True`, ModelSerializer will display reverse relational fields from other models. + +Default: `False` + ## FILTER_BACKEND The filter backend class that should be used for generic filtering. If set to `None` then generic filtering is disabled. diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 4fb802a7c..066d93c54 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -7,6 +7,7 @@ from django.db import models from django.forms import widgets from django.utils.datastructures import SortedDict from rest_framework.compat import get_concrete_model +from rest_framework.settings import api_settings # Note: We do the following so that users of the framework can use this style: # @@ -371,6 +372,7 @@ class ModelSerializerOptions(SerializerOptions): super(ModelSerializerOptions, self).__init__(meta) self.model = getattr(meta, 'model', None) self.read_only_fields = getattr(meta, 'read_only_fields', ()) + self.include_reverse_relations = getattr(meta, 'include_reverse_relations', api_settings.DEFAULT_INCLUDE_REVERSE_RELATIONS) class ModelSerializer(Serializer): @@ -379,6 +381,24 @@ class ModelSerializer(Serializer): """ _options_class = ModelSerializerOptions + def get_reverse_fields(self, opts, fields): + # Construct a list of all relations + relations = [] + relations += [obj for obj in opts.get_all_related_objects() if obj.field.serialize] + relations += [obj for obj in opts.get_all_related_many_to_many_objects() if obj.field.serialize] + + # Construct a list of intermediate models + exclude = [] + for field in fields: + if field.rel and hasattr(field.rel, 'through'): + exclude.append(field.rel.through) + # Intermediate models from reverse relations + for rel in relations: + if rel.field.rel and hasattr(rel.field.rel, 'through'): + exclude.append(rel.field.rel.through) + + return [rel.field for rel in relations if rel.model not in exclude] + def get_default_fields(self): """ Return all the fields that should be serialized for the model. @@ -393,6 +413,11 @@ class ModelSerializer(Serializer): fields += [field for field in opts.fields if field.serialize] fields += [field for field in opts.many_to_many if field.serialize] + reverse_fields = [] + if self.opts.include_reverse_relations: + reverse_fields = self.get_reverse_fields(opts, fields) + fields += reverse_fields + ret = SortedDict() nested = bool(self.opts.depth) is_pk = True # First field in the list is the pk @@ -406,12 +431,21 @@ class ModelSerializer(Serializer): elif model_field.rel: to_many = isinstance(model_field, models.fields.related.ManyToManyField) + # Reverse relational fields must be dealt as Many fields + if model_field.model is not self.opts.model: + to_many = True field = self.get_related_field(model_field, to_many=to_many) else: field = self.get_field(model_field) if field: - ret[model_field.name] = field + if model_field in reverse_fields: + # Get user set 'related_name' or automatically set field + # name e.g. 'comment_set' + name = model_field.related.get_accessor_name() + ret[name] = field + else: + ret[model_field.name] = field for field_name in self.opts.read_only_fields: assert field_name in ret, \ @@ -431,9 +465,17 @@ class ModelSerializer(Serializer): """ Creates a default instance of a nested relational field. """ + # Field has reverse relation if it's referring to different model + if self.opts.model is not model_field.rel.to: + # Get correct model from the relation + model_class = model_field.rel.to + else: + # Forward relation, no need for magic + model_class = model_field.model + class NestedModelSerializer(ModelSerializer): class Meta: - model = model_field.rel.to + model = model_class return NestedModelSerializer() def get_related_field(self, model_field, to_many=False): diff --git a/rest_framework/settings.py b/rest_framework/settings.py index 5c77c55cd..11bc37457 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -55,6 +55,9 @@ DEFAULTS = { 'anon': None, }, + # ModelSerializer + 'DEFAULT_INCLUDE_REVERSE_RELATIONS': False, + # Pagination 'PAGINATE_BY': None, 'PAGINATE_BY_PARAM': None, diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 48b4f1ab9..378e444f7 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -695,6 +695,50 @@ class ManyRelatedTests(TestCase): self.assertEqual(serializer.data, expected) + def test_include_reverse_relations(self): + post = BlogPost.objects.create(title="Test blog post") + post.blogpostcomment_set.create(text="I hate this blog post") + post.blogpostcomment_set.create(text="I love this blog post") + + class BlogPostCommentSerializer(serializers.Serializer): + text = serializers.CharField() + + class BlogPostSerializer(serializers.ModelSerializer): + class Meta: + model = BlogPost + include_reverse_relations = True + + serializer = BlogPostSerializer(instance=post) + expected = { + 'id': 1, 'title': u'Test blog post', 'writer': None, + 'blogpostcomment_set': [1, 2] + } + self.assertEqual(serializer.data, expected) + + def test_depth_include_reverse_relations(self): + post = BlogPost.objects.create(title="Test blog post") + post.blogpostcomment_set.create(text="I hate this blog post") + post.blogpostcomment_set.create(text="I love this blog post") + + class BlogPostCommentSerializer(serializers.Serializer): + text = serializers.CharField() + + class BlogPostSerializer(serializers.ModelSerializer): + class Meta: + model = BlogPost + include_reverse_relations = True + depth = 1 + + serializer = BlogPostSerializer(instance=post) + expected = { + 'id': 1, 'title': u'Test blog post', 'writer': None, + 'blogpostcomment_set': [ + {'id': 1, 'text': u'I hate this blog post', 'blog_post': 1}, + {'id': 2, 'text': u'I love this blog post', 'blog_post': 1} + ] + } + self.assertEqual(serializer.data, expected) + def test_callable_source(self): post = BlogPost.objects.create(title="Test blog post") post.blogpostcomment_set.create(text="I love this blog post")