From 914888d18aee68f813b3ce5ce863e72c7a460aa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Thu, 2 Jan 2020 16:01:13 +0100 Subject: [PATCH 1/5] Merge the utils and shared apps --- .gitlab-ci.yml | 4 ++-- bda/views.py | 2 +- gestioncof/views.py | 2 +- setup.cfg | 3 +-- shared/views/autocomplete.py | 45 ++++++++++++++++++++++++++++++++++++ utils/__init__.py | 0 utils/views/__init__.py | 0 utils/views/autocomplete.py | 25 -------------------- 8 files changed, 50 insertions(+), 31 deletions(-) create mode 100644 shared/views/autocomplete.py delete mode 100644 utils/__init__.py delete mode 100644 utils/views/__init__.py delete mode 100644 utils/views/autocomplete.py diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a8bece7d..9bad2072 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -61,9 +61,9 @@ linters: - pip install --upgrade black isort flake8 script: - black --check . - - isort --recursive --check-only --diff bda bds clubs cof events gestioncof kfet petitscours provisioning shared utils + - isort --recursive --check-only --diff bda bds clubs cof events gestioncof kfet petitscours provisioning shared # Print errors only - - flake8 --exit-zero bda bds clubs cof events gestioncof kfet petitscours provisioning shared utils + - flake8 --exit-zero bda bds clubs cof events gestioncof kfet petitscours provisioning shared cache: key: linters paths: diff --git a/bda/views.py b/bda/views.py index f33b7013..f799360d 100644 --- a/bda/views.py +++ b/bda/views.py @@ -42,7 +42,7 @@ from bda.models import ( Tirage, ) from gestioncof.decorators import BuroRequiredMixin, buro_required, cof_required -from utils.views.autocomplete import Select2QuerySetView +from shared.views.autocomplete import Select2QuerySetView @cof_required diff --git a/gestioncof/views.py b/gestioncof/views.py index ced35cfc..07a0ae03 100644 --- a/gestioncof/views.py +++ b/gestioncof/views.py @@ -58,7 +58,7 @@ from gestioncof.models import ( SurveyQuestion, SurveyQuestionAnswer, ) -from utils.views.autocomplete import Select2QuerySetView +from shared.views.autocomplete import Select2QuerySetView class HomeView(LoginRequiredMixin, TemplateView): diff --git a/setup.cfg b/setup.cfg index 100ddb22..1a9901cb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -9,7 +9,6 @@ source = kfet petitscours shared - utils omit = *migrations* *test*.py @@ -37,7 +36,7 @@ default_section = THIRDPARTY force_grid_wrap = 0 include_trailing_comma = true known_django = django -known_first_party = bda,bds,clubs,cof,events,gestioncof,kfet,petitscours,shared,utils +known_first_party = bda,bds,clubs,cof,events,gestioncof,kfet,petitscours,shared line_length = 88 multi_line_output = 3 not_skip = __init__.py diff --git a/shared/views/autocomplete.py b/shared/views/autocomplete.py new file mode 100644 index 00000000..7fc7a886 --- /dev/null +++ b/shared/views/autocomplete.py @@ -0,0 +1,45 @@ +from dal import autocomplete +from django.db.models import Q + + +class ModelSearch: + """Basic search engine for models based on filtering. + + Subclasses should override the ``model`` class attribute and specify the list of + search fields to be searched in. + """ + + model = None + search_fields = [] + + def get_queryset_filter(self, keywords): + 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): + """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) diff --git a/utils/__init__.py b/utils/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/utils/views/__init__.py b/utils/views/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/utils/views/autocomplete.py b/utils/views/autocomplete.py deleted file mode 100644 index c5d51343..00000000 --- a/utils/views/autocomplete.py +++ /dev/null @@ -1,25 +0,0 @@ -from dal import autocomplete -from django.db.models import Q - - -class Select2QuerySetView(autocomplete.Select2QuerySetView): - model = None - search_fields = [] - - def get_queryset_filter(self): - q = self.q - filter_q = Q() - - if not q: - return filter_q - - words = q.split() - - for word in words: - for field in self.search_fields: - filter_q |= Q(**{"{}__icontains".format(field): word}) - - return filter_q - - def get_queryset(self): - return self.model.objects.filter(self.get_queryset_filter()) From d2c6c9da7ae51fa993cca7146ebf4ef6dbd1b822 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Thu, 2 Jan 2020 17:07:50 +0100 Subject: [PATCH 2/5] Type hints in shared.views.autocomplete --- shared/views/autocomplete.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/shared/views/autocomplete.py b/shared/views/autocomplete.py index 7fc7a886..270eae63 100644 --- a/shared/views/autocomplete.py +++ b/shared/views/autocomplete.py @@ -1,18 +1,22 @@ -from dal import autocomplete -from django.db.models import Q +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: +class ModelSearch(Generic[M]): """Basic search engine for models based on filtering. Subclasses should override the ``model`` class attribute and specify the list of search fields to be searched in. """ - model = None - search_fields = [] + model: Type[M] + search_fields: Iterable[str] - def get_queryset_filter(self, keywords): + def get_queryset_filter(self, keywords: Iterable[str]) -> Q: filter_q = Q() if not keywords: @@ -26,7 +30,7 @@ class ModelSearch: return filter_q - def search(self, keywords): + 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 From e45ee3fb40358036211116d803547788f0b43ee4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Fri, 3 Jan 2020 17:26:12 +0100 Subject: [PATCH 3/5] More documentation for ModelSearch --- shared/views/autocomplete.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/shared/views/autocomplete.py b/shared/views/autocomplete.py index 270eae63..e8d90590 100644 --- a/shared/views/autocomplete.py +++ b/shared/views/autocomplete.py @@ -9,8 +9,23 @@ M = TypeVar("M", bound=Model) class ModelSearch(Generic[M]): """Basic search engine for models based on filtering. - Subclasses should override the ``model`` class attribute and specify the list of - search fields to be searched in. + 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 """ model: Type[M] From a259b04d9cf9b3d8ddd4fe3f96a16cd75a5d6a16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Fri, 3 Jan 2020 17:29:55 +0100 Subject: [PATCH 4/5] Explicative comment about the Type[M] annotation --- shared/views/autocomplete.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/shared/views/autocomplete.py b/shared/views/autocomplete.py index e8d90590..708fe554 100644 --- a/shared/views/autocomplete.py +++ b/shared/views/autocomplete.py @@ -28,6 +28,8 @@ class ModelSearch(Generic[M]): >>> 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] From b8cd5f1da50a60cce446f3d2c1b270f6d3462c66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Wed, 12 Feb 2020 19:01:08 +0100 Subject: [PATCH 5/5] Drop type hints in shared.views.autocomplete --- shared/views/autocomplete.py | 29 ++++++++++------------------- 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/shared/views/autocomplete.py b/shared/views/autocomplete.py index 708fe554..095dc3f8 100644 --- a/shared/views/autocomplete.py +++ b/shared/views/autocomplete.py @@ -1,20 +1,13 @@ -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) +from dal import autocomplete +from django.db.models import Q -class ModelSearch(Generic[M]): +class ModelSearch: """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. + The class should be configured through its ``model`` class attribute: the ``search`` + method will return a queryset of instances of this model. The ``search_fields`` + attributes indicates which fields to search in. Example: @@ -28,12 +21,10 @@ class ModelSearch(Generic[M]): >>> 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] + model = None + search_fields = [] - def get_queryset_filter(self, keywords: Iterable[str]) -> Q: + def get_queryset_filter(self, keywords): filter_q = Q() if not keywords: @@ -47,7 +38,7 @@ class ModelSearch(Generic[M]): return filter_q - def search(self, keywords: Iterable[str]) -> Iterable[M]: + def search(self, keywords): """Returns the queryset of model instances matching all the keywords. The semantic of the search is the following: a model instance appears in the