diff --git a/bds/autocomplete.py b/bds/autocomplete.py
index 7fec4cea..9c9d8e59 100644
--- a/bds/autocomplete.py
+++ b/bds/autocomplete.py
@@ -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()),
]
diff --git a/bds/templates/bds/search_results.html b/bds/templates/bds/search_results.html
index b8d5e241..4c5471ff 100644
--- a/bds/templates/bds/search_results.html
+++ b/bds/templates/bds/search_results.html
@@ -1,75 +1,21 @@
+{% extends "shared/search_results.html" %}
{% load i18n %}
-{% load search_utils %}
-
- {% if members %}
-
- {% for user in members %}
- {% if forloop.counter < 5 %}
- -
-
- {{ user|highlight_user:q }}
-
-
- {% elif forloop.counter == 5 %}
- -
- ...
-
- {% endif %}
- {% endfor %}
- {% endif %}
-
- {% if others %}
-
- {% for user in others %}
- {% if forloop.counter < 5 %}
- -
-
- {{ user|highlight_user:q }}
-
-
- {% elif forloop.counter == 5 %}
- -
- ...
-
- {% endif %}
- {% endfor %}
- {% endif %}
-
- {% if clippers %}
-
- {% for clipper in clippers %}
- {% if forloop.counter < 5 %}
- -
-
- {{ clipper|highlight_clipper:q }}
-
-
- {% elif forloop.counter == 5 %}
- -
- ...
-
- {% endif %}
- {% endfor %}
- {% endif %}
-
- {% if total %}
-
- {% else %}
-
- {% endif %}
-
- -
- {% trans "Créer un compte" %}
+{% block extra_section %}
+
-
+
+
+ {% trans "Créer un compte" %}
+
+
+{% endblock %}
diff --git a/bds/urls.py b/bds/urls.py
index fbddccc6..b067a18f 100644
--- a/bds/urls.py
+++ b/bds/urls.py
@@ -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"),
]
diff --git a/bds/views.py b/bds/views.py
index a2ba3a2c..c612d3a2 100644
--- a/bds/views.py
+++ b/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):
diff --git a/gestioncof/autocomplete.py b/gestioncof/autocomplete.py
index febbcac9..9dae05db 100644
--- a/gestioncof/autocomplete.py
+++ b/gestioncof/autocomplete.py
@@ -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()
diff --git a/gestioncof/templates/gestioncof/search_results.html b/gestioncof/templates/gestioncof/search_results.html
index 126649b6..fdcd36ce 100644
--- a/gestioncof/templates/gestioncof/search_results.html
+++ b/gestioncof/templates/gestioncof/search_results.html
@@ -1,56 +1,21 @@
-{% load search_utils %}
+{% extends "shared/search_results.html" %}
+{% load i18n %}
-
- {% if members %}
-
- {% for user in members %}
- {% if forloop.counter < 5 %}
- -
-
- {{ user|highlight_user:q }}
-
-
- {% elif forloop.counter == 5 %}
- - ...
- {% endif %}
- {% endfor %}
- {% endif %}
-
- {% if others %}
-
- {% for user in others %}
- {% if forloop.counter < 5 %}
- -
-
- {{ user|highlight_user:q }}
-
-
- {% elif forloop.counter == 5 %}
- - ...
- {% endif %}
- {% endfor %}
- {% endif %}
-
- {% if clippers %}
-
- {% for clipper in clippers %}
- {% if forloop.counter < 5 %}
- -
-
- {{ clipper|highlight_clipper:q }}
-
-
- {% elif forloop.counter == 5 %}
- - ...
- {% endif %}
- {% endfor %}
- {% endif %}
-
- {% if total %}
-
- {% else %}
-
- {% endif %}
-
- - Créer un compte
-
+{% block extra_section %}
+
+
+
+ {% trans "Créer un compte" %}
+
+
+{% endblock %}
diff --git a/gestioncof/tests/test_views.py b/gestioncof/tests/test_views.py
index b623b283..0107b6a1 100644
--- a/gestioncof/tests/test_views.py
+++ b/gestioncof/tests/test_views.py
@@ -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):
diff --git a/gestioncof/views.py b/gestioncof/views.py
index a385f460..e399c05f 100644
--- a/gestioncof/views.py
+++ b/gestioncof/views.py
@@ -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()
diff --git a/kfet/autocomplete.py b/kfet/autocomplete.py
index 04a4be2b..ac2c0b8e 100644
--- a/kfet/autocomplete.py
+++ b/kfet/autocomplete.py
@@ -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())]
diff --git a/kfet/static/kfet/css/index.css b/kfet/static/kfet/css/index.css
index fdb86aff..cfaf2d74 100644
--- a/kfet/static/kfet/css/index.css
+++ b/kfet/static/kfet/css/index.css
@@ -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;
}
diff --git a/kfet/templates/kfet/account_create_autocomplete.html b/kfet/templates/kfet/account_create_autocomplete.html
deleted file mode 100644
index 2f04d461..00000000
--- a/kfet/templates/kfet/account_create_autocomplete.html
+++ /dev/null
@@ -1,50 +0,0 @@
-{% load kfet_tags %}
-
-
- -
-
- Créer un compte
-
-
- {% if kfet %}
- - Comptes existants
- {% for user in kfet %}
- - {{ user.account_kfet.account }} [{{ user|highlight_user:q }}]
- {% endfor %}
- {% endif %}
- {% if users_cof %}
- - Membres du COF
- {% for user in users_cof %}
- -
-
- {{ user|highlight_user:q }}
-
-
- {% endfor %}
- {% endif %}
- {% if users_notcof %}
- - Non-membres du COF
- {% for user in users_notcof %}
- -
-
- {{ user|highlight_user:q }}
-
-
- {% endfor %}
- {% endif %}
- {% if clippers %}
- - Utilisateurs clipper
- {% for clipper in clippers %}
- -
-
- {{ clipper|highlight_clipper:q }}
-
-
- {% endfor %}
- {% endif %}
- {% if not q %}
- - Pas de recherche, pas de résultats !
- {% elif not options %}
- - Aucune correspondance trouvée :-(
- {% endif %}
-
diff --git a/kfet/templates/kfet/account_search_autocomplete.html b/kfet/templates/kfet/account_search_autocomplete.html
deleted file mode 100644
index e18eb1eb..00000000
--- a/kfet/templates/kfet/account_search_autocomplete.html
+++ /dev/null
@@ -1,14 +0,0 @@
-{% load kfet_tags %}
-
-
- {% if accounts %}
- {% for trigramme, user in accounts %}
- - {{ user|highlight_text:q }} ({{ trigramme }})
- {% endfor %}
- {% elif not q %}
- - Pas de recherche, pas de résultats !
- {% else %}
- - Aucune correspondance trouvée :-(
- {% endif %}
-
-
diff --git a/kfet/templatetags/kfet_tags.py b/kfet/templatetags/kfet_tags.py
index 4c26dd17..db4cfbf1 100644
--- a/kfet/templatetags/kfet_tags.py
+++ b/kfet/templatetags/kfet_tags.py
@@ -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%s)" % q2, re.IGNORECASE)
- regex = r"\g"
- 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]
diff --git a/kfet/tests/test_views.py b/kfet/tests/test_views.py
index e411bd8d..e08575a3 100644
--- a/kfet/tests/test_views.py
+++ b/kfet/tests/test_views.py
@@ -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):
diff --git a/kfet/views.py b/kfet/views.py
index 5fd502c0..1f78c60d 100644
--- a/kfet/views.py
+++ b/kfet/views.py
@@ -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()
diff --git a/shared/autocomplete.py b/shared/autocomplete.py
index 02b6b661..564a6da7 100644
--- a/shared/autocomplete.py
+++ b/shared/autocomplete.py
@@ -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
diff --git a/shared/templates/shared/search_results.html b/shared/templates/shared/search_results.html
new file mode 100644
index 00000000..66661e8c
--- /dev/null
+++ b/shared/templates/shared/search_results.html
@@ -0,0 +1,17 @@
+{% load i18n %}
+
+
+ {% for section in results %}
+ {% include "shared/search_results_section.html" with section=section %}
+ {% endfor %}
+
+ {% block extra_section %}
+ {% if not results %}
+
+ {% endif %}
+ {% endblock %}
+
diff --git a/shared/templates/shared/search_results_section.html b/shared/templates/shared/search_results_section.html
new file mode 100644
index 00000000..800d6557
--- /dev/null
+++ b/shared/templates/shared/search_results_section.html
@@ -0,0 +1,25 @@
+{% load search_utils %}
+
+
+
+{% for entry in section.entries %}
+ {% if forloop.counter < 5 %}
+
+ {% if entry.link %}
+
+ {{ entry.verbose_name | highlight:q }}
+
+ {% else %}
+
+ {{ entry.verbose_name | highlight:q }}
+
+ {% endif %}
+
+ {% elif forloop.counter == 5 %}
+
+ ...
+
+ {% endif %}
+{% endfor %}
diff --git a/shared/templatetags/search_utils.py b/shared/templatetags/search_utils.py
index 28851248..a98c36e5 100644
--- a/shared/templatetags/search_utils.py
+++ b/shared/templatetags/search_utils.py
@@ -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%s)" % q2, re.IGNORECASE)
return mark_safe(
re.sub(pattern, r"\g", text)
)
-
-
-@register.filter
-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
-def highlight_clipper(clipper, q):
- if clipper.fullname:
- text = "%s (%s)" % (clipper.fullname, clipper.clipper)
- else:
- text = clipper.clipper
- return highlight_text(text, q)
diff --git a/shared/views.py b/shared/views.py
index 8fd28796..31523bad 100644
--- a/shared/views.py
+++ b/shared/views.py
@@ -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