Autocompletion: new de-duplication mechanism
This commit is contained in:
parent
637572ab58
commit
c7ca96bce5
4 changed files with 73 additions and 26 deletions
|
@ -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()),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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()),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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()),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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 this avoids showing the same user twice (here and in the
|
||||||
|
... # 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…
Reference in a new issue