From cc4cb95beff0b75ec169add7e94cc481624a41e6 Mon Sep 17 00:00:00 2001 From: Martin Svoboda Date: Wed, 11 Aug 2021 16:42:43 +0200 Subject: [PATCH] Fixed #33008 -- Fixed prefetch_related() for deleted GenericForeignKeys. Thanks Simon Charette for the implementation idea. --- django/contrib/contenttypes/fields.py | 4 +++- tests/contenttypes_tests/test_fields.py | 19 ++++++++++++++++--- tests/prefetch_related/tests.py | 18 ++++++++++++++++++ 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/django/contrib/contenttypes/fields.py b/django/contrib/contenttypes/fields.py index fbd55606ae3..63ee408bf53 100644 --- a/django/contrib/contenttypes/fields.py +++ b/django/contrib/contenttypes/fields.py @@ -213,7 +213,7 @@ class GenericForeignKey(FieldCacheMixin): gfk_key, True, self.name, - True, + False, ) def __get__(self, instance, cls=None): @@ -229,6 +229,8 @@ class GenericForeignKey(FieldCacheMixin): pk_val = getattr(instance, self.fk_field) rel_obj = self.get_cached_value(instance, default=None) + if rel_obj is None and self.is_cached(instance): + return rel_obj if rel_obj is not None: ct_match = ct_id == self.get_content_type(obj=rel_obj, using=instance._state.db).id pk_match = rel_obj._meta.pk.to_python(pk_val) == rel_obj.pk diff --git a/tests/contenttypes_tests/test_fields.py b/tests/contenttypes_tests/test_fields.py index ce5e244df54..5638846cc5b 100644 --- a/tests/contenttypes_tests/test_fields.py +++ b/tests/contenttypes_tests/test_fields.py @@ -2,14 +2,14 @@ import json from django.contrib.contenttypes.fields import GenericForeignKey from django.db import models -from django.test import SimpleTestCase, TestCase +from django.test import TestCase from django.test.utils import isolate_apps -from .models import Answer, Question +from .models import Answer, Post, Question @isolate_apps('contenttypes_tests') -class GenericForeignKeyTests(SimpleTestCase): +class GenericForeignKeyTests(TestCase): def test_str(self): class Model(models.Model): @@ -24,6 +24,19 @@ class GenericForeignKeyTests(SimpleTestCase): with self.assertRaisesMessage(ValueError, "Custom queryset can't be used for this lookup."): Answer.question.get_prefetch_queryset(Answer.objects.all(), Answer.objects.all()) + def test_get_object_cache_respects_deleted_objects(self): + question = Question.objects.create(text='Who?') + post = Post.objects.create(title='Answer', parent=question) + + question_pk = question.pk + Question.objects.all().delete() + + post = Post.objects.get(pk=post.pk) + with self.assertNumQueries(1): + self.assertEqual(post.object_id, question_pk) + self.assertIsNone(post.parent) + self.assertIsNone(post.parent) + class GenericRelationTests(TestCase): diff --git a/tests/prefetch_related/tests.py b/tests/prefetch_related/tests.py index cba1897fc5d..242ff6c3669 100644 --- a/tests/prefetch_related/tests.py +++ b/tests/prefetch_related/tests.py @@ -1033,6 +1033,24 @@ class GenericRelationTests(TestCase): # instance returned by the manager. self.assertEqual(list(bookmark.tags.all()), list(bookmark.tags.all().all())) + def test_deleted_GFK(self): + TaggedItem.objects.create(tag='awesome', content_object=self.book1) + TaggedItem.objects.create(tag='awesome', content_object=self.book2) + ct = ContentType.objects.get_for_model(Book) + + book1_pk = self.book1.pk + self.book1.delete() + + with self.assertNumQueries(2): + qs = TaggedItem.objects.filter(tag='awesome').prefetch_related('content_object') + result = [ + (tag.object_id, tag.content_type_id, tag.content_object) for tag in qs + ] + self.assertEqual(result, [ + (book1_pk, ct.pk, None), + (self.book2.pk, ct.pk, self.book2), + ]) + class MultiTableInheritanceTest(TestCase):