Generic autocompletion view
This commit is contained in:
parent
30783d677b
commit
e7517195cd
20 changed files with 272 additions and 309 deletions
|
@ -1,5 +1,6 @@
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from shared import autocomplete
|
from shared import autocomplete
|
||||||
|
|
||||||
|
@ -9,6 +10,7 @@ User = get_user_model()
|
||||||
class BDSMemberSearch(autocomplete.ModelSearch):
|
class BDSMemberSearch(autocomplete.ModelSearch):
|
||||||
model = User
|
model = User
|
||||||
search_fields = ["username", "first_name", "last_name"]
|
search_fields = ["username", "first_name", "last_name"]
|
||||||
|
verbose_name = _("Membres du BDS")
|
||||||
|
|
||||||
def get_queryset_filter(self, *args, **kwargs):
|
def get_queryset_filter(self, *args, **kwargs):
|
||||||
qset_filter = super().get_queryset_filter(*args, **kwargs)
|
qset_filter = super().get_queryset_filter(*args, **kwargs)
|
||||||
|
@ -18,10 +20,14 @@ class BDSMemberSearch(autocomplete.ModelSearch):
|
||||||
def result_uuid(self, user):
|
def result_uuid(self, user):
|
||||||
return user.username
|
return user.username
|
||||||
|
|
||||||
|
def result_link(self, user):
|
||||||
|
return "#TODO"
|
||||||
|
|
||||||
|
|
||||||
class BDSOthersSearch(autocomplete.ModelSearch):
|
class BDSOthersSearch(autocomplete.ModelSearch):
|
||||||
model = User
|
model = User
|
||||||
search_fields = ["username", "first_name", "last_name"]
|
search_fields = ["username", "first_name", "last_name"]
|
||||||
|
verbose_name = _("Non-membres du BDS")
|
||||||
|
|
||||||
def get_queryset_filter(self, *args, **kwargs):
|
def get_queryset_filter(self, *args, **kwargs):
|
||||||
qset_filter = super().get_queryset_filter(*args, **kwargs)
|
qset_filter = super().get_queryset_filter(*args, **kwargs)
|
||||||
|
@ -31,12 +37,20 @@ class BDSOthersSearch(autocomplete.ModelSearch):
|
||||||
def result_uuid(self, user):
|
def result_uuid(self, user):
|
||||||
return user.username
|
return user.username
|
||||||
|
|
||||||
|
def result_link(self, user):
|
||||||
|
return "#TODO"
|
||||||
|
|
||||||
|
|
||||||
|
class BDSLDAPSearch(autocomplete.LDAPSearch):
|
||||||
|
def result_link(self, clipper):
|
||||||
|
return "#TODO"
|
||||||
|
|
||||||
|
|
||||||
class BDSSearch(autocomplete.Compose):
|
class BDSSearch(autocomplete.Compose):
|
||||||
search_units = [
|
search_units = [
|
||||||
("members", BDSMemberSearch()),
|
("members", BDSMemberSearch()),
|
||||||
("others", BDSOthersSearch()),
|
("others", BDSOthersSearch()),
|
||||||
("clippers", autocomplete.LDAPSearch()),
|
("clippers", BDSLDAPSearch()),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,75 +1,21 @@
|
||||||
|
{% extends "shared/search_results.html" %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load search_utils %}
|
|
||||||
|
|
||||||
<ul>
|
{% block extra_section %}
|
||||||
{% if members %}
|
<li class="autocomplete-header">
|
||||||
<li class="autocomplete-header">
|
{% if not results %}
|
||||||
<span class="autocomplete-item">{% trans "Membres" %}</span>
|
<span class="autocomplete-item">
|
||||||
</li>
|
{% trans "Aucune correspondance trouvée" %}
|
||||||
{% for user in members %}
|
</span>
|
||||||
{% if forloop.counter < 5 %}
|
{% else %}
|
||||||
<li class="autocomplete-value">
|
<span class="autocomplete-item">
|
||||||
<a class="autocomplete-item" href="#TODO">
|
{% trans "Pas dans la liste ?" %}
|
||||||
{{ user|highlight_user:q }}
|
</span>
|
||||||
</a>
|
{% endif %}
|
||||||
</li>
|
|
||||||
{% elif forloop.counter == 5 %}
|
|
||||||
<li class="autocomplete-more">
|
|
||||||
<span class="autocomplete-item">...</span>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if others %}
|
|
||||||
<li class="autocomplete-header">
|
|
||||||
<span class="autocomplete-item">{% trans "Non-membres" %}</span>
|
|
||||||
</li>
|
|
||||||
{% for user in others %}
|
|
||||||
{% if forloop.counter < 5 %}
|
|
||||||
<li class="autocomplete-value">
|
|
||||||
<a class="autocomplete-item" href="#TODO">
|
|
||||||
{{ user|highlight_user:q }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% elif forloop.counter == 5 %}
|
|
||||||
<li class="autocomplete-more">
|
|
||||||
<span class="autocomplete-item">...</span>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if clippers %}
|
|
||||||
<li class="autocomplete-header">
|
|
||||||
<span class="autocomplete-item">{% trans "Utilisateurs <tt>clipper</tt>" %}</span>
|
|
||||||
</li>
|
|
||||||
{% for clipper in clippers %}
|
|
||||||
{% if forloop.counter < 5 %}
|
|
||||||
<li class="autocomplete-value">
|
|
||||||
<a class="autocomplete-item" href="#TODO">
|
|
||||||
{{ clipper|highlight_clipper:q }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% elif forloop.counter == 5 %}
|
|
||||||
<li class="autocomplete-more">
|
|
||||||
<span class="autocomplete-item">...</span>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if total %}
|
|
||||||
<li class="autocomplete-header">
|
|
||||||
<span class="autocomplete-item">{% trans "Pas dans la liste ?" %}</span>
|
|
||||||
</li>
|
|
||||||
{% else %}
|
|
||||||
<li class="autocomplete-header">
|
|
||||||
<span class="autocomplete-item">{% trans "Aucune correspondance trouvée" %}</span>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<li class="autocomplete-new">
|
|
||||||
<a class="autocomplete-item" href="#TODO">{% trans "Créer un compte" %}</a>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
<li class="autocomplete-new">
|
||||||
|
<a class="autocomplete-item" href="#TODO">
|
||||||
|
{% trans "Créer un compte" %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endblock %}
|
||||||
|
|
|
@ -5,5 +5,5 @@ from bds import views
|
||||||
app_name = "bds"
|
app_name = "bds"
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", views.Home.as_view(), name="home"),
|
path("", views.Home.as_view(), name="home"),
|
||||||
path("autocomplete", views.AutocompleteView.as_view(), name="autocomplete"),
|
path("autocomplete", views.BDSAutocompleteView.as_view(), name="autocomplete"),
|
||||||
]
|
]
|
||||||
|
|
16
bds/views.py
16
bds/views.py
|
@ -1,23 +1,13 @@
|
||||||
from django.http import Http404
|
|
||||||
from django.views.generic import TemplateView
|
from django.views.generic import TemplateView
|
||||||
|
|
||||||
from bds.autocomplete import bds_search
|
from bds.autocomplete import bds_search
|
||||||
from bds.mixins import StaffRequiredMixin
|
from bds.mixins import StaffRequiredMixin
|
||||||
|
from shared.views import AutocompleteView
|
||||||
|
|
||||||
|
|
||||||
class AutocompleteView(StaffRequiredMixin, TemplateView):
|
class BDSAutocompleteView(StaffRequiredMixin, AutocompleteView):
|
||||||
template_name = "bds/search_results.html"
|
template_name = "bds/search_results.html"
|
||||||
|
search_composer = bds_search
|
||||||
def get_context_data(self, *args, **kwargs):
|
|
||||||
ctx = super().get_context_data(*args, **kwargs)
|
|
||||||
if "q" not in self.request.GET:
|
|
||||||
raise Http404
|
|
||||||
q = self.request.GET["q"]
|
|
||||||
ctx["q"] = q
|
|
||||||
results = bds_search.search(q.split())
|
|
||||||
ctx.update(results)
|
|
||||||
ctx["total"] = sum((len(r) for r in results.values()))
|
|
||||||
return ctx
|
|
||||||
|
|
||||||
|
|
||||||
class Home(StaffRequiredMixin, TemplateView):
|
class Home(StaffRequiredMixin, TemplateView):
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from shared import autocomplete
|
from shared import autocomplete
|
||||||
|
|
||||||
|
@ -9,6 +11,7 @@ User = get_user_model()
|
||||||
class COFMemberSearch(autocomplete.ModelSearch):
|
class COFMemberSearch(autocomplete.ModelSearch):
|
||||||
model = User
|
model = User
|
||||||
search_fields = ["username", "first_name", "last_name"]
|
search_fields = ["username", "first_name", "last_name"]
|
||||||
|
verbose_name = _("Membres du COF")
|
||||||
|
|
||||||
def get_queryset_filter(self, *args, **kwargs):
|
def get_queryset_filter(self, *args, **kwargs):
|
||||||
qset_filter = super().get_queryset_filter(*args, **kwargs)
|
qset_filter = super().get_queryset_filter(*args, **kwargs)
|
||||||
|
@ -18,10 +21,14 @@ class COFMemberSearch(autocomplete.ModelSearch):
|
||||||
def result_uuid(self, user):
|
def result_uuid(self, user):
|
||||||
return user.username
|
return user.username
|
||||||
|
|
||||||
|
def result_link(self, user):
|
||||||
|
return reverse("user-registration", args=(user.username,))
|
||||||
|
|
||||||
|
|
||||||
class COFOthersSearch(autocomplete.ModelSearch):
|
class COFOthersSearch(autocomplete.ModelSearch):
|
||||||
model = User
|
model = User
|
||||||
search_fields = ["username", "first_name", "last_name"]
|
search_fields = ["username", "first_name", "last_name"]
|
||||||
|
verbose_name = _("Non-membres du COF")
|
||||||
|
|
||||||
def get_queryset_filter(self, *args, **kwargs):
|
def get_queryset_filter(self, *args, **kwargs):
|
||||||
qset_filter = super().get_queryset_filter(*args, **kwargs)
|
qset_filter = super().get_queryset_filter(*args, **kwargs)
|
||||||
|
@ -31,10 +38,21 @@ class COFOthersSearch(autocomplete.ModelSearch):
|
||||||
def result_uuid(self, user):
|
def result_uuid(self, user):
|
||||||
return user.username
|
return user.username
|
||||||
|
|
||||||
|
def result_link(self, user):
|
||||||
|
return reverse("user-registration", args=(user.username,))
|
||||||
|
|
||||||
|
|
||||||
|
class COFLDAPSearch(autocomplete.LDAPSearch):
|
||||||
|
def result_link(self, clipper):
|
||||||
|
return reverse("clipper-registration", args=(clipper.clipper, clipper.fullname))
|
||||||
|
|
||||||
|
|
||||||
class COFSearch(autocomplete.Compose):
|
class COFSearch(autocomplete.Compose):
|
||||||
search_units = [
|
search_units = [
|
||||||
("members", COFMemberSearch()),
|
("members", COFMemberSearch()),
|
||||||
("others", COFOthersSearch()),
|
("others", COFOthersSearch()),
|
||||||
("clippers", autocomplete.LDAPSearch()),
|
("clippers", COFLDAPSearch()),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
cof_search = COFSearch()
|
||||||
|
|
|
@ -1,56 +1,21 @@
|
||||||
{% load search_utils %}
|
{% extends "shared/search_results.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
<ul>
|
{% block extra_section %}
|
||||||
{% if members %}
|
<li class="autocomplete-header">
|
||||||
<li class="autocomplete-header">Membres</li>
|
{% if not results %}
|
||||||
{% for user in members %}
|
<span class="autocomplete-item">
|
||||||
{% if forloop.counter < 5 %}
|
{% trans "Aucune correspondance trouvée" %}
|
||||||
<li class="autocomplete-value">
|
</span>
|
||||||
<a href="{% url "user-registration" user.username %}">
|
{% else %}
|
||||||
{{ user|highlight_user:q }}
|
<span class="autocomplete-item">
|
||||||
</a>
|
{% trans "Pas dans la liste ?" %}
|
||||||
</li>
|
</span>
|
||||||
{% elif forloop.counter == 5 %}
|
{% endif %}
|
||||||
<li class="autocomplete-more">...</li>
|
</li>
|
||||||
{% endif %}
|
<li class="autocomplete-new">
|
||||||
{% endfor %}
|
<a class="autocomplete-item" href="{% url "empty-registration" %}">
|
||||||
{% endif %}
|
{% trans "Créer un compte" %}
|
||||||
|
</a>
|
||||||
{% if others %}
|
</li>
|
||||||
<li class="autocomplete-header">Non-membres</li>
|
{% endblock %}
|
||||||
{% for user in others %}
|
|
||||||
{% if forloop.counter < 5 %}
|
|
||||||
<li class="autocomplete-value">
|
|
||||||
<a href="{% url "user-registration" user.username %}">
|
|
||||||
{{ user|highlight_user:q }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% elif forloop.counter == 5 %}
|
|
||||||
<li class="autocomplete-more">...</li>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if clippers %}
|
|
||||||
<li class="autocomplete-header">Utilisateurs <tt>clipper</tt></li>
|
|
||||||
{% for clipper in clippers %}
|
|
||||||
{% if forloop.counter < 5 %}
|
|
||||||
<li class="autocomplete-value">
|
|
||||||
<a href="{% url "clipper-registration" clipper.clipper clipper.fullname %}">
|
|
||||||
{{ clipper|highlight_clipper:q }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% elif forloop.counter == 5 %}
|
|
||||||
<li class="autocomplete-more">...</li>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if total %}
|
|
||||||
<li class="autocomplete-header">Pas dans la liste ?</li>
|
|
||||||
{% else %}
|
|
||||||
<li class="autocomplete-header">Aucune correspondance trouvée</li>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<li><a href="{% url "empty-registration" %}">Créer un compte</a></li>
|
|
||||||
</ul>
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ from django.urls import reverse
|
||||||
from bda.models import Salle, Tirage
|
from bda.models import Salle, Tirage
|
||||||
from gestioncof.models import CalendarSubscription, Club, Event, Survey, SurveyAnswer
|
from gestioncof.models import CalendarSubscription, Club, Event, Survey, SurveyAnswer
|
||||||
from gestioncof.tests.mixins import MegaHelperMixin, ViewTestCaseMixin
|
from gestioncof.tests.mixins import MegaHelperMixin, ViewTestCaseMixin
|
||||||
from shared.autocomplete import Clipper
|
from shared.autocomplete import Clipper, LDAPSearch
|
||||||
from shared.tests.mixins import CSVResponseMixin, ICalMixin, MockLDAPMixin
|
from shared.tests.mixins import CSVResponseMixin, ICalMixin, MockLDAPMixin
|
||||||
|
|
||||||
from .utils import create_member, create_root, create_user
|
from .utils import create_member, create_root, create_user
|
||||||
|
@ -290,14 +290,30 @@ class RegistrationAutocompleteViewTests(MockLDAPMixin, ViewTestCaseMixin, TestCa
|
||||||
|
|
||||||
self.assertEqual(r.status_code, 200)
|
self.assertEqual(r.status_code, 200)
|
||||||
|
|
||||||
|
def extract(section):
|
||||||
|
return [r.verbose_name for r in section.entries]
|
||||||
|
|
||||||
|
others = []
|
||||||
|
members = []
|
||||||
|
clippers = []
|
||||||
|
for section in r.context["results"]:
|
||||||
|
if section.name == "others":
|
||||||
|
others = extract(section)
|
||||||
|
elif section.name == "members":
|
||||||
|
members = extract(section)
|
||||||
|
elif section.name == "clippers":
|
||||||
|
clippers = extract(section)
|
||||||
|
else:
|
||||||
|
raise ValueError("Unexpected section name: {}".format(section.name))
|
||||||
|
|
||||||
self.assertQuerysetEqual(
|
self.assertQuerysetEqual(
|
||||||
r.context["others"], map(repr, expected_others), ordered=False
|
others, map(str, expected_others), ordered=False, transform=str
|
||||||
)
|
)
|
||||||
self.assertQuerysetEqual(
|
self.assertQuerysetEqual(
|
||||||
r.context["members"], map(repr, expected_members), ordered=False,
|
members, map(str, expected_members), ordered=False, transform=str
|
||||||
)
|
)
|
||||||
self.assertCountEqual(
|
self.assertSetEqual(
|
||||||
map(str, r.context["clippers"]), map(str, expected_clippers)
|
set(clippers), set(map(LDAPSearch().result_verbose_name, expected_clippers))
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_username(self):
|
def test_username(self):
|
||||||
|
|
|
@ -59,7 +59,7 @@ from gestioncof.models import (
|
||||||
SurveyQuestion,
|
SurveyQuestion,
|
||||||
SurveyQuestionAnswer,
|
SurveyQuestionAnswer,
|
||||||
)
|
)
|
||||||
from shared.views import Select2QuerySetView
|
from shared.views import AutocompleteView, Select2QuerySetView
|
||||||
|
|
||||||
|
|
||||||
class HomeView(LoginRequiredMixin, TemplateView):
|
class HomeView(LoginRequiredMixin, TemplateView):
|
||||||
|
@ -948,14 +948,6 @@ class UserAutocompleteView(BuroRequiredMixin, Select2QuerySetView):
|
||||||
search_fields = ("username", "first_name", "last_name")
|
search_fields = ("username", "first_name", "last_name")
|
||||||
|
|
||||||
|
|
||||||
class RegistrationAutocompleteView(BuroRequiredMixin, TemplateView):
|
class RegistrationAutocompleteView(BuroRequiredMixin, AutocompleteView):
|
||||||
template_name = "gestioncof/search_results.html"
|
template_name = "gestioncof/search_results.html"
|
||||||
|
search_composer = COFSearch()
|
||||||
def get_context_data(self, *args, **kwargs):
|
|
||||||
ctx = super().get_context_data(*args, **kwargs)
|
|
||||||
if "q" not in self.request.GET:
|
|
||||||
raise Http404
|
|
||||||
q = self.request.GET["q"]
|
|
||||||
ctx["q"] = q
|
|
||||||
ctx.update(COFSearch().search(q.split()))
|
|
||||||
return ctx
|
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from shared import autocomplete
|
from shared import autocomplete
|
||||||
|
|
||||||
|
@ -14,12 +16,16 @@ class KfetAccountSearch(autocomplete.ModelSearch):
|
||||||
"last_name",
|
"last_name",
|
||||||
"profile__account_kfet__trigramme",
|
"profile__account_kfet__trigramme",
|
||||||
]
|
]
|
||||||
|
verbose_name = _("Comptes existants")
|
||||||
|
|
||||||
def get_queryset_filter(self, *args, **kwargs):
|
def get_queryset_filter(self, *args, **kwargs):
|
||||||
qset_filter = super().get_queryset_filter(*args, **kwargs)
|
qset_filter = super().get_queryset_filter(*args, **kwargs)
|
||||||
qset_filter &= Q(profile__account_kfet__isnull=False)
|
qset_filter &= Q(profile__account_kfet__isnull=False)
|
||||||
return qset_filter
|
return qset_filter
|
||||||
|
|
||||||
|
def result_verbose_name(self, user):
|
||||||
|
return "{} ({})".format(user, user.profile.account_kfet.trigramme)
|
||||||
|
|
||||||
def result_uuid(self, user):
|
def result_uuid(self, user):
|
||||||
return user.username
|
return user.username
|
||||||
|
|
||||||
|
@ -27,6 +33,7 @@ class KfetAccountSearch(autocomplete.ModelSearch):
|
||||||
class COFMemberSearch(autocomplete.ModelSearch):
|
class COFMemberSearch(autocomplete.ModelSearch):
|
||||||
model = User
|
model = User
|
||||||
search_fields = ["username", "first_name", "last_name"]
|
search_fields = ["username", "first_name", "last_name"]
|
||||||
|
verbose_name = _("Membres du COF")
|
||||||
|
|
||||||
def get_queryset_filter(self, *args, **kwargs):
|
def get_queryset_filter(self, *args, **kwargs):
|
||||||
qset_filter = super().get_queryset_filter(*args, **kwargs)
|
qset_filter = super().get_queryset_filter(*args, **kwargs)
|
||||||
|
@ -36,10 +43,14 @@ class COFMemberSearch(autocomplete.ModelSearch):
|
||||||
def result_uuid(self, user):
|
def result_uuid(self, user):
|
||||||
return user.username
|
return user.username
|
||||||
|
|
||||||
|
def result_link(self, user):
|
||||||
|
return reverse("kfet.account.create.fromuser", args=(user.username,))
|
||||||
|
|
||||||
|
|
||||||
class OthersSearch(autocomplete.ModelSearch):
|
class OthersSearch(autocomplete.ModelSearch):
|
||||||
model = User
|
model = User
|
||||||
search_fields = ["username", "first_name", "last_name"]
|
search_fields = ["username", "first_name", "last_name"]
|
||||||
|
verbose_name = _("Non-membres du COF")
|
||||||
|
|
||||||
def get_queryset_filter(self, *args, **kwargs):
|
def get_queryset_filter(self, *args, **kwargs):
|
||||||
qset_filter = super().get_queryset_filter(*args, **kwargs)
|
qset_filter = super().get_queryset_filter(*args, **kwargs)
|
||||||
|
@ -49,11 +60,25 @@ class OthersSearch(autocomplete.ModelSearch):
|
||||||
def result_uuid(self, user):
|
def result_uuid(self, user):
|
||||||
return user.username
|
return user.username
|
||||||
|
|
||||||
|
def result_link(self, user):
|
||||||
|
return reverse("kfet.account.create.fromuser", args=(user.username,))
|
||||||
|
|
||||||
|
|
||||||
|
class KfetLDAPSearch(autocomplete.LDAPSearch):
|
||||||
|
def result_link(self, clipper):
|
||||||
|
return reverse(
|
||||||
|
"kfet.account.create.fromclipper", args=(clipper.clipper, clipper.fullname)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class KfetAutocomplete(autocomplete.Compose):
|
class KfetAutocomplete(autocomplete.Compose):
|
||||||
search_units = [
|
search_units = [
|
||||||
("kfet", KfetAccountSearch()),
|
("kfet", KfetAccountSearch()),
|
||||||
("users_cof", COFMemberSearch()),
|
("users_cof", COFMemberSearch()),
|
||||||
("users_notcof", OthersSearch()),
|
("users_notcof", OthersSearch()),
|
||||||
("clippers", autocomplete.LDAPSearch()),
|
("clippers", KfetLDAPSearch()),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class KfetAccountOnlyAutocomplete(autocomplete.Compose):
|
||||||
|
search_units = [("kfet", KfetAccountSearch())]
|
||||||
|
|
|
@ -159,7 +159,7 @@
|
||||||
background:rgba(255,255,255,0.9);
|
background:rgba(255,255,255,0.9);
|
||||||
}
|
}
|
||||||
|
|
||||||
#search_results ul li.user_category {
|
#search_results ul li.autocomplete-header {
|
||||||
font-weight:bold;
|
font-weight:bold;
|
||||||
background:#c8102e;
|
background:#c8102e;
|
||||||
color:#fff;
|
color:#fff;
|
||||||
|
@ -178,7 +178,7 @@
|
||||||
text-decoration:none;
|
text-decoration:none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#search_results ul li span.text {
|
#search_results ul li span.autocomplete-item {
|
||||||
display:block;
|
display:block;
|
||||||
padding:5px 20px;
|
padding:5px 20px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,50 +0,0 @@
|
||||||
{% load kfet_tags %}
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<a href="{% url "kfet.account.create.empty" %}">
|
|
||||||
Créer un compte
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% if kfet %}
|
|
||||||
<li class="user_category"><span class="text">Comptes existants</span></li>
|
|
||||||
{% for user in kfet %}
|
|
||||||
<li><span class="text">{{ user.account_kfet.account }} [{{ user|highlight_user:q }}]</span></li>
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
{% if users_cof %}
|
|
||||||
<li class="user_category"><span class="text">Membres du COF</span></li>
|
|
||||||
{% for user in users_cof %}
|
|
||||||
<li>
|
|
||||||
<a href="{% url "kfet.account.create.fromuser" user.username %}">
|
|
||||||
{{ user|highlight_user:q }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
{% if users_notcof %}
|
|
||||||
<li class="user_category"><span class="text">Non-membres du COF</span></li>
|
|
||||||
{% for user in users_notcof %}
|
|
||||||
<li>
|
|
||||||
<a href="{% url "kfet.account.create.fromuser" user.username %}">
|
|
||||||
{{ user|highlight_user:q }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
{% if clippers %}
|
|
||||||
<li class="user_category"><span class="text">Utilisateurs clipper</span></li>
|
|
||||||
{% for clipper in clippers %}
|
|
||||||
<li>
|
|
||||||
<a href="{% url "kfet.account.create.fromclipper" clipper.clipper clipper.fullname%}">
|
|
||||||
{{ clipper|highlight_clipper:q }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
{% if not q %}
|
|
||||||
<li class="user_category"><span class="text">Pas de recherche, pas de résultats !</span></li>
|
|
||||||
{% elif not options %}
|
|
||||||
<li class="user_category"><span class="text">Aucune correspondance trouvée :-(</span></li>
|
|
||||||
{% endif %}
|
|
||||||
</ul>
|
|
|
@ -1,14 +0,0 @@
|
||||||
{% load kfet_tags %}
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
{% if accounts %}
|
|
||||||
{% for trigramme, user in accounts %}
|
|
||||||
<li class='choice'>{{ user|highlight_text:q }} (<span class="trigramme">{{ trigramme }}</span>)</li>
|
|
||||||
{% endfor %}
|
|
||||||
{% elif not q %}
|
|
||||||
<li class="user_category"><span class="text">Pas de recherche, pas de résultats !</span></li>
|
|
||||||
{% else %}
|
|
||||||
<li class="user_category"><span class="text">Aucune correspondance trouvée :-(</span></li>
|
|
||||||
{% endif %}
|
|
||||||
</ul>
|
|
||||||
|
|
|
@ -1,8 +1,4 @@
|
||||||
import re
|
|
||||||
|
|
||||||
from django import template
|
from django import template
|
||||||
from django.utils.html import escape
|
|
||||||
from django.utils.safestring import mark_safe
|
|
||||||
|
|
||||||
from ..utils import to_ukf
|
from ..utils import to_ukf
|
||||||
|
|
||||||
|
@ -11,40 +7,14 @@ register = template.Library()
|
||||||
register.filter("ukf", to_ukf)
|
register.filter("ukf", to_ukf)
|
||||||
|
|
||||||
|
|
||||||
@register.filter()
|
|
||||||
def highlight_text(text, q):
|
|
||||||
q2 = "|".join(re.escape(word) for word in q.split())
|
|
||||||
pattern = re.compile(r"(?P<filter>%s)" % q2, re.IGNORECASE)
|
|
||||||
regex = r"<span class='highlight_autocomplete'>\g<filter></span>"
|
|
||||||
return mark_safe(re.sub(pattern, regex, escape(text)))
|
|
||||||
|
|
||||||
|
|
||||||
@register.filter(is_safe=True)
|
|
||||||
def highlight_user(user, q):
|
|
||||||
if user.first_name and user.last_name:
|
|
||||||
text = "%s %s (%s)" % (user.first_name, user.last_name, user.username)
|
|
||||||
else:
|
|
||||||
text = user.username
|
|
||||||
return highlight_text(text, q)
|
|
||||||
|
|
||||||
|
|
||||||
@register.filter(is_safe=True)
|
|
||||||
def highlight_clipper(clipper, q):
|
|
||||||
if clipper.fullname:
|
|
||||||
text = "%s (%s)" % (clipper.fullname, clipper.clipper)
|
|
||||||
else:
|
|
||||||
text = clipper.clipper
|
|
||||||
return highlight_text(text, q)
|
|
||||||
|
|
||||||
|
|
||||||
@register.filter()
|
@register.filter()
|
||||||
def widget_type(field):
|
def widget_type(field):
|
||||||
return field.field.widget.__class__.__name__
|
return field.field.widget.__class__.__name__
|
||||||
|
|
||||||
|
|
||||||
@register.filter()
|
@register.filter()
|
||||||
def slice(l, start, end=None):
|
def slice(t, start, end=None):
|
||||||
if end is None:
|
if end is None:
|
||||||
end = start
|
end = start
|
||||||
start = 0
|
start = 0
|
||||||
return l[start:end]
|
return t[start:end]
|
||||||
|
|
|
@ -181,9 +181,15 @@ class AccountCreateAutocompleteViewTests(ViewTestCaseMixin, TestCase):
|
||||||
def test_ok(self):
|
def test_ok(self):
|
||||||
r = self.client.get(self.url, {"q": "first"})
|
r = self.client.get(self.url, {"q": "first"})
|
||||||
self.assertEqual(r.status_code, 200)
|
self.assertEqual(r.status_code, 200)
|
||||||
self.assertEqual(len(r.context["users_notcof"]), 0)
|
self.assertEqual(len(r.context["results"]), 1)
|
||||||
self.assertEqual(len(r.context["users_cof"]), 0)
|
(res,) = r.context["results"]
|
||||||
self.assertSetEqual(set(r.context["kfet"]), set([self.users["user"]]))
|
self.assertEqual(res.name, "kfet")
|
||||||
|
|
||||||
|
u = self.users["user"]
|
||||||
|
self.assertSetEqual(
|
||||||
|
{e.verbose_name for e in res.entries},
|
||||||
|
{"{} ({})".format(u, u.profile.account_kfet.trigramme)},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AccountSearchViewTests(ViewTestCaseMixin, TestCase):
|
class AccountSearchViewTests(ViewTestCaseMixin, TestCase):
|
||||||
|
@ -196,7 +202,12 @@ class AccountSearchViewTests(ViewTestCaseMixin, TestCase):
|
||||||
def test_ok(self):
|
def test_ok(self):
|
||||||
r = self.client.get(self.url, {"q": "first"})
|
r = self.client.get(self.url, {"q": "first"})
|
||||||
self.assertEqual(r.status_code, 200)
|
self.assertEqual(r.status_code, 200)
|
||||||
self.assertSetEqual(set(r.context["accounts"]), set([("000", "first last")]))
|
|
||||||
|
u = self.users["user"]
|
||||||
|
self.assertSetEqual(
|
||||||
|
{e.verbose_name for e in r.context["results"][0].entries},
|
||||||
|
{"{} ({})".format(u, u.profile.account_kfet.trigramme)},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AccountReadViewTests(ViewTestCaseMixin, TestCase):
|
class AccountReadViewTests(ViewTestCaseMixin, TestCase):
|
||||||
|
|
|
@ -27,7 +27,7 @@ from django.views.generic.edit import CreateView, DeleteView, UpdateView
|
||||||
from gestioncof.models import CofProfile
|
from gestioncof.models import CofProfile
|
||||||
from kfet import KFET_DELETED_TRIGRAMME, consumers
|
from kfet import KFET_DELETED_TRIGRAMME, consumers
|
||||||
from kfet.auth.decorators import kfet_password_auth
|
from kfet.auth.decorators import kfet_password_auth
|
||||||
from kfet.autocomplete import KfetAccountSearch, KfetAutocomplete
|
from kfet.autocomplete import KfetAccountOnlyAutocomplete, KfetAutocomplete
|
||||||
from kfet.config import kfet_config
|
from kfet.config import kfet_config
|
||||||
from kfet.decorators import teamkfet_required
|
from kfet.decorators import teamkfet_required
|
||||||
from kfet.forms import (
|
from kfet.forms import (
|
||||||
|
@ -79,6 +79,7 @@ from kfet.models import (
|
||||||
TransferGroup,
|
TransferGroup,
|
||||||
)
|
)
|
||||||
from kfet.statistic import DayScale, MonthScale, ScaleMixin, WeekScale, scale_url_params
|
from kfet.statistic import DayScale, MonthScale, ScaleMixin, WeekScale, scale_url_params
|
||||||
|
from shared.views import AutocompleteView
|
||||||
|
|
||||||
from .auth import KFET_GENERIC_TRIGRAMME
|
from .auth import KFET_GENERIC_TRIGRAMME
|
||||||
from .auth.views import ( # noqa
|
from .auth.views import ( # noqa
|
||||||
|
@ -2594,34 +2595,11 @@ class ArticleStatSales(ScaleMixin, JSONDetailView):
|
||||||
# ---
|
# ---
|
||||||
|
|
||||||
|
|
||||||
class AccountCreateAutocompleteView(PermissionRequiredMixin, TemplateView):
|
class AccountCreateAutocompleteView(PermissionRequiredMixin, AutocompleteView):
|
||||||
template_name = "kfet/account_create_autocomplete.html"
|
|
||||||
permission_required = "kfet.is_team"
|
permission_required = "kfet.is_team"
|
||||||
|
search_composer = KfetAutocomplete()
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
ctx = super().get_context_data(**kwargs)
|
|
||||||
if "q" not in self.request.GET:
|
|
||||||
raise Http404
|
|
||||||
q = self.request.GET["q"]
|
|
||||||
ctx["q"] = q
|
|
||||||
results = KfetAutocomplete().search(q.split())
|
|
||||||
ctx["options"] = sum((len(res) for res in results.values()))
|
|
||||||
ctx.update(results)
|
|
||||||
return ctx
|
|
||||||
|
|
||||||
|
|
||||||
class AccountSearchAutocompleteView(PermissionRequiredMixin, TemplateView):
|
class AccountSearchAutocompleteView(PermissionRequiredMixin, AutocompleteView):
|
||||||
template_name = "kfet/account_search_autocomplete.html"
|
|
||||||
permission_required = "kfet.is_team"
|
permission_required = "kfet.is_team"
|
||||||
|
search_composer = KfetAccountOnlyAutocomplete()
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
ctx = super().get_context_data(**kwargs)
|
|
||||||
if "q" not in self.request.GET:
|
|
||||||
raise Http404
|
|
||||||
q = self.request.GET["q"]
|
|
||||||
ctx["q"] = q
|
|
||||||
ctx["accounts"] = [
|
|
||||||
(user.profile.account_kfet.trigramme, user.get_full_name())
|
|
||||||
for user in KfetAccountSearch().search(q.split())
|
|
||||||
]
|
|
||||||
return ctx
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ from collections import namedtuple
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
if getattr(settings, "LDAP_SERVER_URL", None):
|
if getattr(settings, "LDAP_SERVER_URL", None):
|
||||||
import ldap
|
import ldap
|
||||||
|
@ -23,6 +24,17 @@ class SearchUnit:
|
||||||
|
|
||||||
It might optionally implement the following methods and attributes:
|
It might optionally implement the following methods and attributes:
|
||||||
|
|
||||||
|
- verbose_name (attribute): a nice name to refer to the results of this search unit
|
||||||
|
in templates. Examples: "COF Members", "K-Fêt accounts", etc.
|
||||||
|
|
||||||
|
- result_verbose_name (method): a callable that takes one search result as an input
|
||||||
|
and returns a nice name to refer to this particular result in templates.
|
||||||
|
Example: `lambda user: user.get_full_name()`
|
||||||
|
|
||||||
|
- result_link (method): a callable that takes one search result and returns a url
|
||||||
|
to make this particular search result clickable on the search page. For instance
|
||||||
|
this can be a link to a detail view of the object.
|
||||||
|
|
||||||
- result_uuid (method): a callable that takes one result as an input and returns an
|
- 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.
|
identifier that is globally unique across search units for this object.
|
||||||
This is used to compare results coming from different search units in the
|
This is used to compare results coming from different search units in the
|
||||||
|
@ -42,6 +54,16 @@ class SearchUnit:
|
||||||
|
|
||||||
# Optional attributes and methods
|
# Optional attributes and methods
|
||||||
|
|
||||||
|
verbose_name = None
|
||||||
|
|
||||||
|
def result_verbose_name(self, result):
|
||||||
|
"""Hook to customize the way results are displayed."""
|
||||||
|
return str(result)
|
||||||
|
|
||||||
|
def result_link(self, result):
|
||||||
|
"""Hook to add a link on individual results on the search page."""
|
||||||
|
return None
|
||||||
|
|
||||||
def result_uuid(self, result):
|
def result_uuid(self, result):
|
||||||
"""A universal unique identifier for the search results."""
|
"""A universal unique identifier for the search results."""
|
||||||
return None
|
return None
|
||||||
|
@ -74,6 +96,10 @@ class ModelSearch(SearchUnit):
|
||||||
model = None
|
model = None
|
||||||
search_fields = []
|
search_fields = []
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
if self.verbose_name is None:
|
||||||
|
self.verbose_name = "{} search".format(self.model.Meta.verbose_name)
|
||||||
|
|
||||||
def get_queryset_filter(self, keywords):
|
def get_queryset_filter(self, keywords):
|
||||||
filter_q = Q()
|
filter_q = Q()
|
||||||
|
|
||||||
|
@ -111,6 +137,8 @@ class LDAPSearch(SearchUnit):
|
||||||
domain_component = "dc=spi,dc=ens,dc=fr"
|
domain_component = "dc=spi,dc=ens,dc=fr"
|
||||||
search_fields = ["cn", "uid"]
|
search_fields = ["cn", "uid"]
|
||||||
|
|
||||||
|
verbose_name = _("Comptes clippers")
|
||||||
|
|
||||||
def get_ldap_query(self, keywords):
|
def get_ldap_query(self, keywords):
|
||||||
"""Return a search query with the following semantics:
|
"""Return a search query with the following semantics:
|
||||||
|
|
||||||
|
@ -157,6 +185,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_verbose_name(self, clipper):
|
||||||
|
return "{} ({})".format(clipper.fullname, clipper.clipper)
|
||||||
|
|
||||||
def result_uuid(self, clipper):
|
def result_uuid(self, clipper):
|
||||||
return clipper.clipper
|
return clipper.clipper
|
||||||
|
|
||||||
|
|
17
shared/templates/shared/search_results.html
Normal file
17
shared/templates/shared/search_results.html
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
{% for section in results %}
|
||||||
|
{% include "shared/search_results_section.html" with section=section %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% block extra_section %}
|
||||||
|
{% if not results %}
|
||||||
|
<li class="autocomplete-header">
|
||||||
|
<span class="autocomplete-item">
|
||||||
|
{% trans "Aucune correspondance trouvée" %}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
</ul>
|
25
shared/templates/shared/search_results_section.html
Normal file
25
shared/templates/shared/search_results_section.html
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
{% load search_utils %}
|
||||||
|
|
||||||
|
<li class="autocomplete-header">
|
||||||
|
<span class="autocomplete-item">{{ section.verbose_name }}</span>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
{% for entry in section.entries %}
|
||||||
|
{% if forloop.counter < 5 %}
|
||||||
|
<li class="autocomplete-value">
|
||||||
|
{% if entry.link %}
|
||||||
|
<a class="autocomplete-item" href="{{ entry.link }}">
|
||||||
|
{{ entry.verbose_name | highlight:q }}
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<span class="autocomplete-item">
|
||||||
|
{{ entry.verbose_name | highlight:q }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% elif forloop.counter == 5 %}
|
||||||
|
<li class="autocomplete-more">
|
||||||
|
<span class="autocomplete-item">...</span>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
|
@ -6,27 +6,10 @@ from django.utils.safestring import mark_safe
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
def highlight_text(text, q):
|
@register.filter
|
||||||
|
def highlight(text, q):
|
||||||
q2 = "|".join(re.escape(word) for word in q.split())
|
q2 = "|".join(re.escape(word) for word in q.split())
|
||||||
pattern = re.compile(r"(?P<filter>%s)" % q2, re.IGNORECASE)
|
pattern = re.compile(r"(?P<filter>%s)" % q2, re.IGNORECASE)
|
||||||
return mark_safe(
|
return mark_safe(
|
||||||
re.sub(pattern, r"<span class='highlight'>\g<filter></span>", text)
|
re.sub(pattern, r"<span class='highlight'>\g<filter></span>", text)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@register.filter
|
|
||||||
def highlight_user(user, q):
|
|
||||||
if user.first_name and user.last_name:
|
|
||||||
text = "%s %s (<tt>%s</tt>)" % (user.first_name, user.last_name, user.username)
|
|
||||||
else:
|
|
||||||
text = user.username
|
|
||||||
return highlight_text(text, q)
|
|
||||||
|
|
||||||
|
|
||||||
@register.filter
|
|
||||||
def highlight_clipper(clipper, q):
|
|
||||||
if clipper.fullname:
|
|
||||||
text = "%s (<tt>%s</tt>)" % (clipper.fullname, clipper.clipper)
|
|
||||||
else:
|
|
||||||
text = clipper.clipper
|
|
||||||
return highlight_text(text, q)
|
|
||||||
|
|
|
@ -1,4 +1,9 @@
|
||||||
|
from collections import namedtuple
|
||||||
|
|
||||||
from dal import autocomplete
|
from dal import autocomplete
|
||||||
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
from django.http import Http404
|
||||||
|
from django.views.generic import TemplateView
|
||||||
|
|
||||||
from shared.autocomplete import ModelSearch
|
from shared.autocomplete import ModelSearch
|
||||||
|
|
||||||
|
@ -9,3 +14,44 @@ class Select2QuerySetView(ModelSearch, autocomplete.Select2QuerySetView):
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
keywords = self.q.split()
|
keywords = self.q.split()
|
||||||
return super().search(keywords)
|
return super().search(keywords)
|
||||||
|
|
||||||
|
|
||||||
|
Section = namedtuple("Section", ("name", "verbose_name", "entries"))
|
||||||
|
Entry = namedtuple("Entry", ("verbose_name", "link"))
|
||||||
|
|
||||||
|
|
||||||
|
class AutocompleteView(TemplateView):
|
||||||
|
template_name = "shared/search_results.html"
|
||||||
|
search_composer = None
|
||||||
|
|
||||||
|
def get_search_composer(self):
|
||||||
|
if self.search_composer is None:
|
||||||
|
raise ImproperlyConfigured("Please specify a search composer")
|
||||||
|
return self.search_composer
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
ctx = super().get_context_data(**kwargs)
|
||||||
|
if "q" not in self.request.GET:
|
||||||
|
raise Http404
|
||||||
|
q = self.request.GET["q"]
|
||||||
|
ctx["q"] = q
|
||||||
|
ctx["results"] = self.search(keywords=q.split())
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
def search(self, keywords):
|
||||||
|
search_composer = self.get_search_composer()
|
||||||
|
raw_results = search_composer.search(keywords)
|
||||||
|
sections = []
|
||||||
|
for name, unit in search_composer.search_units:
|
||||||
|
entries = [
|
||||||
|
Entry(
|
||||||
|
verbose_name=unit.result_verbose_name(res),
|
||||||
|
link=unit.result_link(res),
|
||||||
|
)
|
||||||
|
for res in raw_results[name]
|
||||||
|
]
|
||||||
|
if entries:
|
||||||
|
sections.append(
|
||||||
|
Section(name=name, verbose_name=unit.verbose_name, entries=entries)
|
||||||
|
)
|
||||||
|
return sections
|
||||||
|
|
Loading…
Reference in a new issue