mirror of
https://github.com/ae-utbm/sith.git
synced 2025-11-24 13:47:01 +00:00
refactor: core/views/index.py
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
64
core/tests/test_notification.py
Normal file
64
core/tests/test_notification.py
Normal 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
|
||||||
12
core/urls.py
12
core/urls.py
@@ -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"),
|
||||||
|
|||||||
@@ -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)
|
|
||||||
|
|||||||
@@ -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():
|
||||||
|
|||||||
Reference in New Issue
Block a user