Merge branch 'kerl/factor_autocompletion_views3' into 'master'
Vue et template génériques d'autocomplétion See merge request klub-dev-ens/gestioCOF!429
This commit is contained in:
commit
c6a6e7fafa
23 changed files with 284 additions and 318 deletions
|
@ -1,5 +1,6 @@
|
|||
from django.contrib.auth import get_user_model
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from shared import autocomplete
|
||||
|
||||
|
@ -9,6 +10,7 @@ User = get_user_model()
|
|||
class BDSMemberSearch(autocomplete.ModelSearch):
|
||||
model = User
|
||||
search_fields = ["username", "first_name", "last_name"]
|
||||
verbose_name = _("Membres du BDS")
|
||||
|
||||
def get_queryset_filter(self, *args, **kwargs):
|
||||
qset_filter = super().get_queryset_filter(*args, **kwargs)
|
||||
|
@ -18,10 +20,14 @@ class BDSMemberSearch(autocomplete.ModelSearch):
|
|||
def result_uuid(self, user):
|
||||
return user.username
|
||||
|
||||
def result_link(self, user):
|
||||
return "#TODO"
|
||||
|
||||
|
||||
class BDSOthersSearch(autocomplete.ModelSearch):
|
||||
model = User
|
||||
search_fields = ["username", "first_name", "last_name"]
|
||||
verbose_name = _("Non-membres du BDS")
|
||||
|
||||
def get_queryset_filter(self, *args, **kwargs):
|
||||
qset_filter = super().get_queryset_filter(*args, **kwargs)
|
||||
|
@ -31,12 +37,20 @@ class BDSOthersSearch(autocomplete.ModelSearch):
|
|||
def result_uuid(self, user):
|
||||
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):
|
||||
search_units = [
|
||||
("members", BDSMemberSearch()),
|
||||
("others", BDSOthersSearch()),
|
||||
("clippers", autocomplete.LDAPSearch()),
|
||||
("clippers", BDSLDAPSearch()),
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -1,75 +1,21 @@
|
|||
{% extends "shared/search_results.html" %}
|
||||
{% load i18n %}
|
||||
{% load search_utils %}
|
||||
|
||||
<ul>
|
||||
{% if members %}
|
||||
<li class="autocomplete-header">
|
||||
<span class="autocomplete-item">{% trans "Membres" %}</span>
|
||||
</li>
|
||||
{% for user in members %}
|
||||
{% 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 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>
|
||||
{% block extra_section %}
|
||||
<li class="autocomplete-header">
|
||||
{% if not results %}
|
||||
<span class="autocomplete-item">
|
||||
{% trans "Aucune correspondance trouvée" %}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="autocomplete-item">
|
||||
{% trans "Pas dans la liste ?" %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</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"
|
||||
urlpatterns = [
|
||||
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 bds.autocomplete import bds_search
|
||||
from bds.mixins import StaffRequiredMixin
|
||||
from shared.views import AutocompleteView
|
||||
|
||||
|
||||
class AutocompleteView(StaffRequiredMixin, TemplateView):
|
||||
class BDSAutocompleteView(StaffRequiredMixin, AutocompleteView):
|
||||
template_name = "bds/search_results.html"
|
||||
|
||||
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
|
||||
search_composer = bds_search
|
||||
|
||||
|
||||
class Home(StaffRequiredMixin, TemplateView):
|
||||
|
|
|
@ -4,9 +4,8 @@ The settings that are not listed here are imported from .common
|
|||
"""
|
||||
import os
|
||||
|
||||
from .common import BASE_DIR, INSTALLED_APPS
|
||||
|
||||
from .common import * # NOQA
|
||||
from .common import BASE_DIR, INSTALLED_APPS
|
||||
|
||||
# ---
|
||||
# BDS-only Django settings
|
||||
|
|
|
@ -4,6 +4,7 @@ The settings that are not listed here are imported from .common
|
|||
"""
|
||||
import os
|
||||
|
||||
from .common import * # NOQA
|
||||
from .common import (
|
||||
AUTHENTICATION_BACKENDS,
|
||||
BASE_DIR,
|
||||
|
@ -13,8 +14,6 @@ from .common import (
|
|||
import_secret,
|
||||
)
|
||||
|
||||
from .common import * # NOQA
|
||||
|
||||
# ---
|
||||
# COF-specific secrets
|
||||
# ---
|
||||
|
|
|
@ -2,9 +2,8 @@
|
|||
import os
|
||||
|
||||
from . import bds_prod
|
||||
from .cof_prod import BASE_DIR, INSTALLED_APPS, MIDDLEWARE, TESTING
|
||||
|
||||
from .cof_prod import * # NOQA
|
||||
from .cof_prod import BASE_DIR, INSTALLED_APPS, MIDDLEWARE, TESTING
|
||||
|
||||
# ---
|
||||
# Merge COF and BDS configs
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
from django.contrib.auth import get_user_model
|
||||
from django.db.models import Q
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from shared import autocomplete
|
||||
|
||||
|
@ -9,6 +11,7 @@ User = get_user_model()
|
|||
class COFMemberSearch(autocomplete.ModelSearch):
|
||||
model = User
|
||||
search_fields = ["username", "first_name", "last_name"]
|
||||
verbose_name = _("Membres du COF")
|
||||
|
||||
def get_queryset_filter(self, *args, **kwargs):
|
||||
qset_filter = super().get_queryset_filter(*args, **kwargs)
|
||||
|
@ -18,10 +21,14 @@ class COFMemberSearch(autocomplete.ModelSearch):
|
|||
def result_uuid(self, user):
|
||||
return user.username
|
||||
|
||||
def result_link(self, user):
|
||||
return reverse("user-registration", args=(user.username,))
|
||||
|
||||
|
||||
class COFOthersSearch(autocomplete.ModelSearch):
|
||||
model = User
|
||||
search_fields = ["username", "first_name", "last_name"]
|
||||
verbose_name = _("Non-membres du COF")
|
||||
|
||||
def get_queryset_filter(self, *args, **kwargs):
|
||||
qset_filter = super().get_queryset_filter(*args, **kwargs)
|
||||
|
@ -31,10 +38,21 @@ class COFOthersSearch(autocomplete.ModelSearch):
|
|||
def result_uuid(self, user):
|
||||
return user.username
|
||||
|
||||
def result_link(self, user):
|
||||
return reverse("user-registration", args=(user.username,))
|
||||
|
||||
class COFSearch(autocomplete.Compose):
|
||||
|
||||
class COFLDAPSearch(autocomplete.LDAPSearch):
|
||||
def result_link(self, clipper):
|
||||
return reverse("clipper-registration", args=(clipper.clipper, clipper.fullname))
|
||||
|
||||
|
||||
class COFAutocomplete(autocomplete.Compose):
|
||||
search_units = [
|
||||
("members", COFMemberSearch()),
|
||||
("others", COFOthersSearch()),
|
||||
("clippers", autocomplete.LDAPSearch()),
|
||||
("clippers", COFLDAPSearch()),
|
||||
]
|
||||
|
||||
|
||||
cof_autocomplete = COFAutocomplete()
|
||||
|
|
|
@ -1,56 +1,21 @@
|
|||
{% load search_utils %}
|
||||
{% extends "shared/search_results.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
<ul>
|
||||
{% if members %}
|
||||
<li class="autocomplete-header">Membres</li>
|
||||
{% for user in members %}
|
||||
{% 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 others %}
|
||||
<li class="autocomplete-header">Non-membres</li>
|
||||
{% 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>
|
||||
{% block extra_section %}
|
||||
<li class="autocomplete-header">
|
||||
{% if not results %}
|
||||
<span class="autocomplete-item">
|
||||
{% trans "Aucune correspondance trouvée" %}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="autocomplete-item">
|
||||
{% trans "Pas dans la liste ?" %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
<li class="autocomplete-new">
|
||||
<a class="autocomplete-item" href="{% url "empty-registration" %}">
|
||||
{% trans "Créer un compte" %}
|
||||
</a>
|
||||
</li>
|
||||
{% endblock %}
|
||||
|
|
|
@ -16,7 +16,7 @@ from django.urls import reverse
|
|||
from bda.models import Salle, Tirage
|
||||
from gestioncof.models import CalendarSubscription, Club, Event, Survey, SurveyAnswer
|
||||
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 .utils import create_member, create_root, create_user
|
||||
|
@ -290,14 +290,30 @@ class RegistrationAutocompleteViewTests(MockLDAPMixin, ViewTestCaseMixin, TestCa
|
|||
|
||||
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(
|
||||
r.context["others"], map(repr, expected_others), ordered=False
|
||||
others, map(str, expected_others), ordered=False, transform=str
|
||||
)
|
||||
self.assertQuerysetEqual(
|
||||
r.context["members"], map(repr, expected_members), ordered=False,
|
||||
members, map(str, expected_members), ordered=False, transform=str
|
||||
)
|
||||
self.assertCountEqual(
|
||||
map(str, r.context["clippers"]), map(str, expected_clippers)
|
||||
self.assertSetEqual(
|
||||
set(clippers), set(map(LDAPSearch().result_verbose_name, expected_clippers))
|
||||
)
|
||||
|
||||
def test_username(self):
|
||||
|
|
|
@ -25,7 +25,7 @@ from django_cas_ng.views import LogoutView as CasLogoutView
|
|||
from icalendar import Calendar, Event as Vevent
|
||||
|
||||
from bda.models import Spectacle, Tirage
|
||||
from gestioncof.autocomplete import COFSearch
|
||||
from gestioncof.autocomplete import cof_autocomplete
|
||||
from gestioncof.decorators import BuroRequiredMixin, buro_required, cof_required
|
||||
from gestioncof.forms import (
|
||||
CalendarForm,
|
||||
|
@ -59,7 +59,7 @@ from gestioncof.models import (
|
|||
SurveyQuestion,
|
||||
SurveyQuestionAnswer,
|
||||
)
|
||||
from shared.views import Select2QuerySetView
|
||||
from shared.views import AutocompleteView, Select2QuerySetView
|
||||
|
||||
|
||||
class HomeView(LoginRequiredMixin, TemplateView):
|
||||
|
@ -948,14 +948,6 @@ class UserAutocompleteView(BuroRequiredMixin, Select2QuerySetView):
|
|||
search_fields = ("username", "first_name", "last_name")
|
||||
|
||||
|
||||
class RegistrationAutocompleteView(BuroRequiredMixin, TemplateView):
|
||||
class RegistrationAutocompleteView(BuroRequiredMixin, AutocompleteView):
|
||||
template_name = "gestioncof/search_results.html"
|
||||
|
||||
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
|
||||
search_composer = cof_autocomplete
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
from django.contrib.auth import get_user_model
|
||||
from django.db.models import Q
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from shared import autocomplete
|
||||
|
||||
|
@ -14,12 +16,16 @@ class KfetAccountSearch(autocomplete.ModelSearch):
|
|||
"last_name",
|
||||
"profile__account_kfet__trigramme",
|
||||
]
|
||||
verbose_name = _("Comptes existants")
|
||||
|
||||
def get_queryset_filter(self, *args, **kwargs):
|
||||
qset_filter = super().get_queryset_filter(*args, **kwargs)
|
||||
qset_filter &= Q(profile__account_kfet__isnull=False)
|
||||
return qset_filter
|
||||
|
||||
def result_verbose_name(self, user):
|
||||
return "{} ({})".format(user, user.profile.account_kfet.trigramme)
|
||||
|
||||
def result_uuid(self, user):
|
||||
return user.username
|
||||
|
||||
|
@ -27,6 +33,7 @@ class KfetAccountSearch(autocomplete.ModelSearch):
|
|||
class COFMemberSearch(autocomplete.ModelSearch):
|
||||
model = User
|
||||
search_fields = ["username", "first_name", "last_name"]
|
||||
verbose_name = _("Membres du COF")
|
||||
|
||||
def get_queryset_filter(self, *args, **kwargs):
|
||||
qset_filter = super().get_queryset_filter(*args, **kwargs)
|
||||
|
@ -36,10 +43,14 @@ class COFMemberSearch(autocomplete.ModelSearch):
|
|||
def result_uuid(self, user):
|
||||
return user.username
|
||||
|
||||
def result_link(self, user):
|
||||
return reverse("kfet.account.create.fromuser", args=(user.username,))
|
||||
|
||||
|
||||
class OthersSearch(autocomplete.ModelSearch):
|
||||
model = User
|
||||
search_fields = ["username", "first_name", "last_name"]
|
||||
verbose_name = _("Non-membres du COF")
|
||||
|
||||
def get_queryset_filter(self, *args, **kwargs):
|
||||
qset_filter = super().get_queryset_filter(*args, **kwargs)
|
||||
|
@ -49,11 +60,31 @@ class OthersSearch(autocomplete.ModelSearch):
|
|||
def result_uuid(self, user):
|
||||
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):
|
||||
search_units = [
|
||||
("kfet", KfetAccountSearch()),
|
||||
("users_cof", COFMemberSearch()),
|
||||
("users_notcof", OthersSearch()),
|
||||
("clippers", autocomplete.LDAPSearch()),
|
||||
("clippers", KfetLDAPSearch()),
|
||||
]
|
||||
|
||||
|
||||
kfet_autocomplete = KfetAutocomplete()
|
||||
|
||||
|
||||
class KfetAccountOnlyAutocomplete(autocomplete.Compose):
|
||||
search_units = [("kfet", KfetAccountSearch())]
|
||||
|
||||
|
||||
kfet_account_only_autocomplete = KfetAccountOnlyAutocomplete()
|
||||
|
|
|
@ -138,7 +138,7 @@
|
|||
* Specific account create
|
||||
*/
|
||||
|
||||
.highlight_autocomplete {
|
||||
.highlight {
|
||||
font-weight:bold;
|
||||
text-decoration:underline;
|
||||
}
|
||||
|
@ -159,7 +159,7 @@
|
|||
background:rgba(255,255,255,0.9);
|
||||
}
|
||||
|
||||
#search_results ul li.user_category {
|
||||
#search_results ul li.autocomplete-header {
|
||||
font-weight:bold;
|
||||
background:#c8102e;
|
||||
color:#fff;
|
||||
|
@ -178,7 +178,7 @@
|
|||
text-decoration:none;
|
||||
}
|
||||
|
||||
#search_results ul li span.text {
|
||||
#search_results ul li span.autocomplete-item {
|
||||
display:block;
|
||||
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.utils.html import escape
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from ..utils import to_ukf
|
||||
|
||||
|
@ -11,40 +7,14 @@ register = template.Library()
|
|||
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()
|
||||
def widget_type(field):
|
||||
return field.field.widget.__class__.__name__
|
||||
|
||||
|
||||
@register.filter()
|
||||
def slice(l, start, end=None):
|
||||
def slice(t, start, end=None):
|
||||
if end is None:
|
||||
end = start
|
||||
start = 0
|
||||
return l[start:end]
|
||||
return t[start:end]
|
||||
|
|
|
@ -181,9 +181,15 @@ class AccountCreateAutocompleteViewTests(ViewTestCaseMixin, TestCase):
|
|||
def test_ok(self):
|
||||
r = self.client.get(self.url, {"q": "first"})
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(len(r.context["users_notcof"]), 0)
|
||||
self.assertEqual(len(r.context["users_cof"]), 0)
|
||||
self.assertSetEqual(set(r.context["kfet"]), set([self.users["user"]]))
|
||||
self.assertEqual(len(r.context["results"]), 1)
|
||||
(res,) = r.context["results"]
|
||||
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):
|
||||
|
@ -196,7 +202,12 @@ class AccountSearchViewTests(ViewTestCaseMixin, TestCase):
|
|||
def test_ok(self):
|
||||
r = self.client.get(self.url, {"q": "first"})
|
||||
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):
|
||||
|
|
|
@ -27,7 +27,7 @@ from django.views.generic.edit import CreateView, DeleteView, UpdateView
|
|||
from gestioncof.models import CofProfile
|
||||
from kfet import KFET_DELETED_TRIGRAMME, consumers
|
||||
from kfet.auth.decorators import kfet_password_auth
|
||||
from kfet.autocomplete import KfetAccountSearch, KfetAutocomplete
|
||||
from kfet.autocomplete import kfet_account_only_autocomplete, kfet_autocomplete
|
||||
from kfet.config import kfet_config
|
||||
from kfet.decorators import teamkfet_required
|
||||
from kfet.forms import (
|
||||
|
@ -79,6 +79,7 @@ from kfet.models import (
|
|||
TransferGroup,
|
||||
)
|
||||
from kfet.statistic import DayScale, MonthScale, ScaleMixin, WeekScale, scale_url_params
|
||||
from shared.views import AutocompleteView
|
||||
|
||||
from .auth import KFET_GENERIC_TRIGRAMME
|
||||
from .auth.views import ( # noqa
|
||||
|
@ -2594,34 +2595,11 @@ class ArticleStatSales(ScaleMixin, JSONDetailView):
|
|||
# ---
|
||||
|
||||
|
||||
class AccountCreateAutocompleteView(PermissionRequiredMixin, TemplateView):
|
||||
template_name = "kfet/account_create_autocomplete.html"
|
||||
class AccountCreateAutocompleteView(PermissionRequiredMixin, AutocompleteView):
|
||||
permission_required = "kfet.is_team"
|
||||
|
||||
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
|
||||
search_composer = kfet_autocomplete
|
||||
|
||||
|
||||
class AccountSearchAutocompleteView(PermissionRequiredMixin, TemplateView):
|
||||
template_name = "kfet/account_search_autocomplete.html"
|
||||
class AccountSearchAutocompleteView(PermissionRequiredMixin, AutocompleteView):
|
||||
permission_required = "kfet.is_team"
|
||||
|
||||
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
|
||||
search_composer = kfet_account_only_autocomplete
|
||||
|
|
|
@ -3,6 +3,7 @@ from collections import namedtuple
|
|||
|
||||
from django.conf import settings
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
if getattr(settings, "LDAP_SERVER_URL", None):
|
||||
import ldap
|
||||
|
@ -23,6 +24,17 @@ class SearchUnit:
|
|||
|
||||
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
|
||||
identifier that is globally unique across search units for this object.
|
||||
This is used to compare results coming from different search units in the
|
||||
|
@ -42,6 +54,16 @@ class SearchUnit:
|
|||
|
||||
# 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):
|
||||
"""A universal unique identifier for the search results."""
|
||||
return None
|
||||
|
@ -74,6 +96,10 @@ class ModelSearch(SearchUnit):
|
|||
model = None
|
||||
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):
|
||||
filter_q = Q()
|
||||
|
||||
|
@ -111,6 +137,8 @@ class LDAPSearch(SearchUnit):
|
|||
domain_component = "dc=spi,dc=ens,dc=fr"
|
||||
search_fields = ["cn", "uid"]
|
||||
|
||||
verbose_name = _("Comptes clippers")
|
||||
|
||||
def get_ldap_query(self, keywords):
|
||||
"""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)
|
||||
return []
|
||||
|
||||
def result_verbose_name(self, clipper):
|
||||
return "{} ({})".format(clipper.fullname, clipper.clipper)
|
||||
|
||||
def result_uuid(self, 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()
|
||||
|
||||
|
||||
def highlight_text(text, q):
|
||||
@register.filter
|
||||
def highlight(text, q):
|
||||
q2 = "|".join(re.escape(word) for word in q.split())
|
||||
pattern = re.compile(r"(?P<filter>%s)" % q2, re.IGNORECASE)
|
||||
return mark_safe(
|
||||
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 django.core.exceptions import ImproperlyConfigured
|
||||
from django.http import Http404
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
from shared.autocomplete import ModelSearch
|
||||
|
||||
|
@ -9,3 +14,44 @@ class Select2QuerySetView(ModelSearch, autocomplete.Select2QuerySetView):
|
|||
def get_queryset(self):
|
||||
keywords = self.q.split()
|
||||
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