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

Fixes #376 #390
This commit is contained in:
Paul Gammans 2024-04-04 16:02:30 +01:00 committed by Brian Kohan
parent 0d820d0641
commit a8276fc8a5
No known key found for this signature in database
GPG Key ID: 5C6CE8BA38C43FC1
5 changed files with 195 additions and 6 deletions

View File

@ -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)
@ -70,6 +76,11 @@ class PolymorphicModelBase(ModelBase):
# 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:

View File

@ -28,7 +28,7 @@ class PolymorphicManager(models.Manager):
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

View File

@ -1,4 +1,4 @@
# Generated by Django 4.2.26 on 2025-12-03 23:29
# Generated by Django 4.2.26 on 2025-12-04 01:12
from django.db import migrations, models
import django.db.models.deletion
@ -917,6 +917,17 @@ class Migration(migrations.Migration):
'swappable': 'POLYMORPHIC_TEST_SWAPPABLE',
},
),
migrations.CreateModel(
name='AliasProxyChild',
fields=[
],
options={
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=('tests.proxybase',),
),
migrations.CreateModel(
name='ProxyChild',
fields=[
@ -972,6 +983,17 @@ class Migration(migrations.Migration):
},
bases=('tests.subclassselectorproxybasemodel',),
),
migrations.CreateModel(
name='TradProxyChild',
fields=[
],
options={
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=('tests.proxybase',),
),
migrations.CreateModel(
name='Bottom',
fields=[
@ -1119,6 +1141,39 @@ class Migration(migrations.Migration):
},
bases=(polymorphic.showfields.ShowFieldTypeAndContent, models.Model),
),
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='ProxyChildAliasProxy',
fields=[
],
options={
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=('tests.tradproxychild',),
),
migrations.CreateModel(
name='PurpleHeadDuck',
fields=[
@ -1130,6 +1185,17 @@ class Migration(migrations.Migration):
},
bases=('tests.blueheadduck', models.Model),
),
migrations.CreateModel(
name='TradProxyOnProxyChild',
fields=[
],
options={
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=('tests.proxychild',),
),
migrations.CreateModel(
name='Model2D',
fields=[

View File

@ -348,7 +348,7 @@ class UUIDPlainC(UUIDPlainB):
field3 = models.CharField(max_length=30)
# base -> proxy
# base(poly) -> proxy
class ProxyBase(PolymorphicModel):
@ -364,7 +364,61 @@ class NonProxyChild(ProxyBase):
name = models.CharField(max_length=30)
# 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 poly) : A concrete django model 1+ fields
# proxy(poly) : A proxy model where it is considered different
# : from it superclasses
# proxy(Traditional Django) : A proxy model where it is an alias for the
# : underlying 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
# child(poly) -> proxy(poly)
class NonAliasNonProxyChild(NonProxyChild):
class Meta:
proxy = True
polymorphic__proxy = False
class ProxiedBase(ShowFieldTypeAndContent, PolymorphicModel):

View File

@ -13,6 +13,7 @@ from polymorphic import query_translate
from polymorphic.managers import PolymorphicManager
from polymorphic.models import PolymorphicTypeInvalid, PolymorphicTypeUndefined
from polymorphic.tests.models import (
AliasProxyChild,
ArtProject,
Base,
BlogA,
@ -91,6 +92,11 @@ from polymorphic.tests.models import (
UUIDResearchProject,
Duck,
PurpleHeadDuck,
NonAliasNonProxyChild,
TradProxyOnProxyChild,
TradProxyChild,
AliasOfNonProxyChild,
ProxyChildAliasProxy,
)
@ -864,6 +870,58 @@ class PolymorphicTests(TransactionTestCase):
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="NonProxyChild1", name="t1")
with self.subTest("superclasses"):
self.assertEqual(7, ProxyBase.objects.count())
self.assertEqual(7, AliasProxyChild.objects.count())
with self.subTest("only complete 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.
@ -873,12 +931,12 @@ class PolymorphicTests(TransactionTestCase):
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