Merge branch 'kerl/factor_autocompletion_views1' into 'master'
Mécanisme de dé-duplication des résultats plus souple pour l'autocomplétion See merge request klub-dev-ens/gestioCOF!427
This commit is contained in:
commit
68ccd4722f
13 changed files with 87 additions and 39 deletions
|
@ -63,7 +63,7 @@ linters:
|
||||||
- pip install --upgrade black isort flake8
|
- pip install --upgrade black isort flake8
|
||||||
script:
|
script:
|
||||||
- black --check .
|
- black --check .
|
||||||
- isort --recursive --check-only --diff bda bds clubs cof events gestioncof kfet petitscours provisioning shared
|
- isort --check --diff .
|
||||||
# Print errors only
|
# Print errors only
|
||||||
- flake8 --exit-zero bda bds clubs cof events gestioncof kfet petitscours provisioning shared
|
- flake8 --exit-zero bda bds clubs cof events gestioncof kfet petitscours provisioning shared
|
||||||
cache:
|
cache:
|
||||||
|
|
|
@ -48,7 +48,7 @@ if type isort &>/dev/null; then
|
||||||
ISORT_OUTPUT="/tmp/gc-isort-output.log"
|
ISORT_OUTPUT="/tmp/gc-isort-output.log"
|
||||||
touch $ISORT_OUTPUT
|
touch $ISORT_OUTPUT
|
||||||
|
|
||||||
if ! echo "$STAGED_PYTHON_FILES" | xargs -d'\n' isort --check-only &>$ISORT_OUTPUT; then
|
if ! echo "$STAGED_PYTHON_FILES" | xargs -d'\n' isort --check &>$ISORT_OUTPUT; then
|
||||||
echo "$STAGED_PYTHON_FILES" | xargs -d'\n' isort &>$ISORT_OUTPUT
|
echo "$STAGED_PYTHON_FILES" | xargs -d'\n' isort &>$ISORT_OUTPUT
|
||||||
printf "Reformatted.\n"
|
printf "Reformatted.\n"
|
||||||
formatter_updated=1
|
formatter_updated=1
|
||||||
|
|
|
@ -15,6 +15,9 @@ class BDSMemberSearch(autocomplete.ModelSearch):
|
||||||
qset_filter &= Q(bds__is_member=True)
|
qset_filter &= Q(bds__is_member=True)
|
||||||
return qset_filter
|
return qset_filter
|
||||||
|
|
||||||
|
def result_uuid(self, user):
|
||||||
|
return user.username
|
||||||
|
|
||||||
|
|
||||||
class BDSOthersSearch(autocomplete.ModelSearch):
|
class BDSOthersSearch(autocomplete.ModelSearch):
|
||||||
model = User
|
model = User
|
||||||
|
@ -25,12 +28,15 @@ class BDSOthersSearch(autocomplete.ModelSearch):
|
||||||
qset_filter &= Q(bds__isnull=True) | Q(bds__is_member=False)
|
qset_filter &= Q(bds__isnull=True) | Q(bds__is_member=False)
|
||||||
return qset_filter
|
return qset_filter
|
||||||
|
|
||||||
|
def result_uuid(self, user):
|
||||||
|
return user.username
|
||||||
|
|
||||||
|
|
||||||
class BDSSearch(autocomplete.Compose):
|
class BDSSearch(autocomplete.Compose):
|
||||||
search_units = [
|
search_units = [
|
||||||
("members", "username", BDSMemberSearch),
|
("members", BDSMemberSearch()),
|
||||||
("others", "username", BDSOthersSearch),
|
("others", BDSOthersSearch()),
|
||||||
("clippers", "clipper", autocomplete.LDAPSearch),
|
("clippers", autocomplete.LDAPSearch()),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,12 +2,12 @@
|
||||||
Django development settings for the cof project.
|
Django development settings for the cof project.
|
||||||
The settings that are not listed here are imported from .common
|
The settings that are not listed here are imported from .common
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from .common import * # NOQA
|
|
||||||
from .common import BASE_DIR, INSTALLED_APPS
|
from .common import BASE_DIR, INSTALLED_APPS
|
||||||
|
|
||||||
|
from .common import * # NOQA
|
||||||
|
|
||||||
# ---
|
# ---
|
||||||
# BDS-only Django settings
|
# BDS-only Django settings
|
||||||
# ---
|
# ---
|
||||||
|
|
|
@ -2,10 +2,8 @@
|
||||||
Django development settings for the cof project.
|
Django development settings for the cof project.
|
||||||
The settings that are not listed here are imported from .common
|
The settings that are not listed here are imported from .common
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from .common import * # NOQA
|
|
||||||
from .common import (
|
from .common import (
|
||||||
AUTHENTICATION_BACKENDS,
|
AUTHENTICATION_BACKENDS,
|
||||||
BASE_DIR,
|
BASE_DIR,
|
||||||
|
@ -15,6 +13,8 @@ from .common import (
|
||||||
import_secret,
|
import_secret,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from .common import * # NOQA
|
||||||
|
|
||||||
# ---
|
# ---
|
||||||
# COF-specific secrets
|
# COF-specific secrets
|
||||||
# ---
|
# ---
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
"""Django local development settings."""
|
"""Django local development settings."""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from . import bds_prod
|
from . import bds_prod
|
||||||
from .cof_prod import * # NOQA
|
|
||||||
from .cof_prod import BASE_DIR, INSTALLED_APPS, MIDDLEWARE, TESTING
|
from .cof_prod import BASE_DIR, INSTALLED_APPS, MIDDLEWARE, TESTING
|
||||||
|
|
||||||
|
from .cof_prod import * # NOQA
|
||||||
|
|
||||||
# ---
|
# ---
|
||||||
# Merge COF and BDS configs
|
# Merge COF and BDS configs
|
||||||
# ---
|
# ---
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
"""
|
"""
|
||||||
Fichier principal de configuration des urls du projet GestioCOF
|
Fichier principal de configuration des urls du projet GestioCOF
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.conf.urls.i18n import i18n_patterns
|
from django.conf.urls.i18n import i18n_patterns
|
||||||
from django.conf.urls.static import static
|
from django.conf.urls.static import static
|
||||||
|
@ -20,6 +19,10 @@ urlpatterns = [
|
||||||
]
|
]
|
||||||
|
|
||||||
if "gestioncof" in settings.INSTALLED_APPS:
|
if "gestioncof" in settings.INSTALLED_APPS:
|
||||||
|
from django_js_reverse.views import urls_js
|
||||||
|
from wagtail.admin import urls as wagtailadmin_urls
|
||||||
|
from wagtail.documents import urls as wagtaildocs_urls
|
||||||
|
|
||||||
from gestioncof import csv_views, views as gestioncof_views
|
from gestioncof import csv_views, views as gestioncof_views
|
||||||
from gestioncof.autocomplete import autocomplete
|
from gestioncof.autocomplete import autocomplete
|
||||||
from gestioncof.urls import (
|
from gestioncof.urls import (
|
||||||
|
@ -29,9 +32,6 @@ if "gestioncof" in settings.INSTALLED_APPS:
|
||||||
export_patterns,
|
export_patterns,
|
||||||
surveys_patterns,
|
surveys_patterns,
|
||||||
)
|
)
|
||||||
from django_js_reverse.views import urls_js
|
|
||||||
from wagtail.admin import urls as wagtailadmin_urls
|
|
||||||
from wagtail.documents import urls as wagtaildocs_urls
|
|
||||||
|
|
||||||
# Also includes BdA, K-Fêt, etc.
|
# Also includes BdA, K-Fêt, etc.
|
||||||
urlpatterns += [
|
urlpatterns += [
|
||||||
|
|
|
@ -12,6 +12,7 @@ class GestioncofConfig(AppConfig):
|
||||||
|
|
||||||
def register_config(self):
|
def register_config(self):
|
||||||
import djconfig
|
import djconfig
|
||||||
|
|
||||||
from .forms import GestioncofConfigForm
|
from .forms import GestioncofConfigForm
|
||||||
|
|
||||||
djconfig.register(GestioncofConfigForm)
|
djconfig.register(GestioncofConfigForm)
|
||||||
|
|
|
@ -18,6 +18,9 @@ class COFMemberSearch(autocomplete.ModelSearch):
|
||||||
qset_filter &= Q(profile__is_cof=True)
|
qset_filter &= Q(profile__is_cof=True)
|
||||||
return qset_filter
|
return qset_filter
|
||||||
|
|
||||||
|
def result_uuid(self, user):
|
||||||
|
return user.username
|
||||||
|
|
||||||
|
|
||||||
class COFOthersSearch(autocomplete.ModelSearch):
|
class COFOthersSearch(autocomplete.ModelSearch):
|
||||||
model = User
|
model = User
|
||||||
|
@ -28,12 +31,15 @@ class COFOthersSearch(autocomplete.ModelSearch):
|
||||||
qset_filter &= Q(profile__is_cof=False)
|
qset_filter &= Q(profile__is_cof=False)
|
||||||
return qset_filter
|
return qset_filter
|
||||||
|
|
||||||
|
def result_uuid(self, user):
|
||||||
|
return user.username
|
||||||
|
|
||||||
|
|
||||||
class COFSearch(autocomplete.Compose):
|
class COFSearch(autocomplete.Compose):
|
||||||
search_units = [
|
search_units = [
|
||||||
("members", "username", COFMemberSearch),
|
("members", COFMemberSearch()),
|
||||||
("others", "username", COFOthersSearch),
|
("others", COFOthersSearch()),
|
||||||
("clippers", "clipper", autocomplete.LDAPSearch),
|
("clippers", autocomplete.LDAPSearch()),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ class KFetConfig(AppConfig):
|
||||||
|
|
||||||
def register_config(self):
|
def register_config(self):
|
||||||
import djconfig
|
import djconfig
|
||||||
|
|
||||||
from kfet.forms import KFetConfigForm
|
from kfet.forms import KFetConfigForm
|
||||||
|
|
||||||
djconfig.register(KFetConfigForm)
|
djconfig.register(KFetConfigForm)
|
||||||
|
|
|
@ -23,6 +23,9 @@ class KfetAccountSearch(autocomplete.ModelSearch):
|
||||||
qset_filter &= Q(profile__account_kfet__isnull=False)
|
qset_filter &= Q(profile__account_kfet__isnull=False)
|
||||||
return qset_filter
|
return qset_filter
|
||||||
|
|
||||||
|
def result_uuid(self, user):
|
||||||
|
return user.username
|
||||||
|
|
||||||
|
|
||||||
class COFMemberSearch(autocomplete.ModelSearch):
|
class COFMemberSearch(autocomplete.ModelSearch):
|
||||||
model = User
|
model = User
|
||||||
|
@ -33,6 +36,9 @@ class COFMemberSearch(autocomplete.ModelSearch):
|
||||||
qset_filter &= Q(profile__account_kfet__isnull=True) & Q(profile__is_cof=True)
|
qset_filter &= Q(profile__account_kfet__isnull=True) & Q(profile__is_cof=True)
|
||||||
return qset_filter
|
return qset_filter
|
||||||
|
|
||||||
|
def result_uuid(self, user):
|
||||||
|
return user.username
|
||||||
|
|
||||||
|
|
||||||
class OthersSearch(autocomplete.ModelSearch):
|
class OthersSearch(autocomplete.ModelSearch):
|
||||||
model = User
|
model = User
|
||||||
|
@ -43,13 +49,16 @@ class OthersSearch(autocomplete.ModelSearch):
|
||||||
qset_filter &= Q(profile__account_kfet__isnull=True) & Q(profile__is_cof=False)
|
qset_filter &= Q(profile__account_kfet__isnull=True) & Q(profile__is_cof=False)
|
||||||
return qset_filter
|
return qset_filter
|
||||||
|
|
||||||
|
def result_uuid(self, user):
|
||||||
|
return user.username
|
||||||
|
|
||||||
|
|
||||||
class KfetAutocomplete(autocomplete.Compose):
|
class KfetAutocomplete(autocomplete.Compose):
|
||||||
search_units = [
|
search_units = [
|
||||||
("kfet", "username", KfetAccountSearch),
|
("kfet", KfetAccountSearch()),
|
||||||
("users_cof", "username", COFMemberSearch),
|
("users_cof", COFMemberSearch()),
|
||||||
("users_notcof", "username", OthersSearch),
|
("users_notcof", OthersSearch()),
|
||||||
("clippers", "clipper", autocomplete.LDAPSearch),
|
("clippers", autocomplete.LDAPSearch()),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -39,5 +39,4 @@ known_django = django
|
||||||
known_first_party = bda,bds,clubs,cof,events,gestioncof,kfet,petitscours,shared
|
known_first_party = bda,bds,clubs,cof,events,gestioncof,kfet,petitscours,shared
|
||||||
line_length = 88
|
line_length = 88
|
||||||
multi_line_output = 3
|
multi_line_output = 3
|
||||||
not_skip = __init__.py
|
|
||||||
sections = FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER
|
sections = FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER
|
||||||
|
|
|
@ -21,14 +21,32 @@ class SearchUnit:
|
||||||
|
|
||||||
A search unit should implement a `search` method taking a list of keywords as
|
A search unit should implement a `search` method taking a list of keywords as
|
||||||
argument and returning an iterable of search results.
|
argument and returning an iterable of search results.
|
||||||
|
|
||||||
|
It might optionally implement the following methods and attributes:
|
||||||
|
|
||||||
|
- result_uuid (method): a callable that takes one result as an input and returns an
|
||||||
|
identifier that is globally unique across search units for this object.
|
||||||
|
This is used to compare results coming from different search units in the
|
||||||
|
`Compose` class. For instance, if the same user can be returned by the LDAP
|
||||||
|
search and a model search instance, using the clipper login as a UUID in both
|
||||||
|
units avoids this user to be returned twice by `Compose`.
|
||||||
|
Returning `None` means that the object should be considered unique.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Mandatory method
|
||||||
|
|
||||||
def search(self, _keywords):
|
def search(self, _keywords):
|
||||||
raise NotImplementedError(
|
raise NotImplementedError(
|
||||||
"Class implementing the SeachUnit interface should implement the search "
|
"Class implementing the SearchUnit interface should implement the search "
|
||||||
"method"
|
"method"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Optional attributes and methods
|
||||||
|
|
||||||
|
def result_uuid(self, result):
|
||||||
|
"""A universal unique identifier for the search results."""
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
# ---
|
# ---
|
||||||
# Model-based search
|
# Model-based search
|
||||||
|
@ -148,6 +166,9 @@ class LDAPSearch(SearchUnit):
|
||||||
django_logger.error("An LDAP error occurred", exc_info=err)
|
django_logger.error("An LDAP error occurred", exc_info=err)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
def result_uuid(self, clipper):
|
||||||
|
return clipper.clipper
|
||||||
|
|
||||||
|
|
||||||
# ---
|
# ---
|
||||||
# Composition of autocomplete units
|
# Composition of autocomplete units
|
||||||
|
@ -157,18 +178,13 @@ class LDAPSearch(SearchUnit):
|
||||||
class Compose:
|
class Compose:
|
||||||
"""Search with several units and remove duplicate results.
|
"""Search with several units and remove duplicate results.
|
||||||
|
|
||||||
The `search_units` class attribute should be a list of tuples of the form `(name,
|
The `search_units` class attribute should be a list of pairs of the form `(name,
|
||||||
uniq_key, search_unit)`.
|
search_unit)`.
|
||||||
|
|
||||||
The `search` method produces a dictionary whose keys are the `name`s given in
|
The `search` method produces a dictionary whose keys are the `name`s given in
|
||||||
`search_units` and whose values are iterables produced by the different search
|
`search_units` and whose values are iterables produced by the different search
|
||||||
units.
|
units.
|
||||||
|
|
||||||
The `uniq_key`s are used to remove duplicates: for instance, say that search unit
|
|
||||||
1 has `uniq_key = "username"` and search unit 2 has `uniq_key = "clipper"`, then
|
|
||||||
search results from unit 2 whose `.clipper` attribute is equal to the
|
|
||||||
`.username` attribute of some result from unit 1 are omitted.
|
|
||||||
|
|
||||||
Typical Example:
|
Typical Example:
|
||||||
|
|
||||||
>>> from django.contrib.auth.models import User
|
>>> from django.contrib.auth.models import User
|
||||||
|
@ -176,11 +192,17 @@ class Compose:
|
||||||
>>> class UserSearch(ModelSearch):
|
>>> class UserSearch(ModelSearch):
|
||||||
... model = User
|
... model = User
|
||||||
... search_fields = ["username", "first_name", "last_name"]
|
... search_fields = ["username", "first_name", "last_name"]
|
||||||
|
...
|
||||||
|
... def result_uuid(self, user):
|
||||||
|
... # Assuming that `.username` stores the clipper login of already
|
||||||
|
... # registered users, this avoids showing the same user twice (here and in
|
||||||
|
... # then ldap unit).
|
||||||
|
... return user.username
|
||||||
>>>
|
>>>
|
||||||
>>> class UserAndClipperSearch(Compose):
|
>>> class UserAndClipperSearch(Compose):
|
||||||
... search_units = [
|
... search_units = [
|
||||||
... ("users", "username", UserSearch),
|
... ("users", UserSearch()),
|
||||||
... ("clippers", "clipper", LDAPSearch),
|
... ("clippers", LDAPSearch()),
|
||||||
... ]
|
... ]
|
||||||
|
|
||||||
In this example, clipper accounts that already have an associated user (i.e. with a
|
In this example, clipper accounts that already have an associated user (i.e. with a
|
||||||
|
@ -190,11 +212,15 @@ class Compose:
|
||||||
search_units = []
|
search_units = []
|
||||||
|
|
||||||
def search(self, keywords):
|
def search(self, keywords):
|
||||||
uniq_results = set()
|
seen_uuids = set()
|
||||||
results = {}
|
results = {}
|
||||||
for name, uniq_key, search_unit in self.search_units:
|
for name, search_unit in self.search_units:
|
||||||
res = search_unit().search(keywords)
|
uniq_res = []
|
||||||
res = [r for r in res if getattr(r, uniq_key) not in uniq_results]
|
for r in search_unit.search(keywords):
|
||||||
uniq_results |= set((getattr(r, uniq_key) for r in res))
|
uuid = search_unit.result_uuid(r)
|
||||||
results[name] = res
|
if uuid is None or uuid not in seen_uuids:
|
||||||
|
uniq_res.append(r)
|
||||||
|
if uuid is not None:
|
||||||
|
seen_uuids.add(uuid)
|
||||||
|
results[name] = uniq_res
|
||||||
return results
|
return results
|
||||||
|
|
Loading…
Add table
Reference in a new issue