Add tests for M2M relations with inheritance and a through model

This particular setup worked fine up to DRF 3.2.5, but is broken in DRF
3.3.0.  Bisecting indicates that 322bda8159
first introduced the regression.

This commit adds a few testcases that pass with DRF 3.2.5 but fail with
DRF 3.3.0 and newer.
This commit is contained in:
Baptiste Jonglez 2016-02-09 07:58:39 +01:00
parent aeda2adeea
commit 4e6bfc40dc
3 changed files with 173 additions and 0 deletions

View File

@ -41,6 +41,24 @@ class ManyToManySource(RESTFrameworkModel):
targets = models.ManyToManyField(ManyToManyTarget, related_name='sources')
# ManyToMany with inheritance and a through model
class ManyToManyThroughTarget(RESTFrameworkModel):
name = models.CharField(max_length=100)
class ManyToManyThroughSource(ManyToManyThroughTarget):
name2 = models.CharField(max_length=100)
targets = models.ManyToManyField(ManyToManyThroughTarget,
through='ManyToManyThrough',
related_name='sources')
class ManyToManyThrough(RESTFrameworkModel):
name = models.CharField(max_length=100)
source = models.ForeignKey(ManyToManyThroughSource, related_name='through')
target = models.ForeignKey(ManyToManyThroughTarget, related_name='through')
# ForeignKey
class ForeignKeyTarget(RESTFrameworkModel):
name = models.CharField(max_length=100)

View File

@ -7,6 +7,7 @@ from rest_framework import serializers
from rest_framework.test import APIRequestFactory
from tests.models import (
ForeignKeySource, ForeignKeyTarget, ManyToManySource, ManyToManyTarget,
ManyToManyThrough, ManyToManyThroughSource, ManyToManyThroughTarget,
NullableForeignKeySource, NullableOneToOneSource, OneToOneTarget
)
@ -22,6 +23,9 @@ urlpatterns = [
url(r'^dummyurl/(?P<pk>[0-9]+)/$', dummy_view, name='dummy-url'),
url(r'^manytomanysource/(?P<pk>[0-9]+)/$', dummy_view, name='manytomanysource-detail'),
url(r'^manytomanytarget/(?P<pk>[0-9]+)/$', dummy_view, name='manytomanytarget-detail'),
url(r'^manytomanythroughsource/(?P<pk>[0-9]+)/$', dummy_view, name='manytomanythroughsource-detail'),
url(r'^manytomanythroughtarget/(?P<pk>[0-9]+)/$', dummy_view, name='manytomanythroughtarget-detail'),
url(r'^manytomanythrough/(?P<pk>[0-9]+)/$', dummy_view, name='manytomanythrough-detail'),
url(r'^foreignkeysource/(?P<pk>[0-9]+)/$', dummy_view, name='foreignkeysource-detail'),
url(r'^foreignkeytarget/(?P<pk>[0-9]+)/$', dummy_view, name='foreignkeytarget-detail'),
url(r'^nullableforeignkeysource/(?P<pk>[0-9]+)/$', dummy_view, name='nullableforeignkeysource-detail'),
@ -43,6 +47,25 @@ class ManyToManySourceSerializer(serializers.HyperlinkedModelSerializer):
fields = ('url', 'name', 'targets')
# ManyToMany with inheritance and a through model
class ManyToManyThroughSourceSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = ManyToManyThroughSource
fields = ('url', 'name', 'name2', 'targets', 'through')
class ManyToManyThroughTargetSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = ManyToManyThroughTarget
fields = ('url', 'name', 'sources', 'through')
class ManyToManyThroughSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = ManyToManyThrough
fields = ('url', 'name', 'source', 'target')
# ForeignKey
class ForeignKeyTargetSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
@ -188,6 +211,64 @@ class HyperlinkedManyToManyTests(TestCase):
self.assertEqual(serializer.data, expected)
class HyperlinkedManyToManyThroughTests(TestCase):
urls = 'tests.test_relations_hyperlink'
def setUp(self):
through_idx = 1
for idx in range(1, 4):
source = ManyToManyThroughSource(name='source-%d' % idx,
name2='source-%d' % idx)
source.save()
for idx in range(4, 7):
target = ManyToManyThroughTarget(name='target-%d' % idx)
target.save()
for (s, t) in [(1, 4), (2, 4), (2, 5), (3, 4), (3, 5), (3, 6)]:
source = ManyToManyThroughSource.objects.get(pk=s)
target = ManyToManyThroughTarget.objects.get(pk=t)
through = ManyToManyThrough(name='through-%d' % through_idx,
source=source, target=target)
through.save()
through_idx += 1
def test_many_to_many_retrieve(self):
queryset = ManyToManyThroughSource.objects.all()
serializer = ManyToManyThroughSourceSerializer(queryset, many=True, context={'request': request})
expected = [
{'url': 'http://testserver/manytomanythroughsource/1/', 'name': 'source-1', 'name2': 'source-1', 'targets': ['http://testserver/manytomanythroughtarget/4/'],
'through': ['http://testserver/manytomanythrough/1/']},
{'url': 'http://testserver/manytomanythroughsource/2/', 'name': 'source-2', 'name2': 'source-2', 'targets': ['http://testserver/manytomanythroughtarget/4/', 'http://testserver/manytomanythroughtarget/5/'],
'through': ['http://testserver/manytomanythrough/2/', 'http://testserver/manytomanythrough/3/']},
{'url': 'http://testserver/manytomanythroughsource/3/', 'name': 'source-3', 'name2': 'source-3', 'targets': ['http://testserver/manytomanythroughtarget/4/', 'http://testserver/manytomanythroughtarget/5/', 'http://testserver/manytomanythroughtarget/6/'],
'through': ['http://testserver/manytomanythrough/4/', 'http://testserver/manytomanythrough/5/', 'http://testserver/manytomanythrough/6/']}
]
with self.assertNumQueries(7):
self.assertEqual(serializer.data, expected)
def test_many_to_many_retrieve_prefetch_related(self):
queryset = ManyToManyThroughSource.objects.all().prefetch_related('targets').prefetch_related('through')
serializer = ManyToManyThroughSourceSerializer(queryset, many=True, context={'request': request})
with self.assertNumQueries(3):
serializer.data
def test_reverse_many_to_many_retrieve(self):
queryset = ManyToManyThroughTarget.objects.all()
serializer = ManyToManyThroughTargetSerializer(queryset, many=True, context={'request': request})
expected = [
{'url': 'http://testserver/manytomanythroughtarget/1/', 'name': 'source-1', 'sources': [], 'through': []},
{'url': 'http://testserver/manytomanythroughtarget/2/', 'name': 'source-2', 'sources': [], 'through': []},
{'url': 'http://testserver/manytomanythroughtarget/3/', 'name': 'source-3', 'sources': [], 'through': []},
{'url': 'http://testserver/manytomanythroughtarget/4/', 'name': 'target-4', 'sources': ['http://testserver/manytomanythroughsource/1/', 'http://testserver/manytomanythroughsource/2/', 'http://testserver/manytomanythroughsource/3/'],
'through': ['http://testserver/manytomanythrough/1/', 'http://testserver/manytomanythrough/2/', 'http://testserver/manytomanythrough/4/']},
{'url': 'http://testserver/manytomanythroughtarget/5/', 'name': 'target-5', 'sources': ['http://testserver/manytomanythroughsource/2/', 'http://testserver/manytomanythroughsource/3/'],
'through': ['http://testserver/manytomanythrough/3/', 'http://testserver/manytomanythrough/5/']},
{'url': 'http://testserver/manytomanythroughtarget/6/', 'name': 'target-6', 'sources': ['http://testserver/manytomanythroughsource/3/'],
'through': ['http://testserver/manytomanythrough/6/']}
]
with self.assertNumQueries(13):
self.assertEqual(serializer.data, expected)
class HyperlinkedForeignKeyTests(TestCase):
urls = 'tests.test_relations_hyperlink'

View File

@ -6,6 +6,7 @@ from django.utils import six
from rest_framework import serializers
from tests.models import (
ForeignKeySource, ForeignKeyTarget, ManyToManySource, ManyToManyTarget,
ManyToManyThrough, ManyToManyThroughSource, ManyToManyThroughTarget,
NullableForeignKeySource, NullableOneToOneSource, OneToOneTarget
)
@ -23,6 +24,25 @@ class ManyToManySourceSerializer(serializers.ModelSerializer):
fields = ('id', 'name', 'targets')
# ManyToMany with inheritance and a through model
class ManyToManyThroughSourceSerializer(serializers.ModelSerializer):
class Meta:
model = ManyToManyThroughSource
fields = ('id', 'name', 'name2', 'targets', 'through')
class ManyToManyThroughTargetSerializer(serializers.ModelSerializer):
class Meta:
model = ManyToManyThroughTarget
fields = ('id', 'name', 'sources', 'through')
class ManyToManyThroughSerializer(serializers.ModelSerializer):
class Meta:
model = ManyToManyThrough
fields = ('id', 'name', 'source', 'target')
# ForeignKey
class ForeignKeyTargetSerializer(serializers.ModelSerializer):
class Meta:
@ -175,6 +195,60 @@ class PKManyToManyTests(TestCase):
self.assertEqual(serializer.data, expected)
class PKManyToManyThroughTests(TestCase):
def setUp(self):
through_idx = 1
for idx in range(1, 4):
source = ManyToManyThroughSource(name='source-%d' % idx,
name2='source-%d' % idx)
source.save()
for idx in range(4, 7):
target = ManyToManyThroughTarget(name='target-%d' % idx)
target.save()
for (s, t) in [(1, 4), (2, 4), (2, 5), (3, 4), (3, 5), (3, 6)]:
source = ManyToManyThroughSource.objects.get(pk=s)
target = ManyToManyThroughTarget.objects.get(pk=t)
through = ManyToManyThrough(name='through-%d' % through_idx,
source=source, target=target)
through.save()
through_idx += 1
def test_many_to_many_retrieve(self):
queryset = ManyToManyThroughSource.objects.all()
serializer = ManyToManyThroughSourceSerializer(queryset, many=True)
expected = [
{'id': 1, 'name': 'source-1', 'name2': 'source-1',
'targets': [4], 'through': [1]},
{'id': 2, 'name': 'source-2', 'name2': 'source-2',
'targets': [4, 5], 'through': [2, 3]},
{'id': 3, 'name': 'source-3', 'name2': 'source-3',
'targets': [4, 5, 6], 'through': [4, 5, 6]}
]
with self.assertNumQueries(7):
self.assertEqual(serializer.data, expected)
def test_many_to_many_retrieve_prefetch_related(self):
queryset = ManyToManyThroughSource.objects.all().prefetch_related('targets').prefetch_related('through')
serializer = ManyToManyThroughSourceSerializer(queryset, many=True)
with self.assertNumQueries(3):
serializer.data
def test_reverse_many_to_many_retrieve(self):
queryset = ManyToManyThroughTarget.objects.all()
serializer = ManyToManyThroughTargetSerializer(queryset, many=True)
expected = [
{'id': 1, 'name': 'source-1', 'sources': [], 'through': []},
{'id': 2, 'name': 'source-2', 'sources': [], 'through': []},
{'id': 3, 'name': 'source-3', 'sources': [], 'through': []},
{'id': 4, 'name': 'target-4', 'sources': [1, 2, 3],
'through': [1, 2, 4]},
{'id': 5, 'name': 'target-5', 'sources': [2, 3], 'through': [3, 5]},
{'id': 6, 'name': 'target-6', 'sources': [3], 'through': [6]}
]
with self.assertNumQueries(13):
self.assertEqual(serializer.data, expected)
class PKForeignKeyTests(TestCase):
def setUp(self):
target = ForeignKeyTarget(name='target-1')