Skip to content

Commit c626431

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

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
@@ -985,6 +985,17 @@ class Migration(migrations.Migration):
985985
'swappable': 'POLYMORPHIC_TEST_SWAPPABLE',
986986
},
987987
),
988+
migrations.CreateModel(
989+
name='AliasProxyChild',
990+
fields=[
991+
],
992+
options={
993+
'proxy': True,
994+
'indexes': [],
995+
'constraints': [],
996+
},
997+
bases=('tests.proxybase',),
998+
),
988999
migrations.CreateModel(
9891000
name='ProxyChild',
9901001
fields=[
@@ -1040,6 +1051,17 @@ class Migration(migrations.Migration):
10401051
},
10411052
bases=('tests.subclassselectorproxybasemodel',),
10421053
),
1054+
migrations.CreateModel(
1055+
name='TradProxyChild',
1056+
fields=[
1057+
],
1058+
options={
1059+
'proxy': True,
1060+
'indexes': [],
1061+
'constraints': [],
1062+
},
1063+
bases=('tests.proxybase',),
1064+
),
10431065
migrations.CreateModel(
10441066
name='Bottom',
10451067
fields=[
@@ -1198,6 +1220,39 @@ class Migration(migrations.Migration):
11981220
},
11991221
bases=(polymorphic.showfields.ShowFieldTypeAndContent, models.Model),
12001222
),
1223+
migrations.CreateModel(
1224+
name='AliasOfNonProxyChild',
1225+
fields=[
1226+
],
1227+
options={
1228+
'proxy': True,
1229+
'indexes': [],
1230+
'constraints': [],
1231+
},
1232+
bases=('tests.nonproxychild',),
1233+
),
1234+
migrations.CreateModel(
1235+
name='NonAliasNonProxyChild',
1236+
fields=[
1237+
],
1238+
options={
1239+
'proxy': True,
1240+
'indexes': [],
1241+
'constraints': [],
1242+
},
1243+
bases=('tests.nonproxychild',),
1244+
),
1245+
migrations.CreateModel(
1246+
name='ProxyChildAliasProxy',
1247+
fields=[
1248+
],
1249+
options={
1250+
'proxy': True,
1251+
'indexes': [],
1252+
'constraints': [],
1253+
},
1254+
bases=('tests.tradproxychild',),
1255+
),
12011256
migrations.CreateModel(
12021257
name='PurpleHeadDuck',
12031258
fields=[
@@ -1209,6 +1264,17 @@ class Migration(migrations.Migration):
12091264
},
12101265
bases=('tests.blueheadduck', models.Model),
12111266
),
1267+
migrations.CreateModel(
1268+
name='TradProxyOnProxyChild',
1269+
fields=[
1270+
],
1271+
options={
1272+
'proxy': True,
1273+
'indexes': [],
1274+
'constraints': [],
1275+
},
1276+
bases=('tests.proxychild',),
1277+
),
12121278
migrations.CreateModel(
12131279
name='Model2D',
12141280
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

@@ -871,6 +877,58 @@ def test_queryset_on_proxy_model_does_not_return_superclasses(self):
871877
assert ProxyBase.objects.count() == 5
872878
assert ProxyChild.objects.count() == 3
873879

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

883-
pb = ProxyBase.objects.get(id=1)
941+
pb = ProxyBase.objects.get(id=nonproxychild.pk)
884942
assert pb.get_real_instance_class() == NonProxyChild
885943
assert pb.get_real_instance() == nonproxychild
886944
assert pb.name == name
887945

888-
pbm = NonProxyChild.objects.get(id=1)
946+
pbm = NonProxyChild.objects.get(id=nonproxychild.pk)
889947
assert pbm.get_real_instance_class() == NonProxyChild
890948
assert pbm.get_real_instance() == nonproxychild
891949
assert pbm.name == name

0 commit comments

Comments
 (0)