kopia lustrzana https://github.com/wagtail/wagtail
133 wiersze
5.6 KiB
Python
133 wiersze
5.6 KiB
Python
from django.contrib.contenttypes.models import ContentType
|
|
from django.db.models import DEFERRED
|
|
from django.utils.functional import cached_property
|
|
|
|
|
|
class SpecificMixin:
|
|
"""
|
|
Mixin for models that support multi-table inheritance and provide a
|
|
``content_type`` field pointing to the specific model class, to provide
|
|
methods and properties for retrieving the specific instance of the model.
|
|
"""
|
|
|
|
def get_specific(self, deferred=False, copy_attrs=None, copy_attrs_exclude=None):
|
|
"""
|
|
Return this object in its most specific subclassed form.
|
|
|
|
By default, a database query is made to fetch all field values for the
|
|
specific object. If you only require access to custom methods or other
|
|
non-field attributes on the specific object, you can use
|
|
``deferred=True`` to avoid this query. However, any attempts to access
|
|
specific field values from the returned object will trigger additional
|
|
database queries.
|
|
|
|
By default, references to all non-field attribute values are copied
|
|
from current object to the returned one. This includes:
|
|
|
|
* Values set by a queryset, for example: annotations, or values set as
|
|
a result of using ``select_related()`` or ``prefetch_related()``.
|
|
* Any ``cached_property`` values that have been evaluated.
|
|
* Attributes set elsewhere in Python code.
|
|
|
|
For fine-grained control over which non-field values are copied to the
|
|
returned object, you can use ``copy_attrs`` to specify a complete list
|
|
of attribute names to include. Alternatively, you can use
|
|
``copy_attrs_exclude`` to specify a list of attribute names to exclude.
|
|
|
|
If called on an object that is already an instance of the most specific
|
|
class, the object will be returned as is, and no database queries or
|
|
other operations will be triggered.
|
|
|
|
If the object was originally created using a model that has since
|
|
been removed from the codebase, an instance of the base class will be
|
|
returned (without any custom field values or other functionality
|
|
present on the original class). Usually, deleting these objects is the
|
|
best course of action, but there is currently no safe way for Wagtail
|
|
to do that at migration time.
|
|
"""
|
|
model_class = self.specific_class
|
|
|
|
if model_class is None:
|
|
# The codebase and database are out of sync (e.g. the model exists
|
|
# on a different git branch and migrations were not applied or
|
|
# reverted before switching branches). So, the best we can do is
|
|
# return the page in it's current form.
|
|
return self
|
|
|
|
if isinstance(self, model_class):
|
|
# self is already an instance of the most specific class.
|
|
return self
|
|
|
|
if deferred:
|
|
# Generate a tuple of values in the order expected by __init__(),
|
|
# with missing values substituted with DEFERRED ()
|
|
values = tuple(
|
|
getattr(self, f.attname, self.pk if f.primary_key else DEFERRED)
|
|
for f in model_class._meta.concrete_fields
|
|
)
|
|
# Create object from known attribute values
|
|
specific_obj = model_class(*values)
|
|
specific_obj._state.adding = self._state.adding
|
|
else:
|
|
# Fetch object from database
|
|
specific_obj = model_class._default_manager.get(id=self.id)
|
|
|
|
# Copy non-field attribute values
|
|
if copy_attrs is not None:
|
|
for attr in (attr for attr in copy_attrs if attr in self.__dict__):
|
|
setattr(specific_obj, attr, getattr(self, attr))
|
|
else:
|
|
exclude = copy_attrs_exclude or ()
|
|
for k, v in ((k, v) for k, v in self.__dict__.items() if k not in exclude):
|
|
# only set values that haven't already been set
|
|
specific_obj.__dict__.setdefault(k, v)
|
|
|
|
return specific_obj
|
|
|
|
@cached_property
|
|
def specific(self):
|
|
"""
|
|
Returns this object in its most specific subclassed form with all field
|
|
values fetched from the database. The result is cached in memory.
|
|
"""
|
|
return self.get_specific()
|
|
|
|
@cached_property
|
|
def specific_deferred(self):
|
|
"""
|
|
Returns this object in its most specific subclassed form without any
|
|
additional field values being fetched from the database. The result
|
|
is cached in memory.
|
|
"""
|
|
return self.get_specific(deferred=True)
|
|
|
|
@cached_property
|
|
def specific_class(self):
|
|
"""
|
|
Return the class that this object would be if instantiated in its
|
|
most specific form.
|
|
|
|
If the model class can no longer be found in the codebase, and the
|
|
relevant ``ContentType`` has been removed by a database migration,
|
|
the return value will be ``None``.
|
|
|
|
If the model class can no longer be found in the codebase, but the
|
|
relevant ``ContentType`` is still present in the database (usually a
|
|
result of switching between git branches without running or reverting
|
|
database migrations beforehand), the return value will be ``None``.
|
|
"""
|
|
return self.cached_content_type.model_class()
|
|
|
|
@property
|
|
def is_specific(self):
|
|
return self.specific_class is not None and isinstance(self, self.specific_class)
|
|
|
|
@property
|
|
def cached_content_type(self):
|
|
"""
|
|
Return this object's ``content_type`` value from the ``ContentType``
|
|
model's cached manager, which will avoid a database query if the
|
|
content type is already in memory.
|
|
"""
|
|
return ContentType.objects.get_for_id(self.content_type_id)
|