Source code for MangAdventure.search

"""Functions used for searching."""

from __future__ import annotations

from typing import TYPE_CHECKING, List, NamedTuple, Tuple

from django.db.models import Count, Max, Q, Sum
from django.utils import timezone as tz

from reader.models import Series

if TYPE_CHECKING:  # pragma: no cover
    from django.db.models.query import QuerySet
    from django.http import HttpRequest


class _SearchParams(NamedTuple):
    """
    A class that represents search parameters.

    :cvar query: The value of the ``query`` parameter.
    :cvar author: The value of the ``author`` parameter.
    :cvar status: The value of the ``status`` parameter.
    :cvar categories: The values of the ``categories`` parameter
                      as a tuple of included/excluded categories.
    """
    query: str
    author: str
    status: str
    categories: Tuple[List[str], List[str]]

    def __bool__(self) -> bool:
        """
        Check whether the parameters can be used in a filter.

        :return: ``True`` if any parameter has a usable value.
        """
        return bool(
            self.query != '' or
            self.author != '' or
            self.status != '' or
            self.categories != ([], [])
        )


[docs]def parse(request: HttpRequest) -> _SearchParams: """ Parse a request and return a :obj:`~collections.namedtuple` of search parameters. :param request: The original request. :return: The parameters of the request. """ categories = request.GET.get('categories', '').split(',') return _SearchParams( query=request.GET.get('q', '').strip(), author=request.GET.get('author', '').strip(), status=request.GET.get('status', '').lower().strip(), categories=( [c.lower() for c in categories if c and c[0] != '-'], [c[1:].lower() for c in categories if c and c[0] == '-'] ) )
[docs]def qsfilter(params: _SearchParams) -> Q: """ Create a `queryset filter`_ from the given search parameters. :param params: A :obj:`~collections.namedtuple` of parameters. :return: The created queryset filter. .. _`queryset filter`: https://docs.djangoproject.com/en/4.1/ topics/db/queries/#complex-lookups-with-q """ filters = Q() if params.query: filters = ( Q(title__icontains=params.query) | Q(aliases__name__icontains=params.query) ) if params.author: filters &= ( Q(authors__name__icontains=params.author) | Q(artists__name__icontains=params.author) | Q(authors__aliases__name__icontains=params.author) | Q(artists__aliases__name__icontains=params.author) ) if params.status and params.status != 'any': filters &= Q(status=params.status) included, excluded = params.categories if excluded: filters &= ~Q(categories__in=excluded) if included: filters &= Q(categories__in=included) return filters
[docs]def query(params: _SearchParams) -> QuerySet: """ Get a queryset of :class:`~reader.models.Series` from the given search parameters. :param params: A :obj:`~collections.namedtuple` of parameters. :return: A queryset of series matching the given parameters. """ if not params: return Series.objects.none() q = Q(chapters__published__lte=tz.now()) return Series.objects.annotate( # type: ignore chapter_count=Count('chapters', filter=q), latest_upload=Max('chapters__published', filter=q), views=Sum('chapters__views', distinct=True) ).complex_filter( qsfilter(params) & Q(chapter_count__gt=0) ).defer( 'licensed', 'manager', 'created', 'modified' ).distinct()
[docs]def get_response(request: HttpRequest) -> QuerySet: """ Get a queryset of :class:`~reader.models.Series` from the given request. :param request: The original request. :return: A queryset of series matching the parameters of the request. """ if slug := request.GET.get('slug'): return Series.objects.filter(slug=slug) if params := parse(request): return query(params) return Series.objects.all()
__all__ = ['parse', 'qsfilter', 'query', 'get_response']