from typing import Generic, Iterable, Type, TypeVar from dal import autocomplete # type: ignore from django.db.models import Model, Q M = TypeVar("M", bound=Model) class ModelSearch(Generic[M]): """Basic search engine for models based on filtering. 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 """ # This says that `model` is the class corresponding to the type variable M (or a # subclass). model: Type[M] search_fields: Iterable[str] def get_queryset_filter(self, keywords: Iterable[str]) -> Q: 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 def search(self, keywords: Iterable[str]) -> Iterable[M]: """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)