Skip to content
Draft
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
5 changes: 5 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
Changelog
=========

v5.0.0 (202x-XX-XX)
-------------------

* Implemented `Fix proxy model support. <https://github.com/jazzband/django-polymorphic/pull/676>`_

v4.3.0 (202X-XX-XX)
-------------------

Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ Advanced topics
formsets
migrating
managers
proxy
advanced
changelog
api/index
Expand Down
66 changes: 66 additions & 0 deletions docs/proxy.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
Proxy Models
============

:pypi:`django-polymorphic` has supported :ref:`proxy models <django:proxy-models>` since they were
introduced in Django but the default implementation is unintuitive. If a row is created from the
proxy model the :class:`~django.contrib.contenttypes.models.ContentType` of the proxy class is
recorded. This allows patterns that need to alter behavior based on which class was used to create
the row.

**By default the queryset on proxy models will filter on instance_of(ProxyModel) which will exclude
any rows that were not created from the proxy.**

.. code-block:: python

from polymorphic.models import PolymorphicModel

class MyModel(PolymorphicModel):
...

class MyProxy(MyModel):
class Meta:
proxy = True


MyModel.objects.create()
MyProxy.objects.create()

assert MyModel.objects.count() == 2
assert MyProxy.objects.count() == 1

This behavior may be unexpected for typical uses of proxy models which involves creating from the
concrete class then accessing from a proxy in the context where you need the modified proxy
interface. There is a
`discussion <https://github.com/jazzband/django-polymorphic/discussions/689>`_ if this should
continue to be the default behavior in version 5+.

Polymorphic Proxy Queries
-------------------------

.. versionadded:: 4.3

If you wish for your proxy model querysets to behave polymorphically by default
(include all rows created by the proxy and concrete models in the class's inheritance tree) then
instead of (or in additon to) :attr:`~django.db.models.Options.proxy` set a
:attr:`Meta.polymorphic_proxy` attribute to True:

.. code-block:: python

from polymorphic.models import PolymorphicModel

class MyModel(PolymorphicModel):
...

class MyProxy(MyModel):
class Meta:
polymorphc_proxy = True


MyModel.objects.create()
MyProxy.objects.create()

assert MyModel.objects.count() == 2
assert MyProxy.objects.count() == 2

for proxy in MyProxy.objects.all():
assert isinstance(proxy, MyProxy) #
13 changes: 13 additions & 0 deletions src/polymorphic/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ 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
if polymorphic__proxy:
attrs["Meta"].proxy = True
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 @@ -77,6 +85,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)
if new_class._meta.pk:
Expand Down
2 changes: 1 addition & 1 deletion src/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
66 changes: 66 additions & 0 deletions src/polymorphic/tests/migrations/0001_initial.py
Original file line number Diff line number Diff line change
Expand Up @@ -985,6 +985,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=[
Expand Down Expand Up @@ -1040,6 +1051,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=[
Expand Down Expand Up @@ -1198,6 +1220,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=[
Expand All @@ -1209,6 +1264,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=[
Expand Down
56 changes: 54 additions & 2 deletions src/polymorphic/tests/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ class UUIDPlainC(UUIDPlainB):
field3 = models.CharField(max_length=30)


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


class ProxyBase(PolymorphicModel):
Expand All @@ -370,7 +370,59 @@ 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:
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:
polymorphic_proxy = True


# child(poly) -> proxy(poly)
class NonAliasNonProxyChild(NonProxyChild):
class Meta:
proxy = True
polymorphic_proxy = False


class ProxiedBase(ShowFieldTypeAndContent, PolymorphicModel):
Expand Down
Loading
Loading