Skip to content

Commit cdb8a52

Browse files
bor3hammblayman
authored andcommitted
Fix/auto prefetch with m2m (#333)
* Get the field queryset in a more reliable way That works through many to many field descriptors, previous way only worked through many to ones. * Update test views to use optimised viewset * Rename comment_set to comments, use json viewset Updates the tests to use the optimised queryset views provided in views.py. For these to work, the serializer fields need to match the model names so I have just changed the related_name in the test models. This could be improved elsewhere, with the queryset prefetching based on the serializer. But for the most basic use case, this is fine. * Correctly prefetch both forward and reverse Based on the relation descriptor. * Support field rename in older django * Update to py2 friendly super * Replace hasattr with explicit version check
1 parent b9de139 commit cdb8a52

File tree

8 files changed

+49
-31
lines changed

8 files changed

+49
-31
lines changed

‎example/models.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ def __str__(self):
7676

7777
@python_2_unicode_compatible
7878
class Comment(BaseModel):
79-
entry = models.ForeignKey(Entry)
79+
entry = models.ForeignKey(Entry, related_name='comments')
8080
body = models.TextField()
8181
author = models.ForeignKey(
8282
Author,

‎example/serializers.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ def __init__(self, *args, **kwargs):
5555
body_format = serializers.SerializerMethodField()
5656
# many related from model
5757
comments = relations.ResourceRelatedField(
58-
source='comment_set', many=True, read_only=True)
58+
many=True, read_only=True)
5959
# many related from serializer
6060
suggested = relations.SerializerMethodResourceRelatedField(
6161
source='get_suggested', model=Entry, many=True, read_only=True,

‎example/tests/integration/test_includes.py

+8-8
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ def test_included_data_on_list(multiple_entries, client, query='?include=comment
1818
assert [x.get('type') for x in included] == ['comments', 'comments'], 'List included types are incorrect'
1919

2020
comment_count = len([resource for resource in included if resource["type"] == "comments"])
21-
expected_comment_count = sum([entry.comment_set.count() for entry in multiple_entries])
21+
expected_comment_count = sum([entry.comments.count() for entry in multiple_entries])
2222
assert comment_count == expected_comment_count, 'List comment count is incorrect'
2323

2424

@@ -33,7 +33,7 @@ def test_included_data_on_detail(single_entry, client, query='?include=comments'
3333
assert [x.get('type') for x in included] == ['comments'], 'Detail included types are incorrect'
3434

3535
comment_count = len([resource for resource in included if resource["type"] == "comments"])
36-
expected_comment_count = single_entry.comment_set.count()
36+
expected_comment_count = single_entry.comments.count()
3737
assert comment_count == expected_comment_count, 'Detail comment count is incorrect'
3838

3939

@@ -81,16 +81,16 @@ def test_deep_included_data_on_list(multiple_entries, client):
8181
], 'List included types are incorrect'
8282

8383
comment_count = len([resource for resource in included if resource["type"] == "comments"])
84-
expected_comment_count = sum([entry.comment_set.count() for entry in multiple_entries])
84+
expected_comment_count = sum([entry.comments.count() for entry in multiple_entries])
8585
assert comment_count == expected_comment_count, 'List comment count is incorrect'
8686

8787
author_count = len([resource for resource in included if resource["type"] == "authors"])
8888
expected_author_count = sum(
89-
[entry.comment_set.filter(author__isnull=False).count() for entry in multiple_entries])
89+
[entry.comments.filter(author__isnull=False).count() for entry in multiple_entries])
9090
assert author_count == expected_author_count, 'List author count is incorrect'
9191

9292
author_bio_count = len([resource for resource in included if resource["type"] == "authorBios"])
93-
expected_author_bio_count = sum([entry.comment_set.filter(
93+
expected_author_bio_count = sum([entry.comments.filter(
9494
author__bio__isnull=False).count() for entry in multiple_entries])
9595
assert author_bio_count == expected_author_bio_count, 'List author bio count is incorrect'
9696

@@ -107,7 +107,7 @@ def test_deep_included_data_on_list(multiple_entries, client):
107107
author_count = len([resource for resource in included if resource["type"] == "authors"])
108108
expected_author_count = sum(
109109
[entry.authors.count() for entry in multiple_entries] +
110-
[entry.comment_set.filter(author__isnull=False).count() for entry in multiple_entries])
110+
[entry.comments.filter(author__isnull=False).count() for entry in multiple_entries])
111111
assert author_count == expected_author_count, 'List author count is incorrect'
112112

113113

@@ -122,9 +122,9 @@ def test_deep_included_data_on_detail(single_entry, client):
122122
'Detail included types are incorrect'
123123

124124
comment_count = len([resource for resource in included if resource["type"] == "comments"])
125-
expected_comment_count = single_entry.comment_set.count()
125+
expected_comment_count = single_entry.comments.count()
126126
assert comment_count == expected_comment_count, 'Detail comment count is incorrect'
127127

128128
author_bio_count = len([resource for resource in included if resource["type"] == "authorBios"])
129-
expected_author_bio_count = single_entry.comment_set.filter(author__bio__isnull=False).count()
129+
expected_author_bio_count = single_entry.comments.filter(author__bio__isnull=False).count()
130130
assert author_bio_count == expected_author_bio_count, 'Detail author bio count is incorrect'

‎example/tests/test_relations.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -104,17 +104,17 @@ def test_deserialize_many_to_many_relation(self):
104104
author_pks = Author.objects.values_list('pk', flat=True)
105105
authors = [{'type': type_string, 'id': pk} for pk in author_pks]
106106

107-
serializer = EntryModelSerializer(data={'authors': authors, 'comment_set': []})
107+
serializer = EntryModelSerializer(data={'authors': authors, 'comments': []})
108108

109109
self.assertTrue(serializer.is_valid())
110110
self.assertEqual(len(serializer.validated_data['authors']), Author.objects.count())
111111
for author in serializer.validated_data['authors']:
112112
self.assertIsInstance(author, Author)
113113

114114
def test_read_only(self):
115-
serializer = EntryModelSerializer(data={'authors': [], 'comment_set': [{'type': 'Comments', 'id': 2}]})
115+
serializer = EntryModelSerializer(data={'authors': [], 'comments': [{'type': 'Comments', 'id': 2}]})
116116
serializer.is_valid(raise_exception=True)
117-
self.assertNotIn('comment_set', serializer.validated_data)
117+
self.assertNotIn('comments', serializer.validated_data)
118118

119119
def test_invalid_resource_id_object(self):
120120
comment = {'body': 'testing 123', 'entry': {'type': 'entry'}, 'author': {'id': '5'}}
@@ -136,8 +136,8 @@ class EntryFKSerializer(serializers.Serializer):
136136

137137
class EntryModelSerializer(serializers.ModelSerializer):
138138
authors = ResourceRelatedField(many=True, queryset=Author.objects)
139-
comment_set = ResourceRelatedField(many=True, read_only=True)
139+
comments = ResourceRelatedField(many=True, read_only=True)
140140

141141
class Meta:
142142
model = Entry
143-
fields = ('authors', 'comment_set')
143+
fields = ('authors', 'comments')

‎example/tests/test_views.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ def test_post_to_one_relationship_should_fail(self):
158158
assert response.status_code == 405, response.content.decode()
159159

160160
def test_post_to_many_relationship_with_no_change(self):
161-
url = '/entries/{}/relationships/comment_set'.format(self.first_entry.id)
161+
url = '/entries/{}/relationships/comments'.format(self.first_entry.id)
162162
request_data = {
163163
'data': [{'type': format_resource_type('Comment'), 'id': str(self.first_comment.id)}, ]
164164
}
@@ -167,7 +167,7 @@ def test_post_to_many_relationship_with_no_change(self):
167167
assert len(response.rendered_content) == 0, response.rendered_content.decode()
168168

169169
def test_post_to_many_relationship_with_change(self):
170-
url = '/entries/{}/relationships/comment_set'.format(self.first_entry.id)
170+
url = '/entries/{}/relationships/comments'.format(self.first_entry.id)
171171
request_data = {
172172
'data': [{'type': format_resource_type('Comment'), 'id': str(self.second_comment.id)}, ]
173173
}
@@ -202,7 +202,7 @@ def test_delete_relationship_overriding_with_none(self):
202202
assert response.data['author'] == None
203203

204204
def test_delete_to_many_relationship_with_no_change(self):
205-
url = '/entries/{}/relationships/comment_set'.format(self.first_entry.id)
205+
url = '/entries/{}/relationships/comments'.format(self.first_entry.id)
206206
request_data = {
207207
'data': [{'type': format_resource_type('Comment'), 'id': str(self.second_comment.id)}, ]
208208
}
@@ -211,7 +211,7 @@ def test_delete_to_many_relationship_with_no_change(self):
211211
assert len(response.rendered_content) == 0, response.rendered_content.decode()
212212

213213
def test_delete_one_to_many_relationship_with_not_null_constraint(self):
214-
url = '/entries/{}/relationships/comment_set'.format(self.first_entry.id)
214+
url = '/entries/{}/relationships/comments'.format(self.first_entry.id)
215215
request_data = {
216216
'data': [{'type': format_resource_type('Comment'), 'id': str(self.first_comment.id)}, ]
217217
}

‎example/tests/unit/test_renderers.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ class DummyTestSerializer(serializers.ModelSerializer):
1616
a single embedded relation
1717
'''
1818
related_models = RelatedModelSerializer(
19-
source='comment_set', many=True, read_only=True)
19+
source='comments', many=True, read_only=True)
2020

2121
class Meta:
2222
model = Entry

‎example/views.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import rest_framework_json_api.metadata
66
import rest_framework_json_api.parsers
77
import rest_framework_json_api.renderers
8-
from rest_framework_json_api.views import RelationshipView
8+
from rest_framework_json_api.views import ModelViewSet, RelationshipView
99
from example.models import Blog, Entry, Author, Comment
1010
from example.serializers import (
1111
BlogSerializer, EntrySerializer, AuthorSerializer, CommentSerializer)
@@ -15,12 +15,12 @@
1515
HTTP_422_UNPROCESSABLE_ENTITY = 422
1616

1717

18-
class BlogViewSet(viewsets.ModelViewSet):
18+
class BlogViewSet(ModelViewSet):
1919
queryset = Blog.objects.all()
2020
serializer_class = BlogSerializer
2121

2222

23-
class JsonApiViewSet(viewsets.ModelViewSet):
23+
class JsonApiViewSet(ModelViewSet):
2424
"""
2525
This is an example on how to configure DRF-jsonapi from
2626
within a class. It allows using DRF-jsonapi alongside
@@ -54,20 +54,20 @@ class BlogCustomViewSet(JsonApiViewSet):
5454
serializer_class = BlogSerializer
5555

5656

57-
class EntryViewSet(viewsets.ModelViewSet):
57+
class EntryViewSet(ModelViewSet):
5858
queryset = Entry.objects.all()
5959
resource_name = 'posts'
6060

6161
def get_serializer_class(self):
6262
return EntrySerializer
6363

6464

65-
class AuthorViewSet(viewsets.ModelViewSet):
65+
class AuthorViewSet(ModelViewSet):
6666
queryset = Author.objects.all()
6767
serializer_class = AuthorSerializer
6868

6969

70-
class CommentViewSet(viewsets.ModelViewSet):
70+
class CommentViewSet(ModelViewSet):
7171
queryset = Comment.objects.all()
7272
serializer_class = CommentSerializer
7373

‎rest_framework_json_api/views.py

+23-5
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,17 @@
66
from django.db.models.manager import Manager
77
if django.VERSION < (1, 9):
88
from django.db.models.fields.related import (
9-
ReverseSingleRelatedObjectDescriptor as ForwardManyToOneDescriptor,
9+
ForeignRelatedObjectsDescriptor as ReverseManyToOneDescriptor,
1010
ManyRelatedObjectsDescriptor as ManyToManyDescriptor,
11+
ReverseSingleRelatedObjectDescriptor as ForwardManyToOneDescriptor,
12+
SingleRelatedObjectDescriptor as ReverseOneToOneDescriptor,
1113
)
1214
else:
1315
from django.db.models.fields.related_descriptors import (
1416
ForwardManyToOneDescriptor,
1517
ManyToManyDescriptor,
18+
ReverseManyToOneDescriptor,
19+
ReverseOneToOneDescriptor,
1620
)
1721
from rest_framework import generics, viewsets
1822
from rest_framework.response import Response
@@ -32,7 +36,7 @@
3236

3337
class ModelViewSet(viewsets.ModelViewSet):
3438
def get_queryset(self, *args, **kwargs):
35-
qs = super().get_queryset(*args, **kwargs)
39+
qs = super(ModelViewSet, self).get_queryset(*args, **kwargs)
3640
included_resources = get_included_resources(self.request)
3741

3842
for included in included_resources:
@@ -44,16 +48,30 @@ def get_queryset(self, *args, **kwargs):
4448
break
4549
field = getattr(level_model, level)
4650
field_class = field.__class__
47-
if not (
51+
52+
is_forward_relation = (
4853
issubclass(field_class, ForwardManyToOneDescriptor)
4954
or issubclass(field_class, ManyToManyDescriptor)
50-
):
55+
)
56+
is_reverse_relation = (
57+
issubclass(field_class, ReverseManyToOneDescriptor)
58+
or issubclass(field_class, ReverseOneToOneDescriptor)
59+
)
60+
if not (is_forward_relation or is_reverse_relation):
5161
break
5262

5363
if level == levels[-1]:
5464
included_model = field
5565
else:
56-
level_model = field.get_queryset().model
66+
if django.VERSION < (1, 9):
67+
model_field = field.related
68+
else:
69+
model_field = field.field
70+
71+
if is_forward_relation:
72+
level_model = model_field.related_model
73+
else:
74+
level_model = model_field.model
5775

5876
if included_model is not None:
5977
qs = qs.prefetch_related(included.replace('.', '__'))

0 commit comments

Comments
 (0)