1 Commits

Author SHA1 Message Date
imperosol
f55627a292 refactor: core/views/index.py 2025-11-24 09:25:38 +01:00
6 changed files with 116 additions and 104 deletions

View File

@@ -9,19 +9,17 @@
{% block content %} {% block content %}
<h4>{% trans %}Users{% endtrans %}</h4> <h4>{% trans %}Users{% endtrans %}</h4>
<ul> <ul>
{% for i in result.users %} {% for user in users %}
{% if user.can_view(i) %} <li>
<li> {{ user_link_with_pict(user) }}
{{ user_link_with_pict(i) }} </li>
</li>
{% endif %}
{% endfor %} {% endfor %}
</ul> </ul>
<h4>{% trans %}Clubs{% endtrans %}</h4> <h4>{% trans %}Clubs{% endtrans %}</h4>
<ul> <ul>
{% for i in result.clubs %} {% for club in clubs %}
<li> <li>
<a href="{{ url("club:club_view", club_id=i.id) }}">{{ i }}</a> <a href="{{ url("club:club_view", club_id=club.id) }}">{{ club }}</a>
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>

View File

@@ -35,6 +35,7 @@ from pytest_django.asserts import assertInHTML, assertRedirects
from antispam.models import ToxicDomain from antispam.models import ToxicDomain
from club.models import Club, Membership from club.models import Club, Membership
from core.baker_recipes import subscriber_user
from core.markdown import markdown from core.markdown import markdown
from core.models import AnonymousUser, Group, Page, User, validate_promo from core.models import AnonymousUser, Group, Page, User, validate_promo
from core.utils import get_last_promo, get_semester_code, get_start_of_semester 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) assert not TestAllowFragmentView.as_view()(request)
request.headers = {"HX-Request": True, **base_headers} request.headers = {"HX-Request": True, **base_headers}
assert TestAllowFragmentView.as_view()(request) 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

View File

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

View File

@@ -24,6 +24,7 @@
from django.urls import path, re_path, register_converter from django.urls import path, re_path, register_converter
from django.views.generic import RedirectView from django.views.generic import RedirectView
from com.views import NewsListView
from core.converters import ( from core.converters import (
BooleanStringConverter, BooleanStringConverter,
FourDigitYearConverter, FourDigitYearConverter,
@@ -53,6 +54,7 @@ from core.views import (
PagePropView, PagePropView,
PageRevView, PageRevView,
PageView, PageView,
SearchView,
SithLoginView, SithLoginView,
SithPasswordChangeDoneView, SithPasswordChangeDoneView,
SithPasswordChangeView, SithPasswordChangeView,
@@ -76,13 +78,9 @@ from core.views import (
UserUpdateProfileView, UserUpdateProfileView,
UserView, UserView,
delete_user_godfather, delete_user_godfather,
index,
logout, logout,
notification, notification,
password_root_change, password_root_change,
search_json,
search_user_json,
search_view,
send_file, send_file,
) )
@@ -91,13 +89,11 @@ register_converter(TwoDigitMonthConverter, "mm")
register_converter(BooleanStringConverter, "bool") register_converter(BooleanStringConverter, "bool")
urlpatterns = [ urlpatterns = [
path("", index, name="index"), path("", NewsListView.as_view(), name="index"),
path("notifications/", NotificationList.as_view(), name="notification_list"), path("notifications/", NotificationList.as_view(), name="notification_list"),
path("notification/<int:notif_id>/", notification, name="notification"), path("notification/<int:notif_id>/", notification, name="notification"),
# Search # Search
path("search/", search_view, name="search"), path("search/", SearchView.as_view(), name="search"),
path("search_json/", search_json, name="search_json"),
path("search_user/", search_user_json, name="search_user"),
# Login and co # Login and co
path("login/", SithLoginView.as_view(), name="login"), path("login/", SithLoginView.as_view(), name="login"),
path("logout/", logout, name="logout"), path("logout/", logout, name="logout"),

View File

@@ -22,106 +22,49 @@
# #
# #
import json
from django.conf import settings from django.conf import settings
from django.contrib.auth.decorators import login_required from django.contrib.auth.mixins import LoginRequiredMixin
from django.core import serializers from django.db.models import F
from django.db.models.query import QuerySet from django.db.models.query import QuerySet
from django.http import JsonResponse from django.http import HttpRequest
from django.shortcuts import redirect, render from django.shortcuts import get_object_or_404, redirect
from django.utils import html from django.views.generic import ListView, TemplateView
from django.utils.text import slugify
from django.views.generic import ListView
from haystack.query import SearchQuerySet
from club.models import Club from club.models import Club
from core.models import Notification, User from core.models import Notification, User
from core.schemas import UserFilterSchema
def index(request, context=None): class NotificationList(LoginRequiredMixin, ListView):
from com.views import NewsListView
return NewsListView.as_view()(request)
class NotificationList(ListView):
model = Notification model = Notification
template_name = "core/notification_list.jinja" template_name = "core/notification_list.jinja"
def get_queryset(self) -> QuerySet[Notification]: 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: if "see_all" in self.request.GET:
self.request.user.notifications.filter(viewed=False).update(viewed=True) self.request.user.notifications.filter(viewed=False).update(viewed=True)
return self.request.user.notifications.order_by("-date")[:20] return self.request.user.notifications.order_by("-date")[:20]
def notification(request, notif_id): def notification(request: HttpRequest, notif_id: int):
notif = Notification.objects.filter(id=notif_id).first() notif = get_object_or_404(Notification, id=notif_id)
if notif: if notif.type not in settings.SITH_PERMANENT_NOTIFICATIONS:
if notif.type not in settings.SITH_PERMANENT_NOTIFICATIONS: notif.viewed = True
notif.viewed = True else:
else: notif.callback()
notif.callback() notif.save()
notif.save() return redirect(notif.url)
return redirect(notif.url)
return redirect("/")
def search_user(query): class SearchView(LoginRequiredMixin, TemplateView):
try: template_name = "core/search.jinja"
# 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 []
def get_context_data(self, **kwargs):
def search_club(query, *, as_json=False): users, clubs = [], []
clubs = [] if query := self.request.GET.get("query"):
if query: users = list(
clubs = Club.objects.filter(name__icontains=query).all() UserFilterSchema(search=query)
clubs = clubs[:5] .filter(User.objects.viewable_by(self.request.user))
if as_json: .order_by(F("last_login").desc(nulls_last=True))
# Re-loads json to avoid double encoding by JsonResponse, but still benefit from serializers )
clubs = json.loads(serializers.serialize("json", clubs, fields=("name"))) clubs = list(Club.objects.filter(name__icontains=query)[:5])
else: return super().get_context_data(**kwargs) | {"users": users, "clubs": clubs}
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)

View File

@@ -24,6 +24,7 @@ from ast import literal_eval
from enum import Enum from enum import Enum
from django import forms from django import forms
from django.db.models import F
from django.http.response import HttpResponseRedirect from django.http.response import HttpResponseRedirect
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _ 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.auth.mixins import FormerSubscriberMixin
from core.models import User from core.models import User
from core.views import search_user from core.schemas import UserFilterSchema
from core.views.forms import SelectDate from core.views.forms import SelectDate
# Enum to select search type # Enum to select search type
@@ -126,11 +127,13 @@ class SearchFormListView(FormerSubscriberMixin, SingleObjectMixin, ListView):
q = q.filter(phone=self.valid_form["phone"]).all() q = q.filter(phone=self.valid_form["phone"]).all()
elif self.search_type == SearchType.QUICK: elif self.search_type == SearchType.QUICK:
if self.valid_form["quick"].strip(): 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: else:
q = [] q = []
if not self.can_see_hidden and len(q) > 0:
q = [user for user in q if user.is_viewable]
else: else:
search_dict = {} search_dict = {}
for key, value in self.valid_form.items(): for key, value in self.valid_form.items():