Skip to content

Commit

Permalink
Add option to model meta class to prevent proxy models being polymorphic
Browse files Browse the repository at this point in the history
  • Loading branch information
pgammans committed Apr 4, 2024
1 parent 3312f5f commit 7fca75a
Show file tree
Hide file tree
Showing 5 changed files with 208 additions and 5 deletions.
11 changes: 11 additions & 0 deletions polymorphic/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ class PolymorphicModelBase(ModelBase):
"""

def __new__(self, model_name, bases, attrs, **kwargs):
polymorphic__proxy = None
if "Meta" in attrs:
if hasattr(attrs["Meta"], "polymorphic__proxy"):
polymorphic__proxy = attrs["Meta"].polymorphic__proxy
del attrs["Meta"].polymorphic__proxy

# Workaround compatibility issue with six.with_metaclass() and custom Django model metaclasses:
if not attrs and model_name == "NewBase":
return super().__new__(self, model_name, bases, attrs, **kwargs)
Expand All @@ -70,6 +76,11 @@ def __new__(self, model_name, bases, attrs, **kwargs):
# for __init__ function of this class (monkeypatching inheritance accessors)
new_class.polymorphic_super_sub_accessors_replaced = False

if polymorphic__proxy is not None:
new_class._meta.polymorphic__proxy = polymorphic__proxy
else:
new_class._meta.polymorphic__proxy = not new_class._meta.proxy

# determine the name of the primary key field and store it into the class variable
# polymorphic_primary_key_name (it is needed by query.py)
for f in new_class._meta.fields:
Expand Down
2 changes: 1 addition & 1 deletion polymorphic/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def from_queryset(cls, queryset_class, class_name=None):

def get_queryset(self):
qs = self.queryset_class(self.model, using=self._db, hints=self._hints)
if self.model._meta.proxy:
if not self.model._meta.polymorphic__proxy:
qs = qs.instance_of(self.model)
return qs

Expand Down
78 changes: 78 additions & 0 deletions polymorphic/tests/migrations/0001_initial.py
Original file line number Diff line number Diff line change
Expand Up @@ -1510,6 +1510,84 @@ class Migration(migrations.Migration):
options={"proxy": True},
bases=("tests.proxybase",),
),
migrations.CreateModel(
name="AliasProxyChild",
fields=[],
options={"proxy": True},
bases=("tests.proxybase",),
),
migrations.CreateModel(
name="NonProxyChildAliasProxy",
fields=[],
options={"proxy": True},
bases=("tests.nonproxychild",),
),
migrations.CreateModel(
name='AliasOfNonProxyChild',
fields=[
],
options={
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=('tests.nonproxychild',),
),
migrations.CreateModel(
name='NonAliasNonProxyChild',
fields=[
],
options={
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=('tests.nonproxychild',),
),
migrations.CreateModel(
name='TradProxyChild',
fields=[
],
options={
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=('tests.proxybase',),
),
migrations.CreateModel(
name='TradProxyOnProxyChild',
fields=[
],
options={
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=('tests.proxychild',),
),
migrations.CreateModel(
name='PolyTradProxyChild',
fields=[
],
options={
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=('tests.tradproxychild',),
),
migrations.CreateModel(
name='ProxyChildAliasProxy',
fields=[
],
options={
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=('tests.tradproxychild',),
),
migrations.CreateModel(
name="ProxyModelBase",
fields=[],
Expand Down
56 changes: 54 additions & 2 deletions polymorphic/tests/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ class UUIDPlainC(UUIDPlainB):
field3 = models.CharField(max_length=10)


# base -> proxy
# base(poly) -> proxy


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


# base -> proxy -> real models
# A traditional django proxy models. ie proxy'ed class is alias class
# but in django_polymorphic this is not so.
#
# We have model types :-
# base(poly) / child(poly) : A concrete polymorphic model 1+ fields
# base(non ploy) : A concrete django model 1+ fields
# proxy(poly) : A proxy model where it considered different
# : from it superclasses
# proxy(Traditional Django) : A proxy model where it is an alias for the
# : underline model


# base(poly) -> proxy(poly) -> proxy(Traditional Django)
class TradProxyOnProxyChild(ProxyChild):
class Meta:
proxy = True
polymorphic__proxy = True


# base(poly) -> proxy(Traditional Django)
class TradProxyChild(ProxyBase):
class Meta:
proxy = True
polymorphic__proxy = True

# base(poly) -> proxy(Traditional Django) -> proxy(poly)
# Not really helpful model as reduces to base(poly) -> proxy(poly)

# base(poly) -> child(poly) -> proxy(Traditional Django)
class AliasOfNonProxyChild(NonProxyChild):
class Meta:
proxy = True
polymorphic__proxy = True


# base(poly) -> proxy(Traditional Django) -> proxy(poly)
class ProxyChildAliasProxy(TradProxyChild):
class Meta:
proxy = True


# base(poly) -> proxy(poly)
class AliasProxyChild(ProxyBase):
class Meta:
proxy = True
polymorphic__proxy = True


# base(poly) -> proxy(poly)
class NonAliasNonProxyChild(NonProxyChild):
class Meta:
proxy = True
polymorphic__proxy = False


class ProxiedBase(ShowFieldTypeAndContent, PolymorphicModel):
Expand Down
66 changes: 64 additions & 2 deletions polymorphic/tests/test_orm.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from polymorphic.managers import PolymorphicManager
from polymorphic.models import PolymorphicTypeInvalid, PolymorphicTypeUndefined
from polymorphic.tests.models import (
AliasProxyChild,
ArtProject,
Base,
BlogA,
Expand Down Expand Up @@ -88,6 +89,13 @@
UUIDPlainC,
UUIDProject,
UUIDResearchProject,

NonAliasNonProxyChild,
TradProxyOnProxyChild,
TradProxyChild,
AliasOfNonProxyChild,
ProxyChildAliasProxy,

)


Expand Down Expand Up @@ -859,6 +867,60 @@ def test_queryset_on_proxy_model_does_not_return_superclasses(self):
assert ProxyBase.objects.count() == 5
assert ProxyChild.objects.count() == 3

def test_queryset_on_polymorphic_proxy_model_returns_superclasses(self):
ProxyBase.objects.create(some_data="Base1")
AliasProxyChild.objects.create(some_data="ProxyChild1")
AliasProxyChild.objects.create(some_data="ProxyChild2")
ProxyChild.objects.create(some_data="PolyChild1")
NonAliasNonProxyChild.objects.create(some_data="SubChild1")
NonAliasNonProxyChild.objects.create(some_data="SubChild2")
NonProxyChild.objects.create(some_data="NonProxChild1", name="t1")

with self.subTest(" superclasses"):
self.assertEqual(7, ProxyBase.objects.count())
self.assertEqual(7, AliasProxyChild.objects.count())
with self.subTest("only compete classes"):
# Non proxy models should not return the proxy siblings
self.assertEqual(1, ProxyChild.objects.count())
self.assertEqual(2, NonAliasNonProxyChild.objects.count())
self.assertEqual(3, NonProxyChild.objects.count())

def test_polymorphic_proxy_object_has_different_ctype_from_base(self):
obj1 = ProxyBase.objects.create(some_data="Base1")
obj2 = AliasProxyChild.objects.create(some_data="ProxyChild1")
obj1_ctype = ContentType.objects.get_for_model(
obj1, for_concrete_model=False)
obj2_ctype = ContentType.objects.get_for_model(
obj2, for_concrete_model=False)
self.assertNotEqual(obj1_ctype, obj2_ctype)

def test_can_create_django_style_proxy_classes_alias(self):
ProxyBase.objects.create(some_data="Base1")
TradProxyChild.objects.create(some_data="Base2")
self.assertEqual(2, ProxyBase.objects.count())
self.assertEqual(2, TradProxyChild.objects.count())
TradProxyOnProxyChild.objects.create()

def test_convert_back_to_django_style_from_polymorphic(self):
ProxyBase.objects.create(some_data="Base1")
ProxyChild.objects.create(some_data="Base1")
TradProxyOnProxyChild.objects.create(some_data="Base3")
self.assertEqual(3, ProxyBase.objects.count())
self.assertEqual(2, ProxyChild.objects.count())
self.assertEqual(3, TradProxyOnProxyChild.objects.count())

def test_convert_back_to_django_style_from_polymorphic_stops_at_concrete(self):
ProxyBase.objects.create(some_data="Base1")
NonProxyChild.objects.create(some_data="Base1")
AliasOfNonProxyChild.objects.create(some_data="Base1")

self.assertEqual(3, ProxyBase.objects.count())
self.assertEqual(2, NonProxyChild.objects.count())
self.assertEqual(2, AliasOfNonProxyChild.objects.count())

def test_revert_back_to_polymorphic_proxy(self):
self.assertFalse(ProxyChildAliasProxy._meta.polymorphic__proxy)

def test_proxy_get_real_instance_class(self):
"""
The call to ``get_real_instance()`` also checks whether the returned model is of the correct type.
Expand All @@ -868,12 +930,12 @@ def test_proxy_get_real_instance_class(self):
name = "Item1"
nonproxychild = NonProxyChild.objects.create(name=name)

pb = ProxyBase.objects.get(id=1)
pb = ProxyBase.objects.get(id=nonproxychild.pk)
assert pb.get_real_instance_class() == NonProxyChild
assert pb.get_real_instance() == nonproxychild
assert pb.name == name

pbm = NonProxyChild.objects.get(id=1)
pbm = NonProxyChild.objects.get(id=nonproxychild.pk)
assert pbm.get_real_instance_class() == NonProxyChild
assert pbm.get_real_instance() == nonproxychild
assert pbm.name == name
Expand Down

0 comments on commit 7fca75a

Please sign in to comment.