Skip to content

Commit a83683d

Browse files
pgammansbckohan
authored andcommitted
Add option to model meta class to prevent proxy models being polymorphic
Fixes #376 #390
1 parent 447e8c2 commit a83683d

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
if new_class._meta.pk:

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
@@ -950,6 +950,17 @@ class Migration(migrations.Migration):
950950
'swappable': 'POLYMORPHIC_TEST_SWAPPABLE',
951951
},
952952
),
953+
migrations.CreateModel(
954+
name='AliasProxyChild',
955+
fields=[
956+
],
957+
options={
958+
'proxy': True,
959+
'indexes': [],
960+
'constraints': [],
961+
},
962+
bases=('tests.proxybase',),
963+
),
953964
migrations.CreateModel(
954965
name='ProxyChild',
955966
fields=[
@@ -1005,6 +1016,17 @@ class Migration(migrations.Migration):
10051016
},
10061017
bases=('tests.subclassselectorproxybasemodel',),
10071018
),
1019+
migrations.CreateModel(
1020+
name='TradProxyChild',
1021+
fields=[
1022+
],
1023+
options={
1024+
'proxy': True,
1025+
'indexes': [],
1026+
'constraints': [],
1027+
},
1028+
bases=('tests.proxybase',),
1029+
),
10081030
migrations.CreateModel(
10091031
name='Bottom',
10101032
fields=[
@@ -1163,6 +1185,39 @@ class Migration(migrations.Migration):
11631185
},
11641186
bases=(polymorphic.showfields.ShowFieldTypeAndContent, models.Model),
11651187
),
1188+
migrations.CreateModel(
1189+
name='AliasOfNonProxyChild',
1190+
fields=[
1191+
],
1192+
options={
1193+
'proxy': True,
1194+
'indexes': [],
1195+
'constraints': [],
1196+
},
1197+
bases=('tests.nonproxychild',),
1198+
),
1199+
migrations.CreateModel(
1200+
name='NonAliasNonProxyChild',
1201+
fields=[
1202+
],
1203+
options={
1204+
'proxy': True,
1205+
'indexes': [],
1206+
'constraints': [],
1207+
},
1208+
bases=('tests.nonproxychild',),
1209+
),
1210+
migrations.CreateModel(
1211+
name='ProxyChildAliasProxy',
1212+
fields=[
1213+
],
1214+
options={
1215+
'proxy': True,
1216+
'indexes': [],
1217+
'constraints': [],
1218+
},
1219+
bases=('tests.tradproxychild',),
1220+
),
11661221
migrations.CreateModel(
11671222
name='PurpleHeadDuck',
11681223
fields=[
@@ -1174,6 +1229,17 @@ class Migration(migrations.Migration):
11741229
},
11751230
bases=('tests.blueheadduck', models.Model),
11761231
),
1232+
migrations.CreateModel(
1233+
name='TradProxyOnProxyChild',
1234+
fields=[
1235+
],
1236+
options={
1237+
'proxy': True,
1238+
'indexes': [],
1239+
'constraints': [],
1240+
},
1241+
bases=('tests.proxychild',),
1242+
),
11771243
migrations.CreateModel(
11781244
name='Model2D',
11791245
fields=[

src/polymorphic/tests/models.py

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

356356

357-
# base -> proxy
357+
# base(poly) -> proxy
358358

359359

360360
class ProxyBase(PolymorphicModel):
@@ -370,7 +370,61 @@ class NonProxyChild(ProxyBase):
370370
name = models.CharField(max_length=30)
371371

372372

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

375429

376430
class ProxiedBase(ShowFieldTypeAndContent, PolymorphicModel):

src/polymorphic/tests/test_orm.py

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from polymorphic.managers import PolymorphicManager
1515
from polymorphic.models import PolymorphicTypeInvalid, PolymorphicTypeUndefined
1616
from polymorphic.tests.models import (
17+
AliasProxyChild,
1718
ArtProject,
1819
Base,
1920
BlogA,
@@ -99,6 +100,11 @@
99100
SpecialAccount1,
100101
SpecialAccount1_1,
101102
SpecialAccount2,
103+
NonAliasNonProxyChild,
104+
TradProxyOnProxyChild,
105+
TradProxyChild,
106+
AliasOfNonProxyChild,
107+
ProxyChildAliasProxy,
102108
)
103109

104110

@@ -872,6 +878,58 @@ def test_queryset_on_proxy_model_does_not_return_superclasses(self):
872878
assert ProxyBase.objects.count() == 5
873879
assert ProxyChild.objects.count() == 3
874880

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

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

889-
pbm = NonProxyChild.objects.get(id=1)
947+
pbm = NonProxyChild.objects.get(id=nonproxychild.pk)
890948
assert pbm.get_real_instance_class() == NonProxyChild
891949
assert pbm.get_real_instance() == nonproxychild
892950
assert pbm.name == name

0 commit comments

Comments
 (0)