Skip to content

Commit 27c6473

Browse files
pgammansbckohan
authored andcommitted
Add option to model meta class to prevent proxy models being polymorphic
Fixes #376 #390
1 parent c541020 commit 27c6473

File tree

5 files changed

+194
-5
lines changed

5 files changed

+194
-5
lines changed

src/polymorphic/base.py

Lines changed: 11 additions & 0 deletions
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)
@@ -77,6 +83,11 @@ def __new__(self, model_name, bases, attrs, **kwargs):
7783
# for __init__ function of this class (monkeypatching inheritance accessors)
7884
new_class.polymorphic_super_sub_accessors_replaced = False
7985

86+
if polymorphic__proxy is not None:
87+
new_class._meta.polymorphic__proxy = polymorphic__proxy
88+
else:
89+
new_class._meta.polymorphic__proxy = not new_class._meta.proxy
90+
8091
# determine the name of the primary key field and store it into the class variable
8192
# polymorphic_primary_key_name (it is needed by query.py)
8293
for f in new_class._meta.fields:

src/polymorphic/managers.py

Lines changed: 1 addition & 1 deletion
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

src/polymorphic/tests/migrations/0001_initial.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -914,6 +914,17 @@ class Migration(migrations.Migration):
914914
'swappable': 'POLYMORPHIC_TEST_SWAPPABLE',
915915
},
916916
),
917+
migrations.CreateModel(
918+
name='AliasProxyChild',
919+
fields=[
920+
],
921+
options={
922+
'proxy': True,
923+
'indexes': [],
924+
'constraints': [],
925+
},
926+
bases=('tests.proxybase',),
927+
),
917928
migrations.CreateModel(
918929
name='ProxyChild',
919930
fields=[
@@ -969,6 +980,17 @@ class Migration(migrations.Migration):
969980
},
970981
bases=('tests.subclassselectorproxybasemodel',),
971982
),
983+
migrations.CreateModel(
984+
name='TradProxyChild',
985+
fields=[
986+
],
987+
options={
988+
'proxy': True,
989+
'indexes': [],
990+
'constraints': [],
991+
},
992+
bases=('tests.proxybase',),
993+
),
972994
migrations.CreateModel(
973995
name='Bottom',
974996
fields=[
@@ -1115,6 +1137,39 @@ class Migration(migrations.Migration):
11151137
},
11161138
bases=(polymorphic.showfields.ShowFieldTypeAndContent, models.Model),
11171139
),
1140+
migrations.CreateModel(
1141+
name='AliasOfNonProxyChild',
1142+
fields=[
1143+
],
1144+
options={
1145+
'proxy': True,
1146+
'indexes': [],
1147+
'constraints': [],
1148+
},
1149+
bases=('tests.nonproxychild',),
1150+
),
1151+
migrations.CreateModel(
1152+
name='NonAliasNonProxyChild',
1153+
fields=[
1154+
],
1155+
options={
1156+
'proxy': True,
1157+
'indexes': [],
1158+
'constraints': [],
1159+
},
1160+
bases=('tests.nonproxychild',),
1161+
),
1162+
migrations.CreateModel(
1163+
name='ProxyChildAliasProxy',
1164+
fields=[
1165+
],
1166+
options={
1167+
'proxy': True,
1168+
'indexes': [],
1169+
'constraints': [],
1170+
},
1171+
bases=('tests.tradproxychild',),
1172+
),
11181173
migrations.CreateModel(
11191174
name='PurpleHeadDuck',
11201175
fields=[
@@ -1126,6 +1181,17 @@ class Migration(migrations.Migration):
11261181
},
11271182
bases=('tests.blueheadduck', models.Model),
11281183
),
1184+
migrations.CreateModel(
1185+
name='TradProxyOnProxyChild',
1186+
fields=[
1187+
],
1188+
options={
1189+
'proxy': True,
1190+
'indexes': [],
1191+
'constraints': [],
1192+
},
1193+
bases=('tests.proxychild',),
1194+
),
11291195
migrations.CreateModel(
11301196
name='Model2D',
11311197
fields=[

src/polymorphic/tests/models.py

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,7 @@ class UUIDPlainC(UUIDPlainB):
353353
field3 = models.CharField(max_length=30)
354354

355355

356-
# base -> proxy
356+
# base(poly) -> proxy
357357

358358

359359
class ProxyBase(PolymorphicModel):
@@ -369,7 +369,61 @@ class NonProxyChild(ProxyBase):
369369
name = models.CharField(max_length=30)
370370

371371

372-
# base -> proxy -> real models
372+
# A traditional django proxy models. ie proxy'ed class is alias class
373+
# but in django_polymorphic this is not so.
374+
#
375+
# We have model types :-
376+
# base(poly) / child(poly) : A concrete polymorphic model 1+ fields
377+
# base(non poly) : A concrete django model 1+ fields
378+
# proxy(poly) : A proxy model where it is considered different
379+
# : from it superclasses
380+
# proxy(Traditional Django) : A proxy model where it is an alias for the
381+
# : underlying model
382+
383+
384+
# base(poly) -> proxy(poly) -> proxy(Traditional Django)
385+
class TradProxyOnProxyChild(ProxyChild):
386+
class Meta:
387+
proxy = True
388+
polymorphic__proxy = True
389+
390+
391+
# base(poly) -> proxy(Traditional Django)
392+
class TradProxyChild(ProxyBase):
393+
class Meta:
394+
proxy = True
395+
polymorphic__proxy = True
396+
397+
398+
# base(poly) -> proxy(Traditional Django) -> proxy(poly)
399+
# Not really helpful model as reduces to base(poly) -> proxy(poly)
400+
401+
402+
# base(poly) -> child(poly) -> proxy(Traditional Django)
403+
class AliasOfNonProxyChild(NonProxyChild):
404+
class Meta:
405+
proxy = True
406+
polymorphic__proxy = True
407+
408+
409+
# base(poly) -> proxy(Traditional Django) -> proxy(poly)
410+
class ProxyChildAliasProxy(TradProxyChild):
411+
class Meta:
412+
proxy = True
413+
414+
415+
# base(poly) -> proxy(poly)
416+
class AliasProxyChild(ProxyBase):
417+
class Meta:
418+
proxy = True
419+
polymorphic__proxy = True
420+
421+
422+
# child(poly) -> proxy(poly)
423+
class NonAliasNonProxyChild(NonProxyChild):
424+
class Meta:
425+
proxy = True
426+
polymorphic__proxy = False
373427

374428

375429
class ProxiedBase(ShowFieldTypeAndContent, PolymorphicModel):

src/polymorphic/tests/test_orm.py

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from polymorphic.managers import PolymorphicManager
1414
from polymorphic.models import PolymorphicTypeInvalid, PolymorphicTypeUndefined
1515
from polymorphic.tests.models import (
16+
AliasProxyChild,
1617
ArtProject,
1718
Base,
1819
BlogA,
@@ -94,6 +95,11 @@
9495
UUIDResearchProject,
9596
Duck,
9697
PurpleHeadDuck,
98+
NonAliasNonProxyChild,
99+
TradProxyOnProxyChild,
100+
TradProxyChild,
101+
AliasOfNonProxyChild,
102+
ProxyChildAliasProxy,
97103
)
98104

99105

@@ -867,6 +873,58 @@ def test_queryset_on_proxy_model_does_not_return_superclasses(self):
867873
assert ProxyBase.objects.count() == 5
868874
assert ProxyChild.objects.count() == 3
869875

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

879-
pb = ProxyBase.objects.get(id=1)
937+
pb = ProxyBase.objects.get(id=nonproxychild.pk)
880938
assert pb.get_real_instance_class() == NonProxyChild
881939
assert pb.get_real_instance() == nonproxychild
882940
assert pb.name == name
883941

884-
pbm = NonProxyChild.objects.get(id=1)
942+
pbm = NonProxyChild.objects.get(id=nonproxychild.pk)
885943
assert pbm.get_real_instance_class() == NonProxyChild
886944
assert pbm.get_real_instance() == nonproxychild
887945
assert pbm.name == name

0 commit comments

Comments
 (0)