import typing as _t
import operator
from functools import reduce
from django.conf import settings
from django.db.models import Q
from django.utils.functional import SimpleLazyObject
from django_filters import rest_framework as filters
from django_filters import CharFilter
from vstutils.utils import lazy_translate as __
from .. import utils
id_help = __('A unique integer value (or comma separated list) identifying this instance.')
name_help = __('A name string value (or comma separated list) of instance.')
name_filter_method = "contains" if settings.CASE_SENSITIVE_API_FILTER else 'icontains'
def _extra_query_search(field, value, stype):
vals = field.rsplit("__", 1)
field_name, tp = vals[0], (list(vals)[1:2] + [""])[0]
negate = tp.upper() == "NOT"
if negate:
field = field_name
field += f"__{stype}"
value = value.split(",") if stype == "in" else value
return {field: value}, negate
def _extra_search(queryset, field, value, stype):
query_filter, negate = _extra_query_search(field, value, stype)
if negate:
return queryset.exclude(**query_filter)
return queryset.filter(**query_filter)
[docs]
def name_filter(queryset, field, value):
"""
Method for searching by part of name. Uses `LIKE` DB condition or `contains` qs-expression.
:param queryset: model queryset for filtration.
:type queryset: django.db.models.query.QuerySet
:param field: field name in FilterSet. Also supports `__not` suffix.
:type field: str
:param value: searching part of name.
:type value: str
:return: filtered queryset.
:rtype: django.db.models.query.QuerySet
"""
return _extra_search(queryset, field, value, name_filter_method)
[docs]
class FkFilterHandler:
"""
Simple handler for filtering by relational fields.
:param related_pk: Field name of related model's primary key. Default is 'id'.
:param related_name: Field name of related model's charfield. Default is 'name'.
:param pk_handler: Changes handler for checking value before search. Sends "0" if handler falls. Default is 'int()'.
Example:
.. sourcecode:: python
class CustomFilterSet(filters.FilterSet):
author = CharFilter(method=vst_filters.FkFilterHandler(related_pk='pk', related_name='email'))
Where ``author`` is ForeignKey to `User` and you want to search by primary key and email.
"""
__slots__ = ('related_pk', 'related_name', 'pk_handler')
def __init__(self, related_pk: str = 'id', related_name: str = 'name', pk_handler: _t.Callable = int):
self.related_pk = related_pk
self.related_name = related_name
self.pk_handler = pk_handler
def __get_q(self, kw, negate):
query = Q(**kw)
if negate:
query.negate()
return query
def list_of_q(self, field, value):
field_split = field.rsplit("__", 1)
if len(field_split) > 1 and field_split[-1] == 'not':
field, suffix = field_split[0], '__not'
else:
suffix = ''
if self.related_pk:
pk_value = SimpleLazyObject(
utils.raise_context_decorator_with_default(default='0')(
lambda: str(self.pk_handler(value)) or "0"
)
)
yield self.__get_q(*_extra_query_search(f'{field}__{self.related_pk}{suffix}', pk_value, "in"))
if self.related_name:
yield self.__get_q(*_extra_query_search(f'{field}__{self.related_name}{suffix}', value, name_filter_method))
def __call__(self, queryset, field, value):
return queryset.filter(
reduce(
operator.or_,
self.list_of_q(field, value)
)
)
[docs]
class DefaultIDFilter(filters.FilterSet):
"""
Basic filterset to search by id. Provides a search for multiple values separated by commas.
Uses :func:`.extra_filter` in fields.
"""
id = CharFilter(method=extra_filter, help_text=id_help, label="Primary keys")
id__not = CharFilter(method=extra_filter, help_text=id_help, label="Exclude primary keys")
[docs]
class DefaultNameFilter(filters.FilterSet):
"""
Basic filterset to search by part of name. Uses `LIKE` DB condition by :func:`.name_filter`.
"""
name = CharFilter(method=name_filter, help_text=name_help, label="Name")
name__not = CharFilter(method=name_filter, help_text=name_help, label='Exclude name')