Skip to content

Commit dbfcb86

Browse files
committed
feat: add support to convert from normal manager/qs to poly versions
1 parent f115219 commit dbfcb86

File tree

5 files changed

+136
-5
lines changed

5 files changed

+136
-5
lines changed

docs/advanced.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,13 @@ About Queryset Methods
177177
relation = models.ForeignKey(BasePolyModel, on_delete=models.CASCADE)
178178
objects = models.Manager.from_queryset(PolymorphicRelatedQuerySet)()
179179

180+
or by converting a models queryset using
181+
182+
class NonPolyModel(models.Model):
183+
relation = models.ForeignKey(BasePolyModel, on_delete=models.CASCADE)
184+
objects = models.Manager.from_queryset(QuerySet)()
185+
186+
``convert_to_polymorphic_queryset(NonPolyModel.objects).filter(...)``
180187

181188
To select related fields the model name comes after the field name and set the
182189
field.

polymorphic/query.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
from django.contrib.contenttypes.models import ContentType
1111
from django.core.exceptions import FieldDoesNotExist
12-
from django.db.models import FilteredRelation
12+
from django.db.models import FilteredRelation, Manager
1313
from django.db.models.constants import LOOKUP_SEP
1414
from django.db.models.query import ModelIterable, Q, QuerySet
1515
from django.db.models.query import BaseIterable, RelatedPopulator
@@ -1015,3 +1015,36 @@ def _get_real_instances(self, base_result_objects):
10151015

10161016
class PolymorphicRelatedQuerySet(PolymorphicRelatedQuerySetMixin, QuerySet):
10171017
pass
1018+
1019+
1020+
def convert_to_polymorphic_queryset(qs):
1021+
"Convert a queryset to one that support polymorphic evaluation"
1022+
1023+
if isinstance(qs, Manager):
1024+
qs = qs.get_queryset()
1025+
1026+
if issubclass(qs.__class__, PolymorphicQuerySetMixin):
1027+
return qs
1028+
1029+
assert issubclass(QuerySet, qs.__class__), (
1030+
"PolymorphicModel: cannot guarantee conversion of"
1031+
f" {qs.__class__} to polymorphic queryset")
1032+
1033+
class RelatedPolyQuerySet(PolymorphicRelatedQuerySetMixin, qs.__class__):
1034+
@classmethod
1035+
def _convert_to(cls, qs):
1036+
c = cls(
1037+
model=qs.model,
1038+
query=qs.query.chain(),
1039+
using=qs._db,
1040+
hints=qs._hints,
1041+
)
1042+
c._sticky_filter = qs._sticky_filter
1043+
c._for_write = qs._for_write
1044+
c._prefetch_related_lookups = qs._prefetch_related_lookups[:]
1045+
c._known_related_objects = qs._known_related_objects
1046+
c._fields = qs._fields
1047+
return c
1048+
1049+
poly_qs = RelatedPolyQuerySet._convert_to(qs)
1050+
return poly_qs

polymorphic/tests/migrations/0001_initial.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2344,4 +2344,22 @@ class Migration(migrations.Migration):
23442344
),
23452345
],
23462346
),
2347+
migrations.CreateModel(
2348+
name="VanillaPlainModel",
2349+
fields=[
2350+
(
2351+
"id",
2352+
models.AutoField(
2353+
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
2354+
),
2355+
),
2356+
(
2357+
"relation",
2358+
models.ForeignKey(
2359+
on_delete=django.db.models.deletion.CASCADE, to="tests.parentmodel"
2360+
),
2361+
),
2362+
],
2363+
),
2364+
23472365
]

polymorphic/tests/models.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -554,10 +554,13 @@ class PlainModel(models.Model):
554554
relation = models.ForeignKey(ParentModel, on_delete=models.CASCADE)
555555
objects = models.Manager.from_queryset(PolymorphicRelatedQuerySet)()
556556

557+
class VanillaPlainModel(models.Model):
558+
relation = models.ForeignKey(ParentModel, on_delete=models.CASCADE)
557559

558560
class RefPlainModel(models.Model):
559561
plainobj = models.ForeignKey(PlainModel, on_delete=models.CASCADE)
560-
objects = models.Manager.from_queryset(PolymorphicRelatedQuerySet)()
562+
objects = models.Manager.from_queryset(QuerySet)()
563+
poly_objects = models.Manager.from_queryset(PolymorphicRelatedQuerySet)()
561564

562565

563566
class PlainModelWithM2M(models.Model):

polymorphic/tests/test_orm.py

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from django.db.utils import IntegrityError
1111
from django.test import TransactionTestCase
1212
from polymorphic import query_translate
13+
from polymorphic.query import convert_to_polymorphic_queryset
1314
from polymorphic.managers import PolymorphicManager
1415
from polymorphic.models import PolymorphicTypeInvalid, PolymorphicTypeUndefined
1516
from polymorphic.tests.models import (
@@ -101,6 +102,7 @@
101102
UUIDPlainC,
102103
UUIDProject,
103104
UUIDResearchProject,
105+
VanillaPlainModel,
104106
)
105107

106108

@@ -1428,6 +1430,74 @@ def test_select_related_on_poly_classes_simple(self):
14281430
self.assertIsInstance(obj_list[2].relation, AltChildModel)
14291431
self.assertIsInstance(obj_list[3].relation, AltChildModel)
14301432

1433+
def test_we_can_upgrade_a_query_set_to_polymorphic_supports_already_ploy_qs(self):
1434+
base_qs = RefPlainModel.poly_objects.get_queryset()
1435+
self.assertIs(convert_to_polymorphic_queryset(base_qs), base_qs)
1436+
1437+
def test_we_can_upgrade_a_query_set_to_polymorphic_supports_non_ploy_qs_on_ploy_object(self):
1438+
base_qs = RefPlainModel.objects.get_queryset()
1439+
self.assertIsNot(convert_to_polymorphic_queryset(base_qs), base_qs)
1440+
self.assertIsInstance(convert_to_polymorphic_queryset(base_qs), PolymorphicRelatedQuerySetMixin)
1441+
1442+
def test_we_can_upgrade_a_query_set_to_polymorphic_supports_non_ploy_managers_on_ploy_object(self):
1443+
base_qs = RefPlainModel.objects
1444+
self.assertIsNot(convert_to_polymorphic_queryset(base_qs), base_qs)
1445+
self.assertIsInstance(convert_to_polymorphic_queryset(base_qs), PolymorphicRelatedQuerySetMixin)
1446+
1447+
def test_we_can_upgrade_a_query_set_to_polymorphic(self):
1448+
# can we fetch the related object but only the minimal 'common' values
1449+
plain_a_obj_1 = PlainA.objects.create(field1="f1")
1450+
plain_a_obj_2 = PlainA.objects.create(field1="f2")
1451+
extra_obj = ModelExtraExternal.objects.create(topic="t1")
1452+
obj_p = ParentModel.objects.create(name="p1")
1453+
obj_c = ChildModel.objects.create(name="c1", other_name="c1name", link_on_child=extra_obj)
1454+
obj_ac1 = AltChildModel.objects.create(
1455+
name="ac1", other_name="ac1name", link_on_altchild=plain_a_obj_1
1456+
)
1457+
obj_ac2 = AltChildModel.objects.create(
1458+
name="ac2", other_name="ac2name", link_on_altchild=plain_a_obj_2
1459+
)
1460+
obj_p_1 = VanillaPlainModel.objects.create(relation=obj_p)
1461+
obj_p_2 = VanillaPlainModel.objects.create(relation=obj_c)
1462+
obj_p_3 = VanillaPlainModel.objects.create(relation=obj_ac1)
1463+
obj_p_4 = VanillaPlainModel.objects.create(relation=obj_ac2)
1464+
1465+
with self.assertNumQueries(1):
1466+
# pos 3 if i cannot do optimized select_related
1467+
obj_list = list(
1468+
VanillaPlainModel.objects.order_by("pk")
1469+
)
1470+
1471+
with self.assertNumQueries(7):
1472+
self.assertEqual(obj_list[0].relation.name, "p1")
1473+
self.assertEqual(obj_list[1].relation.name, "c1")
1474+
self.assertEqual(obj_list[2].relation.name, "ac1")
1475+
self.assertEqual(obj_list[3].relation.name, "ac2")
1476+
1477+
with self.assertNumQueries(1):
1478+
# pos 3 if i cannot do optimized select_related
1479+
obj_list = list(
1480+
convert_to_polymorphic_queryset(
1481+
VanillaPlainModel.objects
1482+
).select_related(
1483+
"relation",
1484+
"relation__childmodel",
1485+
"relation__altchildmodel",
1486+
)
1487+
.order_by("pk")
1488+
)
1489+
1490+
with self.assertNumQueries(0):
1491+
self.assertEqual(obj_list[0].relation.name, "p1")
1492+
self.assertEqual(obj_list[1].relation.name, "c1")
1493+
self.assertEqual(obj_list[2].relation.name, "ac1")
1494+
self.assertEqual(obj_list[3].relation.name, "ac2")
1495+
1496+
self.assertIsInstance(obj_list[0].relation, ParentModel)
1497+
self.assertIsInstance(obj_list[1].relation, ChildModel)
1498+
self.assertIsInstance(obj_list[2].relation, AltChildModel)
1499+
self.assertIsInstance(obj_list[3].relation, AltChildModel)
1500+
14311501
def test_select_related_on_poly_classes_indirect_related(self):
14321502
# can we fetch the related object but only the minimal 'common' values
14331503
plain_a_obj_1 = PlainA.objects.create(field1="f1")
@@ -1457,7 +1527,7 @@ def test_select_related_on_poly_classes_indirect_related(self):
14571527
with self.assertNumQueries(1):
14581528
# pos 3 if i cannot do optimized select_related
14591529
obj_list = list(
1460-
RefPlainModel.objects.select_related(
1530+
RefPlainModel.poly_objects.select_related(
14611531
# "plainobj__relation",
14621532
"plainobj__relation",
14631533
"plainobj__relation__childmodel__link_on_child",
@@ -1504,7 +1574,7 @@ def test_select_related_on_poly_classes_indirect_related(self):
15041574
with self.assertNumQueries(1):
15051575
# pos 3 if i cannot do optimized select_related
15061576
obj_list = list(
1507-
RefPlainModel.objects.select_related(
1577+
RefPlainModel.poly_objects.select_related(
15081578
# "plainobj__relation",
15091579
"plainobj__relation",
15101580
"plainobj__relation__childmodel__link_on_child",
@@ -1554,7 +1624,7 @@ def test_select_related_fecth_all_poly_classes_indirect_related(self):
15541624
with self.assertNumQueries(1):
15551625
# pos 3 if i cannot do optimized select_related
15561626
obj_list = list(
1557-
RefPlainModel.objects.select_related(
1627+
RefPlainModel.poly_objects.select_related(
15581628
# "plainobj__relation",
15591629
"plainobj__relation",
15601630
"plainobj__relation__*",

0 commit comments

Comments
 (0)