From de3400f6082b70b239ca67e0becbc52150026716 Mon Sep 17 00:00:00 2001 From: eofs Date: Thu, 10 Jan 2013 12:10:35 +0200 Subject: [PATCH 01/14] Include fields from reversed relations fixes #569 --- rest_framework/serializers.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index da0af467f..624b69d8b 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -92,7 +92,6 @@ class SerializerOptions(object): self.fields = getattr(meta, 'fields', ()) self.exclude = getattr(meta, 'exclude', ()) - class BaseSerializer(Field): class Meta(object): pass @@ -361,6 +360,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_reversed_relations = getattr(meta, 'include_reversed_relations', False) class ModelSerializer(Serializer): @@ -383,6 +383,12 @@ 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] + reversed_fields = () + if self.opts.include_reversed_relations: + reversed_fields = [obj.field for obj in opts.get_all_related_objects() if obj.field.serialize] + reversed_fields = [obj.field for obj in opts.get_all_related_many_to_many_objects() if obj.field.serialize] + fields += reversed_fields + ret = SortedDict() nested = bool(self.opts.depth) is_pk = True # First field in the list is the pk @@ -401,7 +407,10 @@ class ModelSerializer(Serializer): field = self.get_field(model_field) if field: - ret[model_field.name] = field + if model_field in reversed_fields: + ret[model_field.rel.related_name] = field + else: + ret[model_field.name] = field for field_name in self.opts.read_only_fields: assert field_name in ret, \ @@ -421,9 +430,12 @@ class ModelSerializer(Serializer): """ Creates a default instance of a nested relational field. """ + + # If field is reversed relation, get model from relation + obj_model = model_field.rel.to if self.opts.model is not model_field.rel.to else model_field.model class NestedModelSerializer(ModelSerializer): class Meta: - model = model_field.rel.to + model = obj_model return NestedModelSerializer() def get_related_field(self, model_field, to_many=False): From 3425dfd80117e8c5dbcb2b6aaa781c46b6fa6b6b Mon Sep 17 00:00:00 2001 From: eofs Date: Mon, 14 Jan 2013 12:10:24 +0200 Subject: [PATCH 02/14] Small naming corrections and errors fixed --- rest_framework/serializers.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 624b69d8b..4bbae1665 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -360,7 +360,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_reversed_relations = getattr(meta, 'include_reversed_relations', False) + self.include_reverse_relations = getattr(meta, 'include_reverse_relations', False) class ModelSerializer(Serializer): @@ -369,6 +369,10 @@ class ModelSerializer(Serializer): """ _options_class = ModelSerializerOptions + def get_reverse_fields(self, opts, fields): + relations = [obj for obj in opts.get_all_related_many_to_many_objects() if obj.field.serialize] + return [rel.field for rel in relations] + def get_default_fields(self): """ Return all the fields that should be serialized for the model. @@ -383,11 +387,10 @@ 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] - reversed_fields = () - if self.opts.include_reversed_relations: - reversed_fields = [obj.field for obj in opts.get_all_related_objects() if obj.field.serialize] - reversed_fields = [obj.field for obj in opts.get_all_related_many_to_many_objects() if obj.field.serialize] - fields += reversed_fields + 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) @@ -407,7 +410,7 @@ class ModelSerializer(Serializer): field = self.get_field(model_field) if field: - if model_field in reversed_fields: + if model_field in reverse_fields: ret[model_field.rel.related_name] = field else: ret[model_field.name] = field @@ -431,8 +434,14 @@ class ModelSerializer(Serializer): Creates a default instance of a nested relational field. """ - # If field is reversed relation, get model from relation - obj_model = model_field.rel.to if self.opts.model is not model_field.rel.to else model_field.model + # 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 + obj_model = model_field.rel.to + else: + # Forward relation, no need for magic + obj_model = model_field.model + class NestedModelSerializer(ModelSerializer): class Meta: model = obj_model From e25cea43a9e7d8d5c787e534ddd139df84772f65 Mon Sep 17 00:00:00 2001 From: eofs Date: Mon, 14 Jan 2013 14:19:19 +0200 Subject: [PATCH 03/14] All kind of relations should work now --- rest_framework/serializers.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 4bbae1665..7e9786b4e 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -370,7 +370,9 @@ class ModelSerializer(Serializer): _options_class = ModelSerializerOptions def get_reverse_fields(self, opts, fields): - relations = [obj for obj in opts.get_all_related_many_to_many_objects() if obj.field.serialize] + 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] return [rel.field for rel in relations] def get_default_fields(self): @@ -411,7 +413,10 @@ class ModelSerializer(Serializer): if field: if model_field in reverse_fields: - ret[model_field.rel.related_name] = field + # 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 From b04fd3d0503a289508eadccb044d8d4ac00f3980 Mon Sep 17 00:00:00 2001 From: eofs Date: Mon, 14 Jan 2013 14:32:10 +0200 Subject: [PATCH 04/14] Test case --- rest_framework/tests/serializer.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index bd96ba23e..f9abe0f6b 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -613,7 +613,6 @@ class CallableDefaultValueTests(TestCase): self.assertEquals(instance.pk, 1) self.assertEquals(instance.text, 'overridden') - class ManyRelatedTests(TestCase): def test_reverse_relations(self): post = BlogPost.objects.create(title="Test blog post") @@ -638,6 +637,30 @@ 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 + 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") @@ -901,3 +924,4 @@ class NestedSerializerContextTests(TestCase): # This will raise RuntimeError if context doesn't get passed correctly to the nested Serializers AlbumCollectionSerializer(album_collection, context={'context_item': 'album context'}).data + From 6f36563b9224144aa0c50e434905800240fffabf Mon Sep 17 00:00:00 2001 From: eofs Date: Mon, 14 Jan 2013 16:03:04 +0200 Subject: [PATCH 05/14] Take default value from 'INCLUDE_REVERSE_RELATIONS' settings --- rest_framework/serializers.py | 3 ++- rest_framework/settings.py | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 4c586950e..a40b88b00 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -6,6 +6,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: # @@ -360,7 +361,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', False) + self.include_reverse_relations = getattr(meta, 'include_reverse_relations', api_settings.INCLUDE_REVERSE_RELATIONS) class ModelSerializer(Serializer): diff --git a/rest_framework/settings.py b/rest_framework/settings.py index 5c77c55cd..2453f8400 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -55,6 +55,9 @@ DEFAULTS = { 'anon': None, }, + # ModelSerializer + 'INCLUDE_REVERSE_RELATIONS': False, + # Pagination 'PAGINATE_BY': None, 'PAGINATE_BY_PARAM': None, From c8af10349d29c27c9edd7661fe012ee0d7377f12 Mon Sep 17 00:00:00 2001 From: eofs Date: Mon, 14 Jan 2013 16:06:14 +0200 Subject: [PATCH 06/14] Better name for a variable --- rest_framework/serializers.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index a40b88b00..2b6677545 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -439,18 +439,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 - obj_model = model_field.rel.to + model_class = model_field.rel.to else: # Forward relation, no need for magic - obj_model = model_field.model + model_class = model_field.model class NestedModelSerializer(ModelSerializer): class Meta: - model = obj_model + model = model_class return NestedModelSerializer() def get_related_field(self, model_field, to_many=False): From 33d48041b8e8218c2b5ebb1f8fc725d272fb789c Mon Sep 17 00:00:00 2001 From: eofs Date: Mon, 14 Jan 2013 16:16:03 +0200 Subject: [PATCH 07/14] Docs updated --- docs/api-guide/serializers.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index d98a602f7..4bc822d41 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -243,6 +243,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 `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`. From ad6ca835c01ad76de12c7eb3f28794ff115778b2 Mon Sep 17 00:00:00 2001 From: eofs Date: Tue, 15 Jan 2013 11:21:56 +0200 Subject: [PATCH 08/14] Test updated (Test reverse relations with and without depth) --- rest_framework/tests/serializer.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index f9abe0f6b..a8ea29045 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -642,6 +642,26 @@ class ManyRelatedTests(TestCase): 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() From f3d63deabe8d0ea026754e93f390f5357c65bfd1 Mon Sep 17 00:00:00 2001 From: eofs Date: Tue, 15 Jan 2013 11:23:07 +0200 Subject: [PATCH 09/14] Exclude intermediate models from reverse fields. Always dealt reverse relational fields as Many fields. --- rest_framework/serializers.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 2b6677545..34d8a8759 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -371,10 +371,22 @@ 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] - return [rel.field for rel in relations] + + # 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): """ @@ -456,6 +468,9 @@ class ModelSerializer(Serializer): """ Creates a default instance of a flat relational field. """ + # Reverse relational fields must be dealt as Many fields + if model_field.model is not self.opts.model: + to_many = True # TODO: filter queryset using: # .using(db).complex_filter(self.rel.limit_choices_to) kwargs = { From fa0683156623514e68da8d6643d4fe4f6c0d68a1 Mon Sep 17 00:00:00 2001 From: eofs Date: Tue, 15 Jan 2013 11:46:45 +0200 Subject: [PATCH 10/14] Revert whitespace change --- rest_framework/tests/serializer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index a8ea29045..6b5fe3b3b 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -613,6 +613,7 @@ class CallableDefaultValueTests(TestCase): self.assertEquals(instance.pk, 1) self.assertEquals(instance.text, 'overridden') + class ManyRelatedTests(TestCase): def test_reverse_relations(self): post = BlogPost.objects.create(title="Test blog post") From 60d7d005b4727e1e504e921ec9d50a25c0bc12ee Mon Sep 17 00:00:00 2001 From: eofs Date: Tue, 15 Jan 2013 12:04:57 +0200 Subject: [PATCH 11/14] Once more reverted whitespace changes --- rest_framework/serializers.py | 1 + rest_framework/tests/serializer.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 34d8a8759..79800934c 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -93,6 +93,7 @@ class SerializerOptions(object): self.fields = getattr(meta, 'fields', ()) self.exclude = getattr(meta, 'exclude', ()) + class BaseSerializer(Field): class Meta(object): pass diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 6b5fe3b3b..59fcd6315 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -945,4 +945,3 @@ class NestedSerializerContextTests(TestCase): # This will raise RuntimeError if context doesn't get passed correctly to the nested Serializers AlbumCollectionSerializer(album_collection, context={'context_item': 'album context'}).data - From e1157525f91a113ba47f9478cd7a177ac97cc190 Mon Sep 17 00:00:00 2001 From: eofs Date: Tue, 15 Jan 2013 12:38:47 +0200 Subject: [PATCH 12/14] Setting name changed --- rest_framework/serializers.py | 2 +- rest_framework/settings.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 79800934c..325fbfdb8 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -362,7 +362,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.INCLUDE_REVERSE_RELATIONS) + self.include_reverse_relations = getattr(meta, 'include_reverse_relations', api_settings.DEFAULT_INCLUDE_REVERSE_RELATIONS) class ModelSerializer(Serializer): diff --git a/rest_framework/settings.py b/rest_framework/settings.py index 2453f8400..11bc37457 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -56,7 +56,7 @@ DEFAULTS = { }, # ModelSerializer - 'INCLUDE_REVERSE_RELATIONS': False, + 'DEFAULT_INCLUDE_REVERSE_RELATIONS': False, # Pagination 'PAGINATE_BY': None, From 6bcac18e7e34835422e1a1abdfedb215cbc11d69 Mon Sep 17 00:00:00 2001 From: eofs Date: Tue, 15 Jan 2013 12:57:15 +0200 Subject: [PATCH 13/14] Docs updated --- docs/api-guide/serializers.md | 2 +- docs/api-guide/settings.md | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index 4bc822d41..c9657afdd 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -245,7 +245,7 @@ All the relational fields may be used for any relationship or reverse relationsh ## Reverse relational fields -By default reverse relational fields are not displayed when ModelSerializer is used. You can control this behavior by using `INCLUDE_REVERSE_RELATIONS` setting. +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: 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. From 4d8afddfb2b737dda5db320c6dfc0d190762c6e5 Mon Sep 17 00:00:00 2001 From: eofs Date: Tue, 15 Jan 2013 13:01:21 +0200 Subject: [PATCH 14/14] 'to_many' check moved --- rest_framework/serializers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 325fbfdb8..3cb6cb143 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -421,6 +421,9 @@ 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) @@ -469,9 +472,6 @@ class ModelSerializer(Serializer): """ Creates a default instance of a flat relational field. """ - # Reverse relational fields must be dealt as Many fields - if model_field.model is not self.opts.model: - to_many = True # TODO: filter queryset using: # .using(db).complex_filter(self.rel.limit_choices_to) kwargs = {