Generic autocompletion view

This commit is contained in:
Martin Pépin 2020-07-01 22:29:07 +02:00
parent 30783d677b
commit e7517195cd
No known key found for this signature in database
GPG key ID: E7520278B1774448
20 changed files with 272 additions and 309 deletions

View file

@ -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()),
]

View file

@ -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 %}

View file

@ -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"),
]

View file

@ -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):

View file

@ -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 COFLDAPSearch(autocomplete.LDAPSearch):
def result_link(self, clipper):
return reverse("clipper-registration", args=(clipper.clipper, clipper.fullname))
class COFSearch(autocomplete.Compose):
search_units = [
("members", COFMemberSearch()),
("others", COFOthersSearch()),
("clippers", autocomplete.LDAPSearch()),
("clippers", COFLDAPSearch()),
]
cof_search = COFSearch()

View file

@ -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 %}

View file

@ -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):

View file

@ -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 = COFSearch()

View file

@ -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,25 @@ 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()),
]
class KfetAccountOnlyAutocomplete(autocomplete.Compose):
search_units = [("kfet", KfetAccountSearch())]

View file

@ -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;
}

View file

@ -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>

View file

@ -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>

View file

@ -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]

View file

@ -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):

View file

@ -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 KfetAccountOnlyAutocomplete, KfetAutocomplete
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 = KfetAutocomplete()
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 = KfetAccountOnlyAutocomplete()

View file

@ -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

View 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>

View 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 %}

View file

@ -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)

View file

@ -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