From 4e6bfc40dc6006b22e9bfdf8768522e0039ed33e Mon Sep 17 00:00:00 2001 From: Baptiste Jonglez Date: Tue, 9 Feb 2016 07:58:39 +0100 Subject: [PATCH] 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 322bda815969a49140ed02cab304e13e63b059d5 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. --- tests/models.py | 18 +++++++ tests/test_relations_hyperlink.py | 81 +++++++++++++++++++++++++++++++ tests/test_relations_pk.py | 74 ++++++++++++++++++++++++++++ 3 files changed, 173 insertions(+) diff --git a/tests/models.py b/tests/models.py index 8ec274d8b..8063b3b4c 100644 --- a/tests/models.py +++ b/tests/models.py @@ -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) diff --git a/tests/test_relations_hyperlink.py b/tests/test_relations_hyperlink.py index c0642eda2..a0eb097b5 100644 --- a/tests/test_relations_hyperlink.py +++ b/tests/test_relations_hyperlink.py @@ -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[0-9]+)/$', dummy_view, name='dummy-url'), url(r'^manytomanysource/(?P[0-9]+)/$', dummy_view, name='manytomanysource-detail'), url(r'^manytomanytarget/(?P[0-9]+)/$', dummy_view, name='manytomanytarget-detail'), + url(r'^manytomanythroughsource/(?P[0-9]+)/$', dummy_view, name='manytomanythroughsource-detail'), + url(r'^manytomanythroughtarget/(?P[0-9]+)/$', dummy_view, name='manytomanythroughtarget-detail'), + url(r'^manytomanythrough/(?P[0-9]+)/$', dummy_view, name='manytomanythrough-detail'), url(r'^foreignkeysource/(?P[0-9]+)/$', dummy_view, name='foreignkeysource-detail'), url(r'^foreignkeytarget/(?P[0-9]+)/$', dummy_view, name='foreignkeytarget-detail'), url(r'^nullableforeignkeysource/(?P[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' diff --git a/tests/test_relations_pk.py b/tests/test_relations_pk.py index 169f7d9c5..44d78c49e 100644 --- a/tests/test_relations_pk.py +++ b/tests/test_relations_pk.py @@ -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')