Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to model meta class to prevent proxy models being polymorphic #544

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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