2010-02-19 19:12:21 +03:00
|
|
|
"""
|
2015-10-22 04:11:06 +03:00
|
|
|
Seamless Polymorphic Inheritance for Django Models
|
2010-02-19 19:12:21 +03:00
|
|
|
"""
|
2024-03-12 23:23:19 +03:00
|
|
|
|
2026-01-04 21:24:00 +03:00
|
|
|
import warnings
|
|
|
|
|
|
2017-05-19 09:12:33 +03:00
|
|
|
from django.contrib.contenttypes.models import ContentType
|
2025-12-31 02:42:00 +03:00
|
|
|
from django.db import models, transaction
|
2016-05-28 04:35:39 +03:00
|
|
|
from django.db.utils import DEFAULT_DB_ALIAS
|
2026-01-04 21:24:00 +03:00
|
|
|
from django.utils.functional import classproperty
|
2015-10-22 04:11:06 +03:00
|
|
|
|
|
|
|
|
from .base import PolymorphicModelBase
|
2015-12-28 18:59:39 +03:00
|
|
|
from .managers import PolymorphicManager
|
2015-10-22 04:11:06 +03:00
|
|
|
from .query_translate import translate_polymorphic_Q_object
|
2026-01-12 09:48:39 +03:00
|
|
|
from .utils import get_base_polymorphic_model, lazy_ctype
|
2015-10-22 04:11:06 +03:00
|
|
|
|
|
|
|
|
###################################################################################
|
2015-12-28 17:14:24 +03:00
|
|
|
# PolymorphicModel
|
2015-10-22 04:11:06 +03:00
|
|
|
|
Fix PEP8 whitespace issues
autopep8 -r polymorphic/ example/ -i
--select=E112,E113,E115,E116,E122,E123,E125,E127,E128,E201,E202,E203,E211,E225,E226,E227,E228,E231,E251,E261,E262,E271,E272,E273,E274,E301,E302,E303,E304,E309,E711,E713,W291,W293,W391
--exclude migrations,south_migrations
(line conditiation fixes: E123,E125,E122,E127,E128)
2015-12-28 16:53:32 +03:00
|
|
|
|
2017-08-01 12:38:36 +03:00
|
|
|
class PolymorphicTypeUndefined(LookupError):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
2017-08-01 12:40:55 +03:00
|
|
|
class PolymorphicTypeInvalid(RuntimeError):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
2023-12-20 13:58:00 +03:00
|
|
|
class PolymorphicModel(models.Model, metaclass=PolymorphicModelBase):
|
2015-10-22 04:11:06 +03:00
|
|
|
"""
|
|
|
|
|
Abstract base class that provides polymorphic behaviour
|
|
|
|
|
for any model directly or indirectly derived from it.
|
|
|
|
|
|
2017-01-09 18:53:12 +03:00
|
|
|
PolymorphicModel declares one field for internal use (:attr:`polymorphic_ctype`)
|
|
|
|
|
and provides a polymorphic manager as the default manager (and as 'objects').
|
2015-10-22 04:11:06 +03:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
# for PolymorphicModelBase, so it can tell which models are polymorphic and which are not (duck typing)
|
|
|
|
|
polymorphic_model_marker = True
|
|
|
|
|
|
|
|
|
|
# for PolymorphicQuery, True => an overloaded __repr__ with nicer multi-line output is used by PolymorphicQuery
|
|
|
|
|
polymorphic_query_multiline_output = False
|
|
|
|
|
|
|
|
|
|
# avoid ContentType related field accessor clash (an error emitted by model validation)
|
2017-01-09 18:53:12 +03:00
|
|
|
#: The model field that stores the :class:`~django.contrib.contenttypes.models.ContentType` reference to the actual class.
|
2017-05-19 10:02:03 +03:00
|
|
|
polymorphic_ctype = models.ForeignKey(
|
2019-07-15 10:50:03 +03:00
|
|
|
ContentType,
|
|
|
|
|
null=True,
|
|
|
|
|
editable=False,
|
|
|
|
|
on_delete=models.CASCADE,
|
|
|
|
|
related_name="polymorphic_%(app_label)s.%(class)s_set+",
|
2017-05-19 10:02:03 +03:00
|
|
|
)
|
2015-10-22 04:11:06 +03:00
|
|
|
|
|
|
|
|
# some applications want to know the name of the fields that are added to its models
|
2019-07-15 10:50:03 +03:00
|
|
|
polymorphic_internal_model_fields = ["polymorphic_ctype"]
|
2015-10-22 04:11:06 +03:00
|
|
|
|
|
|
|
|
objects = PolymorphicManager()
|
|
|
|
|
|
2017-05-19 10:14:18 +03:00
|
|
|
class Meta:
|
|
|
|
|
abstract = True
|
|
|
|
|
|
2026-01-04 21:24:00 +03:00
|
|
|
@classproperty
|
|
|
|
|
def polymorphic_primary_key_name(cls):
|
|
|
|
|
"""
|
|
|
|
|
The name of the root primary key field of this polymorphic inheritance chain.
|
|
|
|
|
"""
|
|
|
|
|
warnings.warn(
|
|
|
|
|
"polymorphic_primary_key_name is deprecated and will be removed in "
|
|
|
|
|
"version 5.0, use get_base_polymorphic_model(Model)._meta.pk.attname "
|
|
|
|
|
"instead.",
|
|
|
|
|
DeprecationWarning,
|
|
|
|
|
stacklevel=2,
|
|
|
|
|
)
|
|
|
|
|
return get_base_polymorphic_model(cls, allow_abstract=True)._meta.pk.attname
|
|
|
|
|
|
2015-10-22 04:11:06 +03:00
|
|
|
@classmethod
|
2017-01-09 18:53:12 +03:00
|
|
|
def translate_polymorphic_Q_object(cls, q):
|
|
|
|
|
return translate_polymorphic_Q_object(cls, q)
|
2015-10-22 04:11:06 +03:00
|
|
|
|
2016-05-28 04:35:39 +03:00
|
|
|
def pre_save_polymorphic(self, using=DEFAULT_DB_ALIAS):
|
2015-10-22 04:11:06 +03:00
|
|
|
"""
|
2017-01-09 18:53:12 +03:00
|
|
|
Make sure the ``polymorphic_ctype`` value is correctly set on this model.
|
2025-12-23 17:06:09 +03:00
|
|
|
|
|
|
|
|
This method automatically updates the polymorphic_ctype when:
|
|
|
|
|
- The object is being saved for the first time
|
|
|
|
|
- The object is being saved to a different database than it was loaded from
|
|
|
|
|
|
|
|
|
|
This ensures cross-database saves work correctly without ForeignKeyViolation.
|
2017-01-09 18:53:12 +03:00
|
|
|
"""
|
|
|
|
|
# This function may be called manually in special use-cases. When the object
|
|
|
|
|
# is saved for the first time, we store its real class in polymorphic_ctype.
|
|
|
|
|
# When the object later is retrieved by PolymorphicQuerySet, it uses this
|
|
|
|
|
# field to figure out the real class of this object
|
|
|
|
|
# (used by PolymorphicQuerySet._get_real_instances)
|
2025-12-23 17:06:09 +03:00
|
|
|
|
|
|
|
|
# Update polymorphic_ctype if:
|
|
|
|
|
# 1. It's not set yet (new object), OR
|
|
|
|
|
# 2. The database has changed (cross-database save)
|
|
|
|
|
needs_update = not self.polymorphic_ctype_id or (
|
|
|
|
|
self._state.db and self._state.db != using
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if needs_update:
|
|
|
|
|
# Set polymorphic_ctype_id directly to avoid database router issues
|
|
|
|
|
# when saving across databases
|
|
|
|
|
ctype = ContentType.objects.db_manager(using).get_for_model(
|
2021-11-18 14:40:39 +03:00
|
|
|
self, for_concrete_model=False
|
|
|
|
|
)
|
2025-12-23 17:06:09 +03:00
|
|
|
self.polymorphic_ctype_id = ctype.pk
|
2019-07-15 10:50:03 +03:00
|
|
|
|
2015-10-22 04:11:06 +03:00
|
|
|
def save(self, *args, **kwargs):
|
2017-01-09 18:53:12 +03:00
|
|
|
"""Calls :meth:`pre_save_polymorphic` and saves the model."""
|
2025-12-23 17:19:30 +03:00
|
|
|
# Determine the database to use:
|
|
|
|
|
# 1. Explicit 'using' parameter takes precedence
|
|
|
|
|
# 2. Otherwise use self._state.db (the database the object was loaded from)
|
|
|
|
|
# 3. Fall back to DEFAULT_DB_ALIAS
|
|
|
|
|
# This ensures database routers are respected when no explicit database is specified
|
|
|
|
|
using = kwargs.get("using")
|
|
|
|
|
if using is None:
|
|
|
|
|
using = self._state.db or DEFAULT_DB_ALIAS
|
|
|
|
|
|
2016-05-28 04:35:39 +03:00
|
|
|
self.pre_save_polymorphic(using=using)
|
2021-11-18 14:38:58 +03:00
|
|
|
return super().save(*args, **kwargs)
|
2019-07-15 10:50:03 +03:00
|
|
|
|
2015-10-22 04:11:06 +03:00
|
|
|
save.alters_data = True
|
|
|
|
|
|
2016-05-31 02:55:03 +03:00
|
|
|
def get_real_instance_class(self):
|
2015-10-22 04:11:06 +03:00
|
|
|
"""
|
2017-01-09 18:53:12 +03:00
|
|
|
Return the actual model type of the object.
|
|
|
|
|
|
2015-10-22 04:11:06 +03:00
|
|
|
If a non-polymorphic manager (like base_objects) has been used to
|
|
|
|
|
retrieve objects, then the real class/type of these objects may be
|
|
|
|
|
determined using this method.
|
|
|
|
|
"""
|
2017-08-01 12:38:36 +03:00
|
|
|
if self.polymorphic_ctype_id is None:
|
2019-07-15 10:50:03 +03:00
|
|
|
raise PolymorphicTypeUndefined(
|
2023-12-20 13:29:23 +03:00
|
|
|
f"The model {self.__class__.__name__}#{self.pk} does not have a `polymorphic_ctype_id` value defined.\n"
|
|
|
|
|
f"If you created models outside polymorphic, e.g. through an import or migration, "
|
|
|
|
|
f"make sure the `polymorphic_ctype_id` field points to the ContentType ID of the model subclass."
|
2019-07-15 10:50:03 +03:00
|
|
|
)
|
2017-08-01 12:38:36 +03:00
|
|
|
|
2015-10-22 04:11:06 +03:00
|
|
|
# the following line would be the easiest way to do this, but it produces sql queries
|
|
|
|
|
# return self.polymorphic_ctype.model_class()
|
|
|
|
|
# so we use the following version, which uses the ContentType manager cache.
|
|
|
|
|
# Note that model_class() can return None for stale content types;
|
|
|
|
|
# when the content type record still exists but no longer refers to an existing model.
|
2019-07-15 10:50:03 +03:00
|
|
|
model = (
|
|
|
|
|
ContentType.objects.db_manager(self._state.db)
|
|
|
|
|
.get_for_id(self.polymorphic_ctype_id)
|
|
|
|
|
.model_class()
|
|
|
|
|
)
|
2015-10-22 04:11:06 +03:00
|
|
|
|
|
|
|
|
# Protect against bad imports (dumpdata without --natural) or other
|
|
|
|
|
# issues missing with the ContentType models.
|
2019-07-15 10:50:03 +03:00
|
|
|
if (
|
|
|
|
|
model is not None
|
|
|
|
|
and not issubclass(model, self.__class__)
|
|
|
|
|
and (
|
|
|
|
|
self.__class__._meta.proxy_for_model is None
|
|
|
|
|
or not issubclass(model, self.__class__._meta.proxy_for_model)
|
|
|
|
|
)
|
|
|
|
|
):
|
|
|
|
|
raise PolymorphicTypeInvalid(
|
2026-01-10 10:59:27 +03:00
|
|
|
f"ContentType {self.polymorphic_ctype_id} for {model} #{self.pk} does "
|
|
|
|
|
"not point to a subclass!"
|
2019-07-15 10:50:03 +03:00
|
|
|
)
|
2018-11-22 23:05:57 +03:00
|
|
|
|
2015-10-22 04:11:06 +03:00
|
|
|
return model
|
|
|
|
|
|
2016-05-31 02:55:03 +03:00
|
|
|
def get_real_concrete_instance_class_id(self):
|
2017-01-11 16:17:26 +03:00
|
|
|
model_class = self.get_real_instance_class()
|
|
|
|
|
if model_class is None:
|
2015-10-22 04:11:06 +03:00
|
|
|
return None
|
2019-07-15 10:50:03 +03:00
|
|
|
return (
|
|
|
|
|
ContentType.objects.db_manager(self._state.db)
|
|
|
|
|
.get_for_model(model_class, for_concrete_model=True)
|
|
|
|
|
.pk
|
|
|
|
|
)
|
2015-10-22 04:11:06 +03:00
|
|
|
|
2016-05-31 02:55:03 +03:00
|
|
|
def get_real_concrete_instance_class(self):
|
|
|
|
|
model_class = self.get_real_instance_class()
|
2015-10-22 04:11:06 +03:00
|
|
|
if model_class is None:
|
|
|
|
|
return None
|
2019-07-15 10:50:03 +03:00
|
|
|
return (
|
|
|
|
|
ContentType.objects.db_manager(self._state.db)
|
|
|
|
|
.get_for_model(model_class, for_concrete_model=True)
|
|
|
|
|
.model_class()
|
|
|
|
|
)
|
2015-10-22 04:11:06 +03:00
|
|
|
|
2016-05-31 02:55:03 +03:00
|
|
|
def get_real_instance(self):
|
2017-01-09 18:53:12 +03:00
|
|
|
"""
|
|
|
|
|
Upcast an object to it's actual type.
|
|
|
|
|
|
2015-10-22 04:11:06 +03:00
|
|
|
If a non-polymorphic manager (like base_objects) has been used to
|
|
|
|
|
retrieve objects, then the complete object with it's real class/type
|
|
|
|
|
and all fields may be retrieved with this method.
|
2017-01-09 18:53:12 +03:00
|
|
|
|
2025-05-01 13:37:21 +03:00
|
|
|
If the model of the object's actual type does not exist (i.e. its
|
|
|
|
|
ContentType is stale), this method raises a
|
|
|
|
|
:class:`~polymorphic.models.PolymorphicTypeInvalid` exception.
|
2025-04-20 16:08:21 +03:00
|
|
|
|
2017-01-09 18:53:12 +03:00
|
|
|
.. note::
|
|
|
|
|
Each method call executes one db query (if necessary).
|
|
|
|
|
Use the :meth:`~polymorphic.managers.PolymorphicQuerySet.get_real_instances`
|
|
|
|
|
to upcast a complete list in a single efficient query.
|
|
|
|
|
"""
|
2016-05-31 02:55:03 +03:00
|
|
|
real_model = self.get_real_instance_class()
|
2026-01-02 21:21:18 +03:00
|
|
|
if real_model is self.__class__:
|
2015-10-22 04:11:06 +03:00
|
|
|
return self
|
2025-05-01 13:37:21 +03:00
|
|
|
if real_model is None:
|
|
|
|
|
raise PolymorphicTypeInvalid(
|
|
|
|
|
f"ContentType {self.polymorphic_ctype_id} for {self.__class__} "
|
|
|
|
|
f"#{self.pk} does not have a corresponding model!"
|
|
|
|
|
)
|
2026-01-02 21:21:18 +03:00
|
|
|
return self.__class__.objects.db_manager(self._state.db).get(pk=self.pk)
|
2015-10-22 04:11:06 +03:00
|
|
|
|
2025-12-31 02:42:00 +03:00
|
|
|
def delete(self, using=None, keep_parents=False):
|
|
|
|
|
"""
|
|
|
|
|
Behaves the same as Django's default :meth:`~django.db.models.Model.delete()`,
|
|
|
|
|
but with support for upcasting when ``keep_parents`` is True. When keeping
|
|
|
|
|
parents (upcasting the row) the ``polymorphic_ctype`` fields of the parent rows
|
|
|
|
|
are updated accordingly in a transaction with the child row deletion.
|
|
|
|
|
"""
|
|
|
|
|
# if we are keeping parents, we must first determine which polymorphic_ctypes we
|
|
|
|
|
# need to update
|
|
|
|
|
parent_updates = (
|
|
|
|
|
[
|
|
|
|
|
(parent_model, getattr(self, parent_field.get_attname()))
|
|
|
|
|
for parent_model, parent_field in self._meta.parents.items()
|
|
|
|
|
if issubclass(parent_model, PolymorphicModel)
|
|
|
|
|
]
|
|
|
|
|
if keep_parents
|
|
|
|
|
else []
|
|
|
|
|
)
|
|
|
|
|
if parent_updates:
|
|
|
|
|
with transaction.atomic(using=using):
|
|
|
|
|
# If keeping the parents (upcasting) we need to update the relevant
|
|
|
|
|
# content types for all parent inheritance paths.
|
|
|
|
|
ret = super().delete(using=using, keep_parents=keep_parents)
|
|
|
|
|
for parent_model, pk in parent_updates:
|
2026-01-12 09:48:39 +03:00
|
|
|
parent_model.objects.db_manager(using=using).non_polymorphic().filter(
|
|
|
|
|
pk=pk
|
|
|
|
|
).update(polymorphic_ctype=lazy_ctype(parent_model, using=using))
|
2025-12-31 02:42:00 +03:00
|
|
|
return ret
|
|
|
|
|
return super().delete(using=using, keep_parents=keep_parents)
|
|
|
|
|
|
|
|
|
|
delete.alters_data = True
|