Fixed #33450 -- Fixed RelatedExact lookups for GenericRelation and GenericRel with custom primary key types.

This commit is contained in:
Clifford Gama 2025-03-21 19:23:19 +02:00
parent 1823a80113
commit 381c61096b
No known key found for this signature in database
GPG key ID: BF895AA45E520E21
5 changed files with 69 additions and 5 deletions

View file

@ -288,6 +288,13 @@ class GenericRel(ForeignObjectRel):
on_delete=DO_NOTHING,
)
def get_effective_target_field(self):
"""
Return the field used to prepare lookup values when this relation is
the target of a related exact lookup.
"""
return self.path_infos[0].from_opts.pk
class GenericRelation(ForeignObject):
"""
@ -483,6 +490,13 @@ class GenericRelation(ForeignObject):
qs = getattr(obj, self.name).all()
return str([instance.pk for instance in qs])
def get_effective_target_field(self):
"""
Return the field used to prepare lookup values when this relation is
the target of a related exact lookup.
"""
return self.path_infos[0].from_opts.pk
def contribute_to_class(self, cls, name, **kwargs):
kwargs["private_only"] = True
super().contribute_to_class(cls, name, **kwargs)

View file

@ -106,9 +106,16 @@ class RelatedLookupMixin:
# doesn't have validation for non-integers, so we must run validation
# using the target field.
if self.prepare_rhs and hasattr(self.lhs.output_field, "path_infos"):
# Get the target field. We can safely assume there is only one
# as we don't get to the direct value branch otherwise.
target_field = self.lhs.output_field.path_infos[-1].target_fields[-1]
if hasattr(self.lhs.output_field, "get_effective_target_field"):
# If the output_field specifies a field to prepare the rhs with,
# use it.
target_field = self.lhs.output_field.get_effective_target_field()
else:
# Get the target field. We can safely assume there is only one
# as we don't get to the direct value branch otherwise.
target_field = self.lhs.output_field.path_infos[-1].target_fields[
-1
]
self.rhs = target_field.get_prep_value(self.rhs)
return super().get_prep_lookup()

View file

@ -255,8 +255,12 @@ class FieldGetDbPrepValueMixin:
def get_db_prep_lookup(self, value, connection):
# For relational fields, use the 'target_field' attribute of the
# output_field.
field = getattr(self.lhs.output_field, "target_field", None)
# output_field. If the field provides a get_effective_target_field
# method, use that.
if hasattr(self.lhs.output_field, "get_effective_target_field"):
field = self.lhs.output_field.get_effective_target_field()
else:
field = getattr(self.lhs.output_field, "target_field", None)
get_db_prep_value = (
getattr(field, "get_db_prep_value", None)
or self.lhs.output_field.get_db_prep_value

View file

@ -150,3 +150,25 @@ class AllowsNullGFK(models.Model):
content_type = models.ForeignKey(ContentType, models.SET_NULL, null=True)
object_id = models.PositiveIntegerField(null=True)
content_object = GenericForeignKey()
class Note(models.Model):
title = models.CharField(max_length=30)
content_type = models.ForeignKey(
ContentType, on_delete=models.CASCADE, related_name="notes"
)
object_id = models.CharField(max_length=36)
owner = GenericForeignKey()
class Story(models.Model):
name = models.CharField(max_length=30, primary_key=True)
notes = GenericRelation("Note", related_query_name="story")
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, null=True)
object_id = models.PositiveIntegerField(null=True)
inspiration = GenericForeignKey()
class Idea(models.Model):
description = models.TextField()
stories = GenericRelation(Story)

View file

@ -13,10 +13,13 @@ from .models import (
ForConcreteModelModel,
ForProxyModelModel,
Gecko,
Idea,
ManualPK,
Mineral,
Note,
ProxyRelatedModel,
Rock,
Story,
TaggedItem,
ValuableRock,
ValuableTaggedItem,
@ -860,3 +863,17 @@ class TestInitWithNoneArgument(SimpleTestCase):
# TaggedItem requires a content_type but initializing with None should
# be allowed.
TaggedItem(content_object=None)
class GenericRelationQueryTests(TestCase):
def test_generic_relation_related_exact_lookup_incompatible_pk_types(self):
story = Story.objects.create(name="The Hobbit")
note1 = Note.objects.create(title="Fantastic fantasy", owner=story)
self.assertSequenceEqual(Note.objects.filter(story=story), [note1])
self.assertSequenceEqual(Note.objects.filter(story=story.pk), [note1])
def test_generic_relation_reverse_related_exact_lookup_incompatible_pk_types(self):
idea = Idea.objects.create(description="Eureka")
story = Story.objects.create(name="Bathtime Serendipity", inspiration=idea)
self.assertSequenceEqual(Idea.objects.filter(stories=story), [idea])
self.assertSequenceEqual(Idea.objects.filter(stories=story.pk), [idea])