import abc
from django.apps import apps
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.db.models import F
from django.utils.functional import cached_property
from .base import GenericPrefetchRelatedDescriptor
from .base import GenericSinglePrefetchRelatedDescriptorMixin
[docs]class TopChildDescriptor(GenericSinglePrefetchRelatedDescriptorMixin, GenericPrefetchRelatedDescriptor):
"""
An abstract class for creating prefetchable descriptors which correspond
to the top child in a group of children associated to a parent model.
For example, consider a descriptor for the most recent message in a
conversation. In this case, the children would be the messages, and
the parent would be the conversation. The ordering used to determine
the "top child" would be ``-added``.
"""
[docs] @abc.abstractmethod
def get_child_model(self):
"""
returns the :class:`~django.db.models.model` class for the
children.
"""
[docs] @abc.abstractmethod
def get_parent_model(self):
"""
returns the :class:`~django.db.models.model` class for the
parents.
"""
[docs] @abc.abstractmethod
def get_child_order_by(self):
"""
returns a tuple which will be used to place an ordering on the
children so that we can return the "top" one.
:rtype: tuple
"""
[docs] @abc.abstractmethod
def get_parent_relation(self):
"""
returns the string which specifies how to associate a parent
model to a child.
for example, if the parent were :class:`common.models.user` and the
child were :class:`services.models.service`, then this should be
``'provider__user'``.
:rtype: str
"""
[docs] def get_prefetch_model_class(self):
"""
Returns the model class of the objects that are prefetched
by this descriptor.
:returns: subclass of :class:`django.db.models.model`
"""
return self.get_child_model()
[docs] def get_child_filter_args(self):
"""
returns a tuple of all of the argument filters which should be
used to filter the possible children returned.
:rtype: tuple
"""
return ()
[docs] def get_child_filter_kwargs(self, **kwargs):
"""
returns a dictionary of all of the keyword argument filters
which should be used to filter the possible children returned.
:param dict kwargs: any overrides for the default filter
:rtype: dict
"""
return dict({self.get_parent_relation(): models.OuterRef("pk")}, **kwargs)
[docs] def get_subquery(self):
"""
returns a :class:`queryset` for all of the child models which
should be considered.
:rtype: :class:`queryset`
"""
return (
self.get_child_model()
.objects.filter(*self.get_child_filter_args(), **self.get_child_filter_kwargs())
.order_by(*self.get_child_order_by())
.values_list("pk", flat=True)
)
[docs] def get_top_child_pks(self, parent_pks):
"""
Returns a queryset for the primary keys of the top children for
the parent models whose primary keys are in *parent_pks*.
:param list parent_pks: a list of primary keys for the parent
models whose children we want to fetch.
:rtype: :class:`QuerySet`
"""
return (
self.get_parent_model()
.objects.annotate(top_child_pk=models.Subquery(self.get_subquery()[:1], output_field=models.IntegerField()))
.filter(pk__in=parent_pks)
.values_list("top_child_pk", flat=True)
)
@cached_property
def parent_pk_annotation(self):
"""
Returns the name of the attribute which will be annotated
on child instances and will correspond to the primary key
of the associated parent.
:rtype: str
"""
return "_{}_".format(type(self).__name__.lower())
[docs] def filter_queryset_for_instances(self, queryset, instances):
"""
Returns a :class:`QuerySet` which returns the top children
for each of the parents in *instances*.
.. note::
This does not filter the set of child models which are
included in the "consideration set". To do that, please
override :meth:`get_child_filter_args` and
:meth:`get_child_filter_kwargs`.
:param QuerySet queryset: the queryset of child objects to filter for
*instances*
:param list instances: a list of the parent models whose
children we want to fetch.
:rtype: :class:`django.db.models.QuerySet`
"""
parent_pks = [obj.pk for obj in instances]
return queryset.filter(pk__in=list(self.get_top_child_pks(parent_pks))).annotate(
**{self.parent_pk_annotation: F(self.get_parent_relation())}
)
[docs] def get_join_value_for_instance(self, parent):
"""
Returns the value used to associate the *parent* with the
child fetched during the prefetching process.
In this case, it is the primary key of the parent.
:rtype: int
"""
return parent.pk
[docs]class TopChildDescriptorFromFieldBase(TopChildDescriptor):
"""
A subclass of :class:`TopChildDescriptor` for use when the
children are related to the parent by a foreign key. In that
case, anyone implementing a subclass of this only needs to
implement :meth:`get_child_field`.
"""
[docs] @abc.abstractmethod
def get_child_field(self):
"""
Returns the field on the child model which is a foreign key
to the parent model.
:rtype: :class:`django.db.models.fields.Field`
"""
@cached_property
def child_field(self):
return self.get_child_field()
[docs] def get_child_model(self):
return self.child_field.model
[docs] def get_parent_model(self):
return self.child_field.related_model
[docs] def get_parent_relation(self):
return self.child_field.name
[docs]class TopChildDescriptorFromField(TopChildDescriptorFromFieldBase):
def __init__(self, field, order_by):
self._field = field
self._order_by = order_by
super().__init__()
[docs] def get_child_field(self):
if isinstance(self._field, str):
model_string, field_name = self._field.rsplit(".", 1)
model = apps.get_model(model_string)
self._field = model._meta.get_field(field_name)
return self._field
[docs] def get_child_order_by(self):
return self._order_by
[docs]class TopChildDescriptorFromGenericRelationBase(TopChildDescriptor):
"""
A subclass of :class:`TopChildDescriptor` for use when the children
are described by a
:class:`django.contrib.contenttypes.fields.GenericRelation`.
"""
[docs] @abc.abstractmethod
def get_child_field(self):
"""
Returns the generic relation on the parent model for the children.
:rtype: :class:`django.contrib.contenttypes.fields.GenericRelation`
"""
@cached_property
def child_field(self):
return self.get_child_field()
@cached_property
def content_type(self):
"""
Returns the content type of the parent model.
"""
return ContentType.objects.get_for_model(self.get_parent_model())
[docs] def get_child_model(self):
"""
Returns the :class:`~django.db.models.Model` class for the
children.
"""
return self.child_field.remote_field.model
[docs] def get_parent_model(self):
"""
Returns the :class:`~django.db.models.Model` class for the parent.
"""
return self.child_field.model
[docs] def get_parent_relation(self):
"""
Returns the name of the field on the child corresponding to the
object primary key.
:rtype: str
"""
return self.child_field.object_id_field_name
[docs] def apply_content_type_filter(self, queryset):
"""
Filters the (child) *queryset* to only be those that correspond
to :attr:`content_type`.
:rtype: :class:`django.db.models.QuerySet`
"""
return queryset.filter(**{self.child_field.content_type_field_name: self.content_type.id})
[docs] def get_queryset(self, queryset=None):
"""
Returns a :class:`QuerySet` which returns the top children
for each of the parents who have primary keys in *parent_pks*.
:rtype: :class:`django.db.models.QuerySet`
"""
queryset = super().get_queryset(queryset=queryset)
return self.apply_content_type_filter(queryset)
[docs] def get_subquery(self):
"""
Returns a :class:`QuerySet` for all of the child models which
should be considered.
:rtype: :class:`django.db.models.QuerySet`
"""
subquery = super().get_subquery()
return self.apply_content_type_filter(subquery)
[docs]class TopChildDescriptorFromGenericRelation(TopChildDescriptorFromGenericRelationBase):
"""
For further customization,
"""
def __init__(self, generic_relation, order_by):
self._generic_relation = generic_relation
self._order_by = order_by
super().__init__()
[docs] def get_child_field(self):
return getattr(self.model, self._generic_relation.name).field
[docs] def get_child_order_by(self):
return self._order_by