Skip to content

Commit 7fca75a

Browse files
committed
Add option to model meta class to prevent proxy models being polymorphic
Fixes #376 #390
1 parent 3312f5f commit 7fca75a

File tree

5 files changed

+208
-5
lines changed

5 files changed

+208
-5
lines changed

polymorphic/base.py

+11
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ class PolymorphicModelBase(ModelBase):
5353
"""
5454

5555
def __new__(self, model_name, bases, attrs, **kwargs):
56+
polymorphic__proxy = None
57+
if "Meta" in attrs:
58+
if hasattr(attrs["Meta"], "polymorphic__proxy"):
59+
polymorphic__proxy = attrs["Meta"].polymorphic__proxy
60+
del attrs["Meta"].polymorphic__proxy
61+
5662
# Workaround compatibility issue with six.with_metaclass() and custom Django model metaclasses:
5763
if not attrs and model_name == "NewBase":
5864
return super().__new__(self, model_name, bases, attrs, **kwargs)
@@ -70,6 +76,11 @@ def __new__(self, model_name, bases, attrs, **kwargs):
7076
# for __init__ function of this class (monkeypatching inheritance accessors)
7177
new_class.polymorphic_super_sub_accessors_replaced = False
7278

79+
if polymorphic__proxy is not None:
80+
new_class._meta.polymorphic__proxy = polymorphic__proxy
81+
else:
82+
new_class._meta.polymorphic__proxy = not new_class._meta.proxy
83+
7384
# determine the name of the primary key field and store it into the class variable
7485
# polymorphic_primary_key_name (it is needed by query.py)
7586
for f in new_class._meta.fields:

polymorphic/managers.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def from_queryset(cls, queryset_class, class_name=None):
2828

2929
def get_queryset(self):
3030
qs = self.queryset_class(self.model, using=self._db, hints=self._hints)
31-
if self.model._meta.proxy:
31+
if not self.model._meta.polymorphic__proxy:
3232
qs = qs.instance_of(self.model)
3333
return qs
3434

polymorphic/tests/migrations/0001_initial.py

+78
Original file line numberDiff line numberDiff line change
@@ -1510,6 +1510,84 @@ class Migration(migrations.Migration):
15101510
options={"proxy": True},
15111511
bases=("tests.proxybase",),
15121512
),
1513+
migrations.CreateModel(
1514+
name="AliasProxyChild",
1515+
fields=[],
1516+
options={"proxy": True},
1517+
bases=("tests.proxybase",),
1518+
),
1519+
migrations.CreateModel(
1520+
name="NonProxyChildAliasProxy",
1521+
fields=[],
1522+
options={"proxy": True},
1523+
bases=("tests.nonproxychild",),
1524+
),
1525+
migrations.CreateModel(
1526+
name='AliasOfNonProxyChild',
1527+
fields=[
1528+
],
1529+
options={
1530+
'proxy': True,
1531+
'indexes': [],
1532+
'constraints': [],
1533+
},
1534+
bases=('tests.nonproxychild',),
1535+
),
1536+
migrations.CreateModel(
1537+
name='NonAliasNonProxyChild',
1538+
fields=[
1539+
],
1540+
options={
1541+
'proxy': True,
1542+
'indexes': [],
1543+
'constraints': [],
1544+
},
1545+
bases=('tests.nonproxychild',),
1546+
),
1547+
migrations.CreateModel(
1548+
name='TradProxyChild',
1549+
fields=[
1550+
],
1551+
options={
1552+
'proxy': True,
1553+
'indexes': [],
1554+
'constraints': [],
1555+
},
1556+
bases=('tests.proxybase',),
1557+
),
1558+
migrations.CreateModel(
1559+
name='TradProxyOnProxyChild',
1560+
fields=[
1561+
],
1562+
options={
1563+
'proxy': True,
1564+
'indexes': [],
1565+
'constraints': [],
1566+
},
1567+
bases=('tests.proxychild',),
1568+
),
1569+
migrations.CreateModel(
1570+
name='PolyTradProxyChild',
1571+
fields=[
1572+
],
1573+
options={
1574+
'proxy': True,
1575+
'indexes': [],
1576+
'constraints': [],
1577+
},
1578+
bases=('tests.tradproxychild',),
1579+
),
1580+
migrations.CreateModel(
1581+
name='ProxyChildAliasProxy',
1582+
fields=[
1583+
],
1584+
options={
1585+
'proxy': True,
1586+
'indexes': [],
1587+
'constraints': [],
1588+
},
1589+
bases=('tests.tradproxychild',),
1590+
),
15131591
migrations.CreateModel(
15141592
name="ProxyModelBase",
15151593
fields=[],

polymorphic/tests/models.py

+54-2
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ class UUIDPlainC(UUIDPlainB):
329329
field3 = models.CharField(max_length=10)
330330

331331

332-
# base -> proxy
332+
# base(poly) -> proxy
333333

334334

335335
class ProxyBase(PolymorphicModel):
@@ -345,7 +345,59 @@ class NonProxyChild(ProxyBase):
345345
name = models.CharField(max_length=10)
346346

347347

348-
# base -> proxy -> real models
348+
# A traditional django proxy models. ie proxy'ed class is alias class
349+
# but in django_polymorphic this is not so.
350+
#
351+
# We have model types :-
352+
# base(poly) / child(poly) : A concrete polymorphic model 1+ fields
353+
# base(non ploy) : A concrete django model 1+ fields
354+
# proxy(poly) : A proxy model where it considered different
355+
# : from it superclasses
356+
# proxy(Traditional Django) : A proxy model where it is an alias for the
357+
# : underline model
358+
359+
360+
# base(poly) -> proxy(poly) -> proxy(Traditional Django)
361+
class TradProxyOnProxyChild(ProxyChild):
362+
class Meta:
363+
proxy = True
364+
polymorphic__proxy = True
365+
366+
367+
# base(poly) -> proxy(Traditional Django)
368+
class TradProxyChild(ProxyBase):
369+
class Meta:
370+
proxy = True
371+
polymorphic__proxy = True
372+
373+
# base(poly) -> proxy(Traditional Django) -> proxy(poly)
374+
# Not really helpful model as reduces to base(poly) -> proxy(poly)
375+
376+
# base(poly) -> child(poly) -> proxy(Traditional Django)
377+
class AliasOfNonProxyChild(NonProxyChild):
378+
class Meta:
379+
proxy = True
380+
polymorphic__proxy = True
381+
382+
383+
# base(poly) -> proxy(Traditional Django) -> proxy(poly)
384+
class ProxyChildAliasProxy(TradProxyChild):
385+
class Meta:
386+
proxy = True
387+
388+
389+
# base(poly) -> proxy(poly)
390+
class AliasProxyChild(ProxyBase):
391+
class Meta:
392+
proxy = True
393+
polymorphic__proxy = True
394+
395+
396+
# base(poly) -> proxy(poly)
397+
class NonAliasNonProxyChild(NonProxyChild):
398+
class Meta:
399+
proxy = True
400+
polymorphic__proxy = False
349401

350402

351403
class ProxiedBase(ShowFieldTypeAndContent, PolymorphicModel):

polymorphic/tests/test_orm.py

+64-2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from polymorphic.managers import PolymorphicManager
1313
from polymorphic.models import PolymorphicTypeInvalid, PolymorphicTypeUndefined
1414
from polymorphic.tests.models import (
15+
AliasProxyChild,
1516
ArtProject,
1617
Base,
1718
BlogA,
@@ -88,6 +89,13 @@
8889
UUIDPlainC,
8990
UUIDProject,
9091
UUIDResearchProject,
92+
93+
NonAliasNonProxyChild,
94+
TradProxyOnProxyChild,
95+
TradProxyChild,
96+
AliasOfNonProxyChild,
97+
ProxyChildAliasProxy,
98+
9199
)
92100

93101

@@ -859,6 +867,60 @@ def test_queryset_on_proxy_model_does_not_return_superclasses(self):
859867
assert ProxyBase.objects.count() == 5
860868
assert ProxyChild.objects.count() == 3
861869

870+
def test_queryset_on_polymorphic_proxy_model_returns_superclasses(self):
871+
ProxyBase.objects.create(some_data="Base1")
872+
AliasProxyChild.objects.create(some_data="ProxyChild1")
873+
AliasProxyChild.objects.create(some_data="ProxyChild2")
874+
ProxyChild.objects.create(some_data="PolyChild1")
875+
NonAliasNonProxyChild.objects.create(some_data="SubChild1")
876+
NonAliasNonProxyChild.objects.create(some_data="SubChild2")
877+
NonProxyChild.objects.create(some_data="NonProxChild1", name="t1")
878+
879+
with self.subTest(" superclasses"):
880+
self.assertEqual(7, ProxyBase.objects.count())
881+
self.assertEqual(7, AliasProxyChild.objects.count())
882+
with self.subTest("only compete classes"):
883+
# Non proxy models should not return the proxy siblings
884+
self.assertEqual(1, ProxyChild.objects.count())
885+
self.assertEqual(2, NonAliasNonProxyChild.objects.count())
886+
self.assertEqual(3, NonProxyChild.objects.count())
887+
888+
def test_polymorphic_proxy_object_has_different_ctype_from_base(self):
889+
obj1 = ProxyBase.objects.create(some_data="Base1")
890+
obj2 = AliasProxyChild.objects.create(some_data="ProxyChild1")
891+
obj1_ctype = ContentType.objects.get_for_model(
892+
obj1, for_concrete_model=False)
893+
obj2_ctype = ContentType.objects.get_for_model(
894+
obj2, for_concrete_model=False)
895+
self.assertNotEqual(obj1_ctype, obj2_ctype)
896+
897+
def test_can_create_django_style_proxy_classes_alias(self):
898+
ProxyBase.objects.create(some_data="Base1")
899+
TradProxyChild.objects.create(some_data="Base2")
900+
self.assertEqual(2, ProxyBase.objects.count())
901+
self.assertEqual(2, TradProxyChild.objects.count())
902+
TradProxyOnProxyChild.objects.create()
903+
904+
def test_convert_back_to_django_style_from_polymorphic(self):
905+
ProxyBase.objects.create(some_data="Base1")
906+
ProxyChild.objects.create(some_data="Base1")
907+
TradProxyOnProxyChild.objects.create(some_data="Base3")
908+
self.assertEqual(3, ProxyBase.objects.count())
909+
self.assertEqual(2, ProxyChild.objects.count())
910+
self.assertEqual(3, TradProxyOnProxyChild.objects.count())
911+
912+
def test_convert_back_to_django_style_from_polymorphic_stops_at_concrete(self):
913+
ProxyBase.objects.create(some_data="Base1")
914+
NonProxyChild.objects.create(some_data="Base1")
915+
AliasOfNonProxyChild.objects.create(some_data="Base1")
916+
917+
self.assertEqual(3, ProxyBase.objects.count())
918+
self.assertEqual(2, NonProxyChild.objects.count())
919+
self.assertEqual(2, AliasOfNonProxyChild.objects.count())
920+
921+
def test_revert_back_to_polymorphic_proxy(self):
922+
self.assertFalse(ProxyChildAliasProxy._meta.polymorphic__proxy)
923+
862924
def test_proxy_get_real_instance_class(self):
863925
"""
864926
The call to ``get_real_instance()`` also checks whether the returned model is of the correct type.
@@ -868,12 +930,12 @@ def test_proxy_get_real_instance_class(self):
868930
name = "Item1"
869931
nonproxychild = NonProxyChild.objects.create(name=name)
870932

871-
pb = ProxyBase.objects.get(id=1)
933+
pb = ProxyBase.objects.get(id=nonproxychild.pk)
872934
assert pb.get_real_instance_class() == NonProxyChild
873935
assert pb.get_real_instance() == nonproxychild
874936
assert pb.name == name
875937

876-
pbm = NonProxyChild.objects.get(id=1)
938+
pbm = NonProxyChild.objects.get(id=nonproxychild.pk)
877939
assert pbm.get_real_instance_class() == NonProxyChild
878940
assert pbm.get_real_instance() == nonproxychild
879941
assert pbm.name == name

0 commit comments

Comments
 (0)