diff --git a/core/templates/core/search.jinja b/core/templates/core/search.jinja
index ea26129d..632c9299 100644
--- a/core/templates/core/search.jinja
+++ b/core/templates/core/search.jinja
@@ -9,19 +9,17 @@
{% block content %}
{% trans %}Users{% endtrans %}
- {% for i in result.users %}
- {% if user.can_view(i) %}
- -
- {{ user_link_with_pict(i) }}
-
- {% endif %}
+ {% for user in users %}
+ -
+ {{ user_link_with_pict(user) }}
+
{% endfor %}
{% trans %}Clubs{% endtrans %}
- {% for i in result.clubs %}
+ {% for club in clubs %}
-
- {{ i }}
+ {{ club }}
{% endfor %}
diff --git a/core/tests/test_core.py b/core/tests/test_core.py
index 77d3f535..01419621 100644
--- a/core/tests/test_core.py
+++ b/core/tests/test_core.py
@@ -35,6 +35,7 @@ from pytest_django.asserts import assertInHTML, assertRedirects
from antispam.models import ToxicDomain
from club.models import Club, Membership
+from core.baker_recipes import subscriber_user
from core.markdown import markdown
from core.models import AnonymousUser, Group, Page, User, validate_promo
from core.utils import get_last_promo, get_semester_code, get_start_of_semester
@@ -551,3 +552,10 @@ def test_allow_fragment_mixin():
assert not TestAllowFragmentView.as_view()(request)
request.headers = {"HX-Request": True, **base_headers}
assert TestAllowFragmentView.as_view()(request)
+
+
+@pytest.mark.django_db
+def test_search_view(client: Client):
+ client.force_login(subscriber_user.make())
+ response = client.get(reverse("core:search", query={"query": "foo"}))
+ assert response.status_code == 200
diff --git a/core/tests/test_notification.py b/core/tests/test_notification.py
new file mode 100644
index 00000000..8f468179
--- /dev/null
+++ b/core/tests/test_notification.py
@@ -0,0 +1,64 @@
+from datetime import timedelta
+from operator import attrgetter
+
+import pytest
+from bs4 import BeautifulSoup
+from django.test import Client, TestCase
+from django.urls import reverse
+from django.utils.timezone import now
+from model_bakery import baker, seq
+from pytest_django.asserts import assertRedirects
+
+from core.baker_recipes import subscriber_user
+from core.models import Notification
+
+
+@pytest.mark.django_db
+class TestNotificationList(TestCase):
+ @classmethod
+ def setUpTestData(cls):
+ cls.user = subscriber_user.make()
+ url = reverse("core:user_profile", kwargs={"user_id": cls.user.id})
+ cls.notifs = baker.make(
+ Notification,
+ user=cls.user,
+ url=url,
+ viewed=False,
+ date=seq(now() - timedelta(days=1), timedelta(hours=1)),
+ _quantity=10,
+ _bulk_create=True,
+ )
+
+ def test_list(self):
+ self.client.force_login(self.user)
+ response = self.client.get(reverse("core:notification_list"))
+ assert response.status_code == 200
+ soup = BeautifulSoup(response.text, "lxml")
+ ul = soup.find("ul", id="notifications")
+ elements = list(ul.find_all("li"))
+ assert len(elements) == len(self.notifs)
+ notifs = sorted(self.notifs, key=attrgetter("date"), reverse=True)
+ for element, notif in zip(elements, notifs, strict=True):
+ assert element.find("a")["href"] == reverse(
+ "core:notification", kwargs={"notif_id": notif.id}
+ )
+
+ def test_read_all(self):
+ self.client.force_login(self.user)
+ response = self.client.get(
+ reverse("core:notification_list", query={"read_all": None})
+ )
+ assert response.status_code == 200
+ assert not self.user.notifications.filter(viewed=True).exists()
+
+
+@pytest.mark.django_db
+def test_notification_redirect(client: Client):
+ user = subscriber_user.make()
+ url = reverse("core:user_profile", kwargs={"user_id": user.id})
+ notif = baker.make(Notification, user=user, url=url, viewed=False)
+ client.force_login(user)
+ response = client.get(reverse("core:notification", kwargs={"notif_id": notif.id}))
+ assertRedirects(response, url)
+ notif.refresh_from_db()
+ assert notif.viewed is True
diff --git a/core/urls.py b/core/urls.py
index 07719f34..0695e009 100644
--- a/core/urls.py
+++ b/core/urls.py
@@ -24,6 +24,7 @@
from django.urls import path, re_path, register_converter
from django.views.generic import RedirectView
+from com.views import NewsListView
from core.converters import (
BooleanStringConverter,
FourDigitYearConverter,
@@ -53,6 +54,7 @@ from core.views import (
PagePropView,
PageRevView,
PageView,
+ SearchView,
SithLoginView,
SithPasswordChangeDoneView,
SithPasswordChangeView,
@@ -76,13 +78,9 @@ from core.views import (
UserUpdateProfileView,
UserView,
delete_user_godfather,
- index,
logout,
notification,
password_root_change,
- search_json,
- search_user_json,
- search_view,
send_file,
)
@@ -91,13 +89,11 @@ register_converter(TwoDigitMonthConverter, "mm")
register_converter(BooleanStringConverter, "bool")
urlpatterns = [
- path("", index, name="index"),
+ path("", NewsListView.as_view(), name="index"),
path("notifications/", NotificationList.as_view(), name="notification_list"),
path("notification//", notification, name="notification"),
# Search
- path("search/", search_view, name="search"),
- path("search_json/", search_json, name="search_json"),
- path("search_user/", search_user_json, name="search_user"),
+ path("search/", SearchView.as_view(), name="search"),
# Login and co
path("login/", SithLoginView.as_view(), name="login"),
path("logout/", logout, name="logout"),
diff --git a/core/views/index.py b/core/views/index.py
index 06e2a8e3..a6af4648 100644
--- a/core/views/index.py
+++ b/core/views/index.py
@@ -22,106 +22,49 @@
#
#
-import json
-
from django.conf import settings
-from django.contrib.auth.decorators import login_required
-from django.core import serializers
+from django.contrib.auth.mixins import LoginRequiredMixin
+from django.db.models import F
from django.db.models.query import QuerySet
-from django.http import JsonResponse
-from django.shortcuts import redirect, render
-from django.utils import html
-from django.utils.text import slugify
-from django.views.generic import ListView
-from haystack.query import SearchQuerySet
+from django.http import HttpRequest
+from django.shortcuts import get_object_or_404, redirect
+from django.views.generic import ListView, TemplateView
from club.models import Club
from core.models import Notification, User
+from core.schemas import UserFilterSchema
-def index(request, context=None):
- from com.views import NewsListView
-
- return NewsListView.as_view()(request)
-
-
-class NotificationList(ListView):
+class NotificationList(LoginRequiredMixin, ListView):
model = Notification
template_name = "core/notification_list.jinja"
def get_queryset(self) -> QuerySet[Notification]:
- if self.request.user.is_anonymous:
- return Notification.objects.none()
- # TODO: Bulk update in django 2.2
if "see_all" in self.request.GET:
self.request.user.notifications.filter(viewed=False).update(viewed=True)
return self.request.user.notifications.order_by("-date")[:20]
-def notification(request, notif_id):
- notif = Notification.objects.filter(id=notif_id).first()
- if notif:
- if notif.type not in settings.SITH_PERMANENT_NOTIFICATIONS:
- notif.viewed = True
- else:
- notif.callback()
- notif.save()
- return redirect(notif.url)
- return redirect("/")
+def notification(request: HttpRequest, notif_id: int):
+ notif = get_object_or_404(Notification, id=notif_id)
+ if notif.type not in settings.SITH_PERMANENT_NOTIFICATIONS:
+ notif.viewed = True
+ else:
+ notif.callback()
+ notif.save()
+ return redirect(notif.url)
-def search_user(query):
- try:
- # slugify turns everything into ascii and every whitespace into -
- # it ends by removing duplicate - (so ' - ' will turn into '-')
- # replace('-', ' ') because search is whitespace based
- query = slugify(query).replace("-", " ")
- # TODO: is this necessary?
- query = html.escape(query)
- res = (
- SearchQuerySet()
- .models(User)
- .autocomplete(auto=query)
- .order_by("-last_login")
- .load_all()[:20]
- )
- return [r.object for r in res]
- except TypeError:
- return []
+class SearchView(LoginRequiredMixin, TemplateView):
+ template_name = "core/search.jinja"
-
-def search_club(query, *, as_json=False):
- clubs = []
- if query:
- clubs = Club.objects.filter(name__icontains=query).all()
- clubs = clubs[:5]
- if as_json:
- # Re-loads json to avoid double encoding by JsonResponse, but still benefit from serializers
- clubs = json.loads(serializers.serialize("json", clubs, fields=("name")))
- else:
- clubs = list(clubs)
- return clubs
-
-
-@login_required
-def search_view(request):
- result = {
- "users": search_user(request.GET.get("query", "")),
- "clubs": search_club(request.GET.get("query", "")),
- }
- return render(request, "core/search.jinja", context={"result": result})
-
-
-@login_required
-def search_user_json(request):
- result = {"users": search_user(request.GET.get("query", ""))}
- return JsonResponse(result)
-
-
-@login_required
-def search_json(request):
- result = {
- "users": search_user(request.GET.get("query", "")),
- "clubs": search_club(request.GET.get("query", ""), as_json=True),
- }
- return JsonResponse(result)
+ def get_context_data(self, **kwargs):
+ users, clubs = [], []
+ if query := self.request.GET.get("query"):
+ users = list(
+ UserFilterSchema(search=query)
+ .filter(User.objects.viewable_by(self.request.user))
+ .order_by(F("last_login").desc(nulls_last=True))
+ )
+ clubs = list(Club.objects.filter(name__icontains=query)[:5])
+ return super().get_context_data(**kwargs) | {"users": users, "clubs": clubs}
diff --git a/matmat/views.py b/matmat/views.py
index 30eb9541..18d0c482 100644
--- a/matmat/views.py
+++ b/matmat/views.py
@@ -24,6 +24,7 @@ from ast import literal_eval
from enum import Enum
from django import forms
+from django.db.models import F
from django.http.response import HttpResponseRedirect
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
@@ -34,7 +35,7 @@ from phonenumber_field.widgets import RegionalPhoneNumberWidget
from core.auth.mixins import FormerSubscriberMixin
from core.models import User
-from core.views import search_user
+from core.schemas import UserFilterSchema
from core.views.forms import SelectDate
# Enum to select search type
@@ -126,11 +127,13 @@ class SearchFormListView(FormerSubscriberMixin, SingleObjectMixin, ListView):
q = q.filter(phone=self.valid_form["phone"]).all()
elif self.search_type == SearchType.QUICK:
if self.valid_form["quick"].strip():
- q = search_user(self.valid_form["quick"])
+ q = list(
+ UserFilterSchema(search=self.valid_form["quick"])
+ .filter(User.objects.viewable_by(self.request.user))
+ .order_by(F("last_login").desc(nulls_last=True))
+ )
else:
q = []
- if not self.can_see_hidden and len(q) > 0:
- q = [user for user in q if user.is_viewable]
else:
search_dict = {}
for key, value in self.valid_form.items():