2020-01-02 17:07:50 +01:00
|
|
|
from typing import Generic, Iterable, Type, TypeVar
|
2020-01-02 16:01:13 +01:00
|
|
|
|
2020-01-02 17:07:50 +01:00
|
|
|
from dal import autocomplete # type: ignore
|
|
|
|
from django.db.models import Model, Q
|
2020-01-02 16:01:13 +01:00
|
|
|
|
2020-01-02 17:07:50 +01:00
|
|
|
M = TypeVar("M", bound=Model)
|
|
|
|
|
|
|
|
|
|
|
|
class ModelSearch(Generic[M]):
|
2020-01-02 16:01:13 +01:00
|
|
|
"""Basic search engine for models based on filtering.
|
|
|
|
|
2020-01-03 17:26:12 +01:00
|
|
|
As the type hints indicate, the class is generic with respect to the model. This
|
|
|
|
means that the ``search`` method only returns instances of the model specified as
|
|
|
|
the ``model`` class attribute in subclasses.
|
|
|
|
|
|
|
|
The ``search_fields`` attributes indicates which fields to search in during the
|
|
|
|
search.
|
|
|
|
|
|
|
|
Example:
|
|
|
|
|
|
|
|
>>> from django.contrib.auth.models import User
|
|
|
|
>>>
|
|
|
|
>>> class UserSearch(ModelSearch):
|
|
|
|
... model = User
|
|
|
|
... search_fields = ["username", "first_name", "last_name"]
|
|
|
|
>>>
|
|
|
|
>>> user_search = UserSearch() # has type ModelSearch[User]
|
|
|
|
>>> user_search.search(["toto", "foo"]) # returns a queryset of Users
|
2020-01-02 16:01:13 +01:00
|
|
|
"""
|
|
|
|
|
2020-01-02 17:07:50 +01:00
|
|
|
model: Type[M]
|
|
|
|
search_fields: Iterable[str]
|
2020-01-02 16:01:13 +01:00
|
|
|
|
2020-01-02 17:07:50 +01:00
|
|
|
def get_queryset_filter(self, keywords: Iterable[str]) -> Q:
|
2020-01-02 16:01:13 +01:00
|
|
|
filter_q = Q()
|
|
|
|
|
|
|
|
if not keywords:
|
|
|
|
return filter_q
|
|
|
|
|
|
|
|
for keyword in keywords:
|
|
|
|
kw_filter = Q()
|
|
|
|
for field in self.search_fields:
|
|
|
|
kw_filter |= Q(**{"{}__icontains".format(field): keyword})
|
|
|
|
filter_q &= kw_filter
|
|
|
|
|
|
|
|
return filter_q
|
|
|
|
|
2020-01-02 17:07:50 +01:00
|
|
|
def search(self, keywords: Iterable[str]) -> Iterable[M]:
|
2020-01-02 16:01:13 +01:00
|
|
|
"""Returns the queryset of model instances matching all the keywords.
|
|
|
|
|
|
|
|
The semantic of the search is the following: a model instance appears in the
|
|
|
|
search results iff all of the keywords given as arguments occur in at least one
|
|
|
|
of the search fields.
|
|
|
|
"""
|
|
|
|
|
|
|
|
return self.model.objects.filter(self.get_queryset_filter(keywords))
|
|
|
|
|
|
|
|
|
|
|
|
class Select2QuerySetView(ModelSearch, autocomplete.Select2QuerySetView):
|
|
|
|
"""Compatibility layer between ModelSearch and Select2QuerySetView."""
|
|
|
|
|
|
|
|
def get_queryset(self):
|
|
|
|
keywords = self.q.split()
|
|
|
|
return super().search(keywords)
|