# encoding: utf-8
from functools import wraps
import six
from django.db.models import BooleanField
from ..exceptions import QueryablePropertyError
from ..managers import QueryablePropertiesQuerySetMixin
from ..utils.internal import InjectableMixin, QueryPath
REMAINING_LOOKUPS = '*' #: A Constant that can be used instead of lookup names to match all remaining lookups.
class LookupFilterMeta(type):
"""
Metaclass for classes that use the :class:`LookupFilterMixin` to detect the
individual registered filter methods and make them available to the main
filter method.
"""
def __new__(mcs, name, bases, attrs):
# Find all methods that have been marked with lookups via the
# `lookup_filter` decorator.
mappings = {}
for attr_name, attr in six.iteritems(attrs):
if callable(attr) and hasattr(attr, '_lookups'):
for lookup in attr._lookups:
mappings[lookup] = attr_name
# Let the class construction take care of the lookup mappings of the
# base class(es) and add the ones from the current class to them.
cls = super(LookupFilterMeta, mcs).__new__(mcs, name, bases, attrs)
cls._lookup_mappings = dict(cls._lookup_mappings, **mappings)
return cls
[docs]
class LookupFilterMixin(six.with_metaclass(LookupFilterMeta, InjectableMixin)):
"""
A mixin for queryable properties that allows to implement queryset
filtering via individual methods for different lookups.
"""
# Avoid overriding the __reduce__ implementation of queryable properties.
_dynamic_pickling = False
# Stores mappings of lookups to the names of their corresponding filter
# functions.
_lookup_mappings = {}
remaining_lookups_via_parent = False
def __init__(self, *args, **kwargs):
self.lookup_mappings = {lookup: getattr(self, name) for lookup, name in six.iteritems(self._lookup_mappings)}
super(LookupFilterMixin, self).__init__(*args, **kwargs)
[docs]
@classmethod
def lookup_filter(cls, *lookups):
"""
Decorator for individual filter methods of classes that use the
:class:`LookupFilterMixin` to register the decorated methods for the
given lookups.
:param str lookups: The lookups to register the decorated method for.
:return: The actual internal decorator.
:rtype: function
"""
def decorator(func):
func._lookups = lookups # Store the lookups on the function to be able to read them in the meta class.
return func
return decorator
[docs]
@classmethod
def boolean_filter(cls, method):
"""
Decorator for individual filter methods of classes that use the
:class:`LookupFilterMixin` to register the methods that are simple
boolean filters (i.e. the filter can only be called with a ``True`` or
``False`` value). This automatically restricts the usable lookups to
``exact``. Decorated methods should not expect the ``lookup`` and
``value`` parameters and should always return a ``Q`` object
representing the positive (i.e. ``True``) filter case. The decorator
will automatically negate the condition if the filter was called with a
``False`` value.
:param function method: The method to decorate.
:return: The decorated method.
:rtype: function
"""
@wraps(method)
def filter_wrapper(self, model, lookup, value):
"""Actual filter method that negates the condition if required."""
condition = method(self, model)
if not value:
condition.negate()
return condition
lookup_decorator = cls.lookup_filter('exact')
return lookup_decorator(filter_wrapper)
def get_filter(self, cls, lookup, value):
# Resolve the correct method to call for the given lookup in this order:
# 1. Check if there is an explicit method for the given lookup.
# 2. Check if a method is configured for REMAINING_LOOKUPS.
# 3. Check if a fallback to the parent class implementation is allowed.
method = self.lookup_mappings.get(lookup) or self.lookup_mappings.get(REMAINING_LOOKUPS)
if not method:
if not self.remaining_lookups_via_parent:
raise QueryablePropertyError(
'Queryable property "{prop}" does not implement filtering with lookup "{lookup}".'
.format(prop=self, lookup=lookup)
)
method = super(LookupFilterMixin, self).get_filter
return method(cls, lookup, value)
# Aliases to allow the usage of the decorators without the "LookupFilterMixin."
# prefix.
boolean_filter = LookupFilterMixin.boolean_filter
lookup_filter = LookupFilterMixin.lookup_filter
[docs]
class SetterMixin(object):
"""
A mixin for queryable properties that also define a setter.
"""
[docs]
def set_value(self, obj, value): # pragma: no cover
"""
Setter method for the queryable property, which will be called when the
property is write-accessed.
:param django.db.models.Model obj: The object on which the property was
accessed.
:param value: The value to set.
"""
raise NotImplementedError()
[docs]
class AnnotationMixin(InjectableMixin):
"""
A mixin for queryable properties that allows to add an annotation to
represent them to querysets.
"""
# Avoid overriding the __reduce__ implementation of queryable properties.
_dynamic_pickling = False
filter_requires_annotation = True
@property
def admin_order_field(self):
"""
Return the field name for the ordering in the admin, which is simply
the property's name since it's annotatable.
:return: The field name for ordering in the admin.
:rtype: str
"""
return self.name
[docs]
def get_annotation(self, cls): # pragma: no cover
"""
Construct an annotation representing this property that can be added
to querysets of the model associated with this property.
:param type cls: The model class of which a queryset should be
annotated.
:return: An annotation object.
"""
raise NotImplementedError()
def get_filter(self, cls, lookup, value):
# Since annotations can be filtered like regular fields, a Q object
# that simply passes the filter through can be used.
return (QueryPath(self.name) + lookup).build_filter(value)
[docs]
class AnnotationGetterMixin(AnnotationMixin):
"""
A mixin for queryable properties that support annotation and use their
annotation even to provide the value for their getter (i.e. perform a query
to retrieve the getter value).
"""
def __init__(self, cached=None, *args, **kwargs):
"""
Initialize a new queryable property based that uses the
:class:`AnnotationGetterMixin`.
:param cached: Determines if values obtained by the getter should be
cached (similar to ``cached_property``). A value of None
means using the default value.
"""
super(AnnotationGetterMixin, self).__init__(*args, **kwargs)
if cached is not None:
self.cached = cached
def get_value(self, obj):
queryset = self.get_queryset_for_object(obj).distinct().select_properties(self.name)
return queryset.values_list(self.name, flat=True).get()
[docs]
def get_queryset(self, model):
"""
Construct a base queryset for the given model class that can be used
to build queries in property code.
:param model: The model class to build the queryset for.
"""
# Inject the mixin to be able to use select_properties in the getter.
return QueryablePropertiesQuerySetMixin.inject_into_object(model._base_manager.all())
[docs]
def get_queryset_for_object(self, obj):
"""
Construct a base queryset that can be used to retrieve the getter value
for the given object.
:param django.db.models.Model obj: The object to build the queryset
for.
:return: A base queryset for the correct model that is already filtered
for the given object.
:rtype: django.db.models.QuerySet
"""
return self.get_queryset(obj.__class__).filter(pk=obj.pk)
[docs]
class UpdateMixin(object):
"""
A mixin for queryable properties that allows to use themselves in update
queries.
"""
[docs]
def get_update_kwargs(self, cls, value): # pragma: no cover
"""
Resolve an update keyword argument for this property into the actual
keyword arguments to emulate an update using this property.
:param type cls: The model class of which an update query should be
performed.
:param value: The value passed to the update call for this property.
:return: The actual keyword arguments to set in the update call instead
of the given one.
:rtype: dict
"""
raise NotImplementedError()
class BooleanMixin(LookupFilterMixin):
"""
Internal mixin class for common properties that return boolean values,
which is intended to be used in conjunction with one of the annotation
mixins.
"""
filter_requires_annotation = False
def _get_condition(self, cls): # pragma: no cover
"""
Build the query filter condition for this boolean property, which is
used for both the filter and the annotation implementation.
:param type cls: The model class of which a queryset should be filtered
or annotated.
:return: The filter condition for this property.
:rtype: django.db.models.Q
"""
raise NotImplementedError()
@boolean_filter
def get_exact_filter(self, cls):
return self._get_condition(cls)
def get_annotation(self, cls):
from django.db.models import Case, When
return Case(
When(self._get_condition(cls), then=True),
default=False,
output_field=BooleanField()
)
class SubqueryMixin(AnnotationGetterMixin):
"""
Internal mixin class for common properties that are based on custom
subqueries.
"""
def __init__(self, queryset, **kwargs):
"""
Initialize a new subquery-based queryable property.
:param queryset: The internal queryset to use as the subquery or a
callable without arguments that generates the internal
queryset.
:type queryset: django.db.models.QuerySet | function
"""
self.queryset = queryset
super(SubqueryMixin, self).__init__(**kwargs)
def _build_subquery(self, queryset): # pragma: no cover
"""
Build the subquery annotation that should be used to represent this
queryable property in querysets.
:param django.db.models.QuerySet queryset: The internal queryset to
base the annotation on.
:return: The subquery annotation.
:rtype: django.db.models.expressions.Subquery
"""
raise NotImplementedError()
def get_annotation(self, cls):
queryset = self.queryset
if callable(queryset):
queryset = queryset()
return self._build_subquery(queryset)