93 lines
3.7 KiB
Python
93 lines
3.7 KiB
Python
from django.db import models
|
|
from django.db.models.fields.related_descriptors import ManyToManyDescriptor
|
|
from django.utils.functional import cached_property
|
|
|
|
|
|
class FilterableManyToManyDescriptor(ManyToManyDescriptor):
|
|
def __init__(self, *args, **kwargs):
|
|
self.q_filter = kwargs.pop('q_filter', None)
|
|
super().__init__(*args, **kwargs)
|
|
|
|
@classmethod
|
|
def _get_manager_with_default_filtering(cls, manager, q_filter):
|
|
"""Wrap the manager class to add an extra filter returned via get_queryset."""
|
|
|
|
class ManagerWithFiltering(manager):
|
|
def get_queryset(self):
|
|
# Check the queryset caching django uses during these lookups -
|
|
# we only want to add the q_filter the first time.
|
|
from_cache = self.prefetch_cache_name in getattr(
|
|
self.instance, '_prefetched_objects_cache', {}
|
|
)
|
|
qs = super().get_queryset()
|
|
if not from_cache and q_filter:
|
|
# Here is where we add the filter.
|
|
qs = qs.filter(q_filter)
|
|
return qs
|
|
|
|
return ManagerWithFiltering
|
|
|
|
@cached_property
|
|
def related_manager_cls(self):
|
|
cls = super().related_manager_cls
|
|
return self._get_manager_with_default_filtering(cls, self.q_filter)
|
|
|
|
|
|
class FilterableManyToManyField(models.fields.related.ManyToManyField):
|
|
"""This class builds on ManyToManyField to allow us to filter the relation
|
|
to a subset, similar to how we use the unfiltered manager to filter out
|
|
deleted instances of other foreign keys.
|
|
|
|
It takes an additional Q object arg (q_filter) which will be applied to the
|
|
queryset on *both* sides of the many-to-many relation. Because it's
|
|
applied to both sides the filter will typically be on the ManyToManyField
|
|
itself.
|
|
|
|
For example, class A and class B have a ManyToMany relation between them,
|
|
via class M (so M would have a foreign key to both A and B).
|
|
For an instance a of A, a.m would be:
|
|
`B.objects.filter(a__in=a.id, q_filter)`,
|
|
and for an instance b of B, b.m would be:
|
|
`A.objects.filter(b__in=b.id, q_filter)`.
|
|
If `q_filter` was `Q(m__deleted=False)` it would filter out all soft
|
|
deleted instances of M.
|
|
"""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
self.q_filter = kwargs.pop('q_filter', None)
|
|
super().__init__(*args, **kwargs)
|
|
|
|
def contribute_to_class(self, cls, name, **kwargs):
|
|
"""Override `setattr`.
|
|
|
|
All we're doing here is overriding the `setattr` so it creates an
|
|
instance of FilterableManyToManyDescriptor rather than
|
|
ManyToManyDescriptor, and pass down the q_filter property.
|
|
"""
|
|
super().contribute_to_class(cls, name, **kwargs)
|
|
# Add the descriptor for the m2m relation.
|
|
setattr(
|
|
cls,
|
|
self.name,
|
|
FilterableManyToManyDescriptor(
|
|
self.remote_field, reverse=False, q_filter=self.q_filter
|
|
),
|
|
)
|
|
|
|
def contribute_to_related_class(self, cls, related):
|
|
"""Override `setattr`.
|
|
|
|
All we're doing here is overriding the `setattr` so it creates an
|
|
instance of FilterableManyToManyDescriptor rather than
|
|
ManyToManyDescriptor, and pass down the q_filter property.
|
|
"""
|
|
super().contribute_to_related_class(cls, related)
|
|
if not self.remote_field.is_hidden() and not related.related_model._meta.swapped:
|
|
setattr(
|
|
cls,
|
|
related.get_accessor_name(),
|
|
FilterableManyToManyDescriptor(
|
|
self.remote_field, reverse=True, q_filter=self.q_filter
|
|
),
|
|
)
|