-{% endblock %}
\ No newline at end of file
+{% endblock %}
diff --git a/apps/activity/tests/test_activities.py b/apps/activity/tests/test_activities.py
index 1fcc7769..379f5d1b 100644
--- a/apps/activity/tests/test_activities.py
+++ b/apps/activity/tests/test_activities.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
+# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from datetime import timedelta
diff --git a/apps/activity/urls.py b/apps/activity/urls.py
index 73e4a385..3578c340 100644
--- a/apps/activity/urls.py
+++ b/apps/activity/urls.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
+# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.urls import path
diff --git a/apps/activity/views.py b/apps/activity/views.py
index 1f966c65..87c35ef0 100644
--- a/apps/activity/views.py
+++ b/apps/activity/views.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
+# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from hashlib import md5
@@ -17,14 +17,16 @@ from django.utils.translation import gettext_lazy as _
from django.views import View
from django.views.decorators.cache import cache_page
from django.views.generic import DetailView, TemplateView, UpdateView
-from django_tables2.views import SingleTableView
+from django.views.generic.list import ListView
+from django_tables2.views import MultiTableMixin, SingleTableMixin
+from api.viewsets import is_regex
from note.models import Alias, NoteSpecial, NoteUser
from permission.backends import PermissionBackend
from permission.views import ProtectQuerysetMixin, ProtectedCreateView
from .forms import ActivityForm, GuestForm
-from .models import Activity, Entry, Guest
-from .tables import ActivityTable, EntryTable, GuestTable
+from .models import Activity, Entry, Guest, Opener
+from .tables import ActivityTable, EntryTable, GuestTable, OpenerTable
class ActivityCreateView(ProtectQuerysetMixin, ProtectedCreateView):
@@ -57,26 +59,36 @@ class ActivityCreateView(ProtectQuerysetMixin, ProtectedCreateView):
return reverse_lazy('activity:activity_detail', kwargs={"pk": self.object.pk})
-class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
+class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, ListView):
"""
Displays all Activities, and classify if they are on-going or upcoming ones.
"""
model = Activity
- table_class = ActivityTable
- ordering = ('-date_start',)
+ tables = [
+ lambda data: ActivityTable(data, prefix="all-"),
+ lambda data: ActivityTable(data, prefix="upcoming-"),
+ ]
extra_context = {"title": _("Activities")}
def get_queryset(self, **kwargs):
return super().get_queryset(**kwargs).distinct()
+ def get_tables_data(self):
+ # first table = all activities, second table = upcoming
+ return [
+ self.get_queryset().order_by("-date_start"),
+ Activity.objects.filter(date_end__gt=timezone.now())
+ .filter(PermissionBackend.filter_queryset(self.request, Activity, "view"))
+ .distinct()
+ .order_by("date_start")
+ ]
+
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
- upcoming_activities = Activity.objects.filter(date_end__gt=timezone.now())
- context['upcoming'] = ActivityTable(
- data=upcoming_activities.filter(PermissionBackend.filter_queryset(self.request, Activity, "view")),
- prefix='upcoming-',
- )
+ tables = context["tables"]
+ for name, table in zip(["table", "upcoming"], tables):
+ context[name] = table
started_activities = self.get_queryset().filter(open=True, valid=True).distinct().all()
context["started_activities"] = started_activities
@@ -84,7 +96,7 @@ class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView
return context
-class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
+class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, DetailView):
"""
Shows details about one activity. Add guest to context
"""
@@ -92,15 +104,40 @@ class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
context_object_name = "activity"
extra_context = {"title": _("Activity detail")}
+ tables = [
+ lambda data: GuestTable(data, prefix="guests-"),
+ lambda data: OpenerTable(data, prefix="opener-"),
+ ]
+
+ def get_tables_data(self):
+ return [
+ Guest.objects.filter(activity=self.object)
+ .filter(PermissionBackend.filter_queryset(self.request, Guest, "view")),
+ self.object.opener.filter(activity=self.object)
+ .filter(PermissionBackend.filter_queryset(self.request, Opener, "view")),
+ ]
+
def get_context_data(self, **kwargs):
context = super().get_context_data()
- table = GuestTable(data=Guest.objects.filter(activity=self.object)
- .filter(PermissionBackend.filter_queryset(self.request, Guest, "view")))
- context["guests"] = table
+ tables = context["tables"]
+ for name, table in zip(["guests", "opener"], tables):
+ context[name] = table
context["activity_started"] = timezone.now() > timezone.localtime(self.object.date_start)
+ context["widget"] = {
+ "name": "opener",
+ "resetable": True,
+ "attrs": {
+ "class": "autocomplete form-control",
+ "id": "opener",
+ "api_url": "/api/note/alias/?note__polymorphic_ctype__model=noteuser",
+ "name_field": "name",
+ "placeholder": ""
+ }
+ }
+
return context
@@ -157,12 +194,14 @@ class ActivityInviteView(ProtectQuerysetMixin, ProtectedCreateView):
return reverse_lazy('activity:activity_detail', kwargs={"pk": self.kwargs["pk"]})
-class ActivityEntryView(LoginRequiredMixin, TemplateView):
+class ActivityEntryView(LoginRequiredMixin, SingleTableMixin, TemplateView):
"""
Manages entry to an activity
"""
template_name = "activity/activity_entry.html"
+ table_class = EntryTable
+
def dispatch(self, request, *args, **kwargs):
"""
Don't display the entry interface if the user has no right to see it (no right to add an entry for itself),
@@ -197,13 +236,16 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
if "search" in self.request.GET and self.request.GET["search"]:
pattern = self.request.GET["search"]
- if pattern[0] != "^":
- pattern = "^" + pattern
+
+ # Check if this is a valid regex. If not, we won't check regex
+ valid_regex = is_regex(pattern)
+ suffix = "__iregex" if valid_regex else "__istartswith"
+ pattern = "^" + pattern if valid_regex and pattern[0] != "^" else pattern
guest_qs = guest_qs.filter(
- Q(first_name__iregex=pattern)
- | Q(last_name__iregex=pattern)
- | Q(inviter__alias__name__iregex=pattern)
- | Q(inviter__alias__normalized_name__iregex=Alias.normalize(pattern))
+ Q(**{f"first_name{suffix}": pattern})
+ | Q(**{f"last_name{suffix}": pattern})
+ | Q(**{f"inviter__alias__name{suffix}": pattern})
+ | Q(**{f"inviter__alias__normalized_name{suffix}": Alias.normalize(pattern)})
)
else:
guest_qs = guest_qs.none()
@@ -235,11 +277,15 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
if "search" in self.request.GET and self.request.GET["search"]:
pattern = self.request.GET["search"]
+
+ # Check if this is a valid regex. If not, we won't check regex
+ valid_regex = is_regex(pattern)
+ suffix = "__iregex" if valid_regex else "__icontains"
note_qs = note_qs.filter(
- Q(note__noteuser__user__first_name__iregex=pattern)
- | Q(note__noteuser__user__last_name__iregex=pattern)
- | Q(name__iregex=pattern)
- | Q(normalized_name__iregex=Alias.normalize(pattern))
+ Q(**{f"note__noteuser__user__first_name{suffix}": pattern})
+ | Q(**{f"note__noteuser__user__last_name{suffix}": pattern})
+ | Q(**{f"name{suffix}": pattern})
+ | Q(**{f"normalized_name{suffix}": Alias.normalize(pattern)})
)
else:
note_qs = note_qs.none()
@@ -251,15 +297,9 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
if settings.DATABASES[note_qs.db]["ENGINE"] == 'django.db.backends.postgresql' else note_qs.distinct()[:20]
return note_qs
- def get_context_data(self, **kwargs):
- """
- Query the list of Guest and Note to the activity and add information to makes entry with JS.
- """
- context = super().get_context_data(**kwargs)
-
+ def get_table_data(self):
activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request, Activity, "view"))\
.distinct().get(pk=self.kwargs["pk"])
- context["activity"] = activity
matched = []
@@ -272,8 +312,17 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
note.activity = activity
matched.append(note)
- table = EntryTable(data=matched)
- context["table"] = table
+ return matched
+
+ def get_context_data(self, **kwargs):
+ """
+ Query the list of Guest and Note to the activity and add information to makes entry with JS.
+ """
+ context = super().get_context_data(**kwargs)
+
+ activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request, Activity, "view"))\
+ .distinct().get(pk=self.kwargs["pk"])
+ context["activity"] = activity
context["entries"] = Entry.objects.filter(activity=activity)
@@ -315,8 +364,8 @@ X-WR-CALNAME:Kfet Calendar
NAME:Kfet Calendar
CALSCALE:GREGORIAN
BEGIN:VTIMEZONE
-TZID:Europe/Berlin
-X-LIC-LOCATION:Europe/Berlin
+TZID:Europe/Paris
+X-LIC-LOCATION:Europe/Paris
BEGIN:DAYLIGHT
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
@@ -338,10 +387,10 @@ END:VTIMEZONE
DTSTAMP:{"{:%Y%m%dT%H%M%S}".format(activity.date_start)}Z
UID:{md5((activity.name + "$" + str(activity.id) + str(activity.date_start)).encode("UTF-8")).hexdigest()}
SUMMARY;CHARSET=UTF-8:{self.multilines(activity.name, 75, 22)}
-DTSTART;TZID=Europe/Berlin:{"{:%Y%m%dT%H%M%S}".format(activity.date_start)}
-DTEND;TZID=Europe/Berlin:{"{:%Y%m%dT%H%M%S}".format(activity.date_end)}
+DTSTART:{"{:%Y%m%dT%H%M%S}Z".format(activity.date_start)}
+DTEND:{"{:%Y%m%dT%H%M%S}Z".format(activity.date_end)}
LOCATION:{self.multilines(activity.location, 75, 9) if activity.location else "Kfet"}
-DESCRIPTION;CHARSET=UTF-8:""" + self.multilines(activity.description.replace("\n", "\\n"), 75, 26) + """
+DESCRIPTION;CHARSET=UTF-8:""" + self.multilines(activity.description.replace("\n", "\\n"), 75, 26) + f"""
-- {activity.organizer.name}
END:VEVENT
"""
diff --git a/apps/api/__init__.py b/apps/api/__init__.py
index bfa0f07d..2e6addb0 100644
--- a/apps/api/__init__.py
+++ b/apps/api/__init__.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
+# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
default_app_config = 'api.apps.APIConfig'
diff --git a/apps/api/apps.py b/apps/api/apps.py
index ebce358c..46fa6979 100644
--- a/apps/api/apps.py
+++ b/apps/api/apps.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
+# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.apps import AppConfig
diff --git a/apps/api/filters.py b/apps/api/filters.py
new file mode 100644
index 00000000..cb51c37c
--- /dev/null
+++ b/apps/api/filters.py
@@ -0,0 +1,42 @@
+import re
+from functools import lru_cache
+
+from rest_framework.filters import SearchFilter
+
+
+class RegexSafeSearchFilter(SearchFilter):
+ @lru_cache
+ def validate_regex(self, search_term) -> bool:
+ try:
+ re.compile(search_term)
+ return True
+ except re.error:
+ return False
+
+ def get_search_fields(self, view, request):
+ """
+ Ensure that given regex are valid.
+ If not, we consider that the user is trying to search by substring.
+ """
+ search_fields = super().get_search_fields(view, request)
+ search_terms = self.get_search_terms(request)
+
+ for search_term in search_terms:
+ if not self.validate_regex(search_term):
+ # Invalid regex. We assume we don't query by regex but by substring.
+ search_fields = [f.replace('$', '') for f in search_fields]
+ break
+
+ return search_fields
+
+ def get_search_terms(self, request):
+ """
+ Ensure that search field is a valid regex query. If not, we remove extra characters.
+ """
+ terms = super().get_search_terms(request)
+ if not all(self.validate_regex(term) for term in terms):
+ # Invalid regex. If a ^ is prefixed to the search term, we remove it.
+ terms = [term[1:] if term[0] == '^' else term for term in terms]
+ # Same for dollars.
+ terms = [term[:-1] if term[-1] == '$' else term for term in terms]
+ return terms
diff --git a/apps/api/serializers.py b/apps/api/serializers.py
index 0bae937f..74a95775 100644
--- a/apps/api/serializers.py
+++ b/apps/api/serializers.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
+# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
diff --git a/apps/api/tests.py b/apps/api/tests.py
index 36de0658..6be818bf 100644
--- a/apps/api/tests.py
+++ b/apps/api/tests.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
+# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import json
@@ -12,11 +12,12 @@ from django.contrib.contenttypes.models import ContentType
from django.db.models.fields.files import ImageFieldFile
from django.test import TestCase
from django_filters.rest_framework import DjangoFilterBackend
+from phonenumbers import PhoneNumber
+from rest_framework.filters import OrderingFilter
+from api.filters import RegexSafeSearchFilter
from member.models import Membership, Club
from note.models import NoteClub, NoteUser, Alias, Note
from permission.models import PermissionMask, Permission, Role
-from phonenumbers import PhoneNumber
-from rest_framework.filters import SearchFilter, OrderingFilter
from .viewsets import ContentTypeViewSet, UserViewSet
@@ -87,7 +88,7 @@ class TestAPI(TestCase):
resp = self.client.get(url + f"?ordering=-{field}")
self.assertEqual(resp.status_code, 200)
- if SearchFilter in backends:
+ if RegexSafeSearchFilter in backends:
# Basic search
for field in viewset.search_fields:
obj = self.fix_note_object(obj, field)
diff --git a/apps/api/urls.py b/apps/api/urls.py
index 4cb8085e..0659427f 100644
--- a/apps/api/urls.py
+++ b/apps/api/urls.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
+# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.conf import settings
diff --git a/apps/api/views.py b/apps/api/views.py
index 9718336d..43ef3f78 100644
--- a/apps/api/views.py
+++ b/apps/api/views.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
+# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib.auth.models import User
diff --git a/apps/api/viewsets.py b/apps/api/viewsets.py
index faeadee1..b1d42c50 100644
--- a/apps/api/viewsets.py
+++ b/apps/api/viewsets.py
@@ -1,19 +1,29 @@
-# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
+# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
+import re
+
from django.contrib.contenttypes.models import ContentType
from django_filters.rest_framework import DjangoFilterBackend
from django.db.models import Q
from django.conf import settings
from django.contrib.auth.models import User
-from rest_framework.filters import SearchFilter
from rest_framework.viewsets import ReadOnlyModelViewSet, ModelViewSet
from permission.backends import PermissionBackend
from note.models import Alias
+from .filters import RegexSafeSearchFilter
from .serializers import UserSerializer, ContentTypeSerializer
+def is_regex(pattern):
+ try:
+ re.compile(pattern)
+ return True
+ except (re.error, TypeError):
+ return False
+
+
class ReadProtectedModelViewSet(ModelViewSet):
"""
Protect a ModelViewSet by filtering the objects that the user cannot see.
@@ -60,34 +70,38 @@ class UserViewSet(ReadProtectedModelViewSet):
if "search" in self.request.GET:
pattern = self.request.GET["search"]
+ # Check if this is a valid regex. If not, we won't check regex
+ valid_regex = is_regex(pattern)
+ suffix = "__iregex" if valid_regex else "__istartswith"
+ prefix = "^" if valid_regex else ""
# Filter with different rules
# We use union-all to keep each filter rule sorted in result
queryset = queryset.filter(
# Match without normalization
- note__alias__name__iregex="^" + pattern
+ Q(**{f"note__alias__name{suffix}": prefix + pattern})
).union(
queryset.filter(
# Match with normalization
- Q(note__alias__normalized_name__iregex="^" + Alias.normalize(pattern))
- & ~Q(note__alias__name__iregex="^" + pattern)
+ Q(**{f"note__alias__normalized_name{suffix}": prefix + Alias.normalize(pattern)})
+ & ~Q(**{f"note__alias__name{suffix}": prefix + pattern})
),
all=True,
).union(
queryset.filter(
# Match on lower pattern
- Q(note__alias__normalized_name__iregex="^" + pattern.lower())
- & ~Q(note__alias__normalized_name__iregex="^" + Alias.normalize(pattern))
- & ~Q(note__alias__name__iregex="^" + pattern)
+ Q(**{f"note__alias__normalized_name{suffix}": prefix + pattern.lower()})
+ & ~Q(**{f"note__alias__normalized_name{suffix}": prefix + Alias.normalize(pattern)})
+ & ~Q(**{f"note__alias__name{suffix}": prefix + pattern})
),
all=True,
).union(
queryset.filter(
# Match on firstname or lastname
- (Q(last_name__iregex="^" + pattern) | Q(first_name__iregex="^" + pattern))
- & ~Q(note__alias__normalized_name__iregex="^" + pattern.lower())
- & ~Q(note__alias__normalized_name__iregex="^" + Alias.normalize(pattern))
- & ~Q(note__alias__name__iregex="^" + pattern)
+ (Q(**{f"last_name{suffix}": prefix + pattern}) | Q(**{f"first_name{suffix}": prefix + pattern}))
+ & ~Q(**{f"note__alias__normalized_name{suffix}": prefix + pattern.lower()})
+ & ~Q(**{f"note__alias__normalized_name{suffix}": prefix + Alias.normalize(pattern)})
+ & ~Q(**{f"note__alias__name{suffix}": prefix + pattern})
),
all=True,
)
@@ -107,6 +121,6 @@ class ContentTypeViewSet(ReadOnlyModelViewSet):
"""
queryset = ContentType.objects.order_by('id')
serializer_class = ContentTypeSerializer
- filter_backends = [DjangoFilterBackend, SearchFilter]
+ filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
filterset_fields = ['id', 'app_label', 'model', ]
search_fields = ['$app_label', '$model', ]
diff --git a/apps/logs/__init__.py b/apps/logs/__init__.py
index 708d35df..5271d575 100644
--- a/apps/logs/__init__.py
+++ b/apps/logs/__init__.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
+# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
default_app_config = 'logs.apps.LogsConfig'
diff --git a/apps/logs/api/serializers.py b/apps/logs/api/serializers.py
index f861bd21..cca94155 100644
--- a/apps/logs/api/serializers.py
+++ b/apps/logs/api/serializers.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
+# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from rest_framework import serializers
diff --git a/apps/logs/api/urls.py b/apps/logs/api/urls.py
index d0044c9b..9f255dd5 100644
--- a/apps/logs/api/urls.py
+++ b/apps/logs/api/urls.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
+# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from .views import ChangelogViewSet
diff --git a/apps/logs/api/views.py b/apps/logs/api/views.py
index eab1f1e4..655a473a 100644
--- a/apps/logs/api/views.py
+++ b/apps/logs/api/views.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
+# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django_filters.rest_framework import DjangoFilterBackend
diff --git a/apps/logs/apps.py b/apps/logs/apps.py
index bdf52d5e..366981ec 100644
--- a/apps/logs/apps.py
+++ b/apps/logs/apps.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
+# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.apps import AppConfig
diff --git a/apps/logs/signals.py b/apps/logs/signals.py
index a3166eed..c4fc08f3 100644
--- a/apps/logs/signals.py
+++ b/apps/logs/signals.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
+# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib.contenttypes.models import ContentType
@@ -56,13 +56,13 @@ def save_object(sender, instance, **kwargs):
# noinspection PyProtectedMember
previous = instance._previous
- # Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP
+ # Si un⋅e utilisateur⋅rice est connecté⋅e, on récupère l'utilisateur⋅rice courant⋅e ainsi que son adresse IP
request = get_current_request()
if request is None:
# Si la modification n'a pas été faite via le client Web, on suppose que c'est du à `manage.py`
# On récupère alors l'utilisateur·trice connecté·e à la VM, et on récupère la note associée
- # IMPORTANT : l'utilisateur dans la VM doit être un des alias note du respo info
+ # IMPORTANT : l'utilisateur⋅rice dans la VM doit être un des alias note du respo info
ip = "127.0.0.1"
username = Alias.normalize(getpass.getuser())
note = NoteUser.objects.filter(alias__normalized_name=username)
@@ -134,13 +134,13 @@ def delete_object(sender, instance, **kwargs):
if instance._meta.label_lower in EXCLUDED or hasattr(instance, "_no_signal"):
return
- # Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP
+ # Si un⋅e utilisateur⋅rice est connecté⋅e, on récupère l'utilisateur⋅rice courant⋅e ainsi que son adresse IP
request = get_current_request()
if request is None:
# Si la modification n'a pas été faite via le client Web, on suppose que c'est du à `manage.py`
# On récupère alors l'utilisateur·trice connecté·e à la VM, et on récupère la note associée
- # IMPORTANT : l'utilisateur dans la VM doit être un des alias note du respo info
+ # IMPORTANT : l'utilisateur⋅rice dans la VM doit être un des alias note du respo info
ip = "127.0.0.1"
username = Alias.normalize(getpass.getuser())
note = NoteUser.objects.filter(alias__normalized_name=username)
diff --git a/apps/member/__init__.py b/apps/member/__init__.py
index ae68f6ce..d0ab8586 100644
--- a/apps/member/__init__.py
+++ b/apps/member/__init__.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
+# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
default_app_config = 'member.apps.MemberConfig'
diff --git a/apps/member/admin.py b/apps/member/admin.py
index b3352c1a..3adc54ed 100644
--- a/apps/member/admin.py
+++ b/apps/member/admin.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
+# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib import admin
diff --git a/apps/member/api/serializers.py b/apps/member/api/serializers.py
index ef1c586d..31bb1655 100644
--- a/apps/member/api/serializers.py
+++ b/apps/member/api/serializers.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
+# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from rest_framework import serializers
diff --git a/apps/member/api/urls.py b/apps/member/api/urls.py
index c55b0969..6f323186 100644
--- a/apps/member/api/urls.py
+++ b/apps/member/api/urls.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
+# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from .views import ProfileViewSet, ClubViewSet, MembershipViewSet
diff --git a/apps/member/api/views.py b/apps/member/api/views.py
index 43127507..a24a12d6 100644
--- a/apps/member/api/views.py
+++ b/apps/member/api/views.py
@@ -1,8 +1,9 @@
-# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
+# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django_filters.rest_framework import DjangoFilterBackend
-from rest_framework.filters import OrderingFilter, SearchFilter
+from rest_framework.filters import OrderingFilter
+from api.filters import RegexSafeSearchFilter
from api.viewsets import ReadProtectedModelViewSet
from .serializers import ProfileSerializer, ClubSerializer, MembershipSerializer
@@ -17,7 +18,7 @@ class ProfileViewSet(ReadProtectedModelViewSet):
"""
queryset = Profile.objects.order_by('id')
serializer_class = ProfileSerializer
- filter_backends = [DjangoFilterBackend, SearchFilter]
+ filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
filterset_fields = ['user', 'user__first_name', 'user__last_name', 'user__username', 'user__email',
'user__note__alias__name', 'user__note__alias__normalized_name', 'phone_number', "section",
'department', 'promotion', 'address', 'paid', 'ml_events_registration', 'ml_sport_registration',
@@ -34,7 +35,7 @@ class ClubViewSet(ReadProtectedModelViewSet):
"""
queryset = Club.objects.order_by('id')
serializer_class = ClubSerializer
- filter_backends = [DjangoFilterBackend, SearchFilter]
+ filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
filterset_fields = ['name', 'email', 'note__alias__name', 'note__alias__normalized_name', 'parent_club',
'parent_club__name', 'require_memberships', 'membership_fee_paid', 'membership_fee_unpaid',
'membership_duration', 'membership_start', 'membership_end', ]
@@ -49,7 +50,7 @@ class MembershipViewSet(ReadProtectedModelViewSet):
"""
queryset = Membership.objects.order_by('id')
serializer_class = MembershipSerializer
- filter_backends = [DjangoFilterBackend, OrderingFilter, SearchFilter]
+ filter_backends = [DjangoFilterBackend, OrderingFilter, RegexSafeSearchFilter]
filterset_fields = ['club__name', 'club__email', 'club__note__alias__name', 'club__note__alias__normalized_name',
'user__username', 'user__last_name', 'user__first_name', 'user__email',
'user__note__alias__name', 'user__note__alias__normalized_name',
diff --git a/apps/member/apps.py b/apps/member/apps.py
index 1090c072..840c8783 100644
--- a/apps/member/apps.py
+++ b/apps/member/apps.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
+# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.apps import AppConfig
diff --git a/apps/member/auth.py b/apps/member/auth.py
index 888adea1..06e48afa 100644
--- a/apps/member/auth.py
+++ b/apps/member/auth.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
+# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from cas_server.auth import DjangoAuthUser # pragma: no cover
diff --git a/apps/member/forms.py b/apps/member/forms.py
index 0d78c726..a74ddb90 100644
--- a/apps/member/forms.py
+++ b/apps/member/forms.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
+# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import io
@@ -139,6 +139,9 @@ class ImageForm(forms.Form):
return cleaned_data
+ def is_valid(self):
+ return super().is_valid() or super().clean().get('image') is None
+
class ClubForm(forms.ModelForm):
def clean(self):
@@ -152,7 +155,7 @@ class ClubForm(forms.ModelForm):
class Meta:
model = Club
- fields = '__all__'
+ exclude = ("add_registration_form",)
widgets = {
"membership_fee_paid": AmountInput(),
"membership_fee_unpaid": AmountInput(),
@@ -208,9 +211,9 @@ class MembershipForm(forms.ModelForm):
class Meta:
model = Membership
fields = ('user', 'date_start')
- # Le champ d'utilisateur est remplacé par un champ d'auto-complétion.
+ # Le champ d'utilisateur⋅rice est remplacé par un champ d'auto-complétion.
# Quand des lettres sont tapées, une requête est envoyée sur l'API d'auto-complétion
- # et récupère les noms d'utilisateur valides
+ # et récupère les noms d'utilisateur⋅rices valides
widgets = {
'user':
Autocomplete(
diff --git a/apps/member/hashers.py b/apps/member/hashers.py
index 32f8c63e..659aab39 100644
--- a/apps/member/hashers.py
+++ b/apps/member/hashers.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
+# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import hashlib
diff --git a/apps/member/migrations/0012_club_add_registration_form.py b/apps/member/migrations/0012_club_add_registration_form.py
new file mode 100644
index 00000000..aad75f81
--- /dev/null
+++ b/apps/member/migrations/0012_club_add_registration_form.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.2.28 on 2024-07-15 09:24
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('member', '0011_profile_vss_charter_read'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='club',
+ name='add_registration_form',
+ field=models.BooleanField(default=False, verbose_name='add to registration form'),
+ ),
+ ]
diff --git a/apps/member/migrations/0013_auto_20240801_1436.py b/apps/member/migrations/0013_auto_20240801_1436.py
new file mode 100644
index 00000000..ddd9924d
--- /dev/null
+++ b/apps/member/migrations/0013_auto_20240801_1436.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.2.28 on 2024-08-01 12:36
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('member', '0012_club_add_registration_form'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='profile',
+ name='promotion',
+ field=models.PositiveSmallIntegerField(default=2024, help_text='Year of entry to the school (None if not ENS student)', null=True, verbose_name='promotion'),
+ ),
+ ]
diff --git a/apps/member/models.py b/apps/member/models.py
index a18b18f0..78d59667 100644
--- a/apps/member/models.py
+++ b/apps/member/models.py
@@ -259,6 +259,11 @@ class Club(models.Model):
help_text=_('Maximal date of a membership, after which members must renew it.'),
)
+ add_registration_form = models.BooleanField(
+ verbose_name=_("add to registration form"),
+ default=False,
+ )
+
class Meta:
verbose_name = _("club")
verbose_name_plural = _("clubs")
@@ -290,7 +295,14 @@ class Club(models.Model):
today = datetime.date.today()
- while (today - self.membership_start).days >= 365:
+ # Avoid any problems on February 29
+ if self.membership_start.month == 2 and self.membership_start.day == 29:
+ self.membership_start -= datetime.timedelta(days=1)
+ if self.membership_end.month == 2 and self.membership_end.day == 29:
+ self.membership_end += datetime.timedelta(days=1)
+
+ while today >= datetime.date(self.membership_start.year + 1,
+ self.membership_start.month, self.membership_start.day):
if self.membership_start:
self.membership_start = datetime.date(self.membership_start.year + 1,
self.membership_start.month, self.membership_start.day)
@@ -468,10 +480,10 @@ class Membership(models.Model):
if self.club.parent_club.name == "BDE":
parent_membership.roles.set(
- Role.objects.filter(Q(name="Adhérent BDE") | Q(name="Membre de club")).all())
+ Role.objects.filter(Q(name="Adhérent⋅e BDE") | Q(name="Membre de club")).all())
elif self.club.parent_club.name == "Kfet":
parent_membership.roles.set(
- Role.objects.filter(Q(name="Adhérent Kfet") | Q(name="Membre de club")).all())
+ Role.objects.filter(Q(name="Adhérent⋅e Kfet") | Q(name="Membre de club")).all())
else:
parent_membership.roles.set(Role.objects.filter(name="Membre de club").all())
parent_membership.save()
diff --git a/apps/member/signals.py b/apps/member/signals.py
index 197c6413..94dd4021 100644
--- a/apps/member/signals.py
+++ b/apps/member/signals.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
+# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
diff --git a/apps/member/tables.py b/apps/member/tables.py
index 7f7e54fd..46be76e7 100644
--- a/apps/member/tables.py
+++ b/apps/member/tables.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
+# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from datetime import date
diff --git a/apps/member/templates/member/picture_update.html b/apps/member/templates/member/picture_update.html
index 51d05f91..60707dcb 100644
--- a/apps/member/templates/member/picture_update.html
+++ b/apps/member/templates/member/picture_update.html
@@ -14,6 +14,9 @@ SPDX-License-Identifier: GPL-3.0-or-later
diff --git a/apps/member/templatetags/memberinfo.py b/apps/member/templatetags/memberinfo.py
index f528677e..c9b3b60c 100644
--- a/apps/member/templatetags/memberinfo.py
+++ b/apps/member/templatetags/memberinfo.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
+# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from datetime import date
diff --git a/apps/member/tests/test_memberships.py b/apps/member/tests/test_memberships.py
index de9f3d3d..1a59c253 100644
--- a/apps/member/tests/test_memberships.py
+++ b/apps/member/tests/test_memberships.py
@@ -291,7 +291,7 @@ class TestMemberships(TestCase):
response = self.client.post(reverse("member:club_manage_roles", args=(self.membership.pk,)), data=dict(
roles=[role.id for role in Role.objects.filter(
- Q(name="Membre de club") | Q(name="Trésorier·ère de club") | Q(name="Bureau de club")).all()],
+ Q(name="Membre de club") | Q(name="Trésorièr⋅e de club") | Q(name="Bureau de club")).all()],
))
self.assertRedirects(response, self.user.profile.get_absolute_url(), 302, 200)
self.membership.refresh_from_db()
diff --git a/apps/member/urls.py b/apps/member/urls.py
index 54b0f91d..51d506ab 100644
--- a/apps/member/urls.py
+++ b/apps/member/urls.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
+# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.urls import path
diff --git a/apps/member/views.py b/apps/member/views.py
index e56ed7b2..348bf089 100644
--- a/apps/member/views.py
+++ b/apps/member/views.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
+# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from datetime import timedelta, date
@@ -16,8 +16,9 @@ from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.views.generic import DetailView, UpdateView, TemplateView
from django.views.generic.edit import FormMixin
-from django_tables2.views import SingleTableView
+from django_tables2.views import MultiTableMixin, SingleTableMixin, SingleTableView
from rest_framework.authtoken.models import Token
+from api.viewsets import is_regex
from note.models import Alias, NoteClub, NoteUser, Trust
from note.models.transactions import Transaction, SpecialTransaction
from note.tables import HistoryTable, AliasTable, TrustTable, TrustedTable
@@ -219,16 +220,20 @@ class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
if "search" in self.request.GET and self.request.GET["search"]:
pattern = self.request.GET["search"]
+ # Check if this is a valid regex. If not, we won't check regex
+ valid_regex = is_regex(pattern)
+ suffix = "__iregex" if valid_regex else "__istartswith"
+ prefix = "^" if valid_regex else ""
qs = qs.filter(
- username__iregex="^" + pattern
+ Q(**{f"username{suffix}": prefix + pattern})
).union(
qs.filter(
- (Q(alias__iregex="^" + pattern)
- | Q(normalized_alias__iregex="^" + Alias.normalize(pattern))
- | Q(last_name__iregex="^" + pattern)
- | Q(first_name__iregex="^" + pattern)
+ (Q(**{f"alias{suffix}": prefix + pattern})
+ | Q(**{f"normalized_alias{suffix}": prefix + Alias.normalize(pattern)})
+ | Q(**{f"last_name{suffix}": prefix + pattern})
+ | Q(**{f"first_name{suffix}": prefix + pattern})
| Q(email__istartswith=pattern))
- & ~Q(username__iregex="^" + pattern)
+ & ~Q(**{f"username{suffix}": prefix + pattern})
), all=True)
else:
qs = qs.none()
@@ -243,7 +248,7 @@ class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
return context
-class ProfileTrustView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
+class ProfileTrustView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, DetailView):
"""
View and manage user trust relationships
"""
@@ -252,13 +257,25 @@ class ProfileTrustView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
context_object_name = 'user_object'
extra_context = {"title": _("Note friendships")}
+ tables = [
+ lambda data: TrustTable(data, prefix="trust-"),
+ lambda data: TrustedTable(data, prefix="trusted-"),
+ ]
+
+ def get_tables_data(self):
+ note = self.object.note
+ return [
+ note.trusting.filter(PermissionBackend.filter_queryset(self.request, Trust, "view")).distinct(),
+ note.trusted.filter(PermissionBackend.filter_queryset(self.request, Trust, "view")).distinct(),
+ ]
+
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
- note = context['object'].note
- context["trusting"] = TrustTable(
- note.trusting.filter(PermissionBackend.filter_queryset(self.request, Trust, "view")).distinct().all())
- context["trusted_by"] = TrustedTable(
- note.trusted.filter(PermissionBackend.filter_queryset(self.request, Trust, "view")).distinct().all())
+
+ tables = context["tables"]
+ for name, table in zip(["trusting", "trusted_by"], tables):
+ context[name] = table
+
context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_trust", Trust(
trusting=context["object"].note,
trusted=context["object"].note
@@ -277,7 +294,7 @@ class ProfileTrustView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
return context
-class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
+class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableMixin, DetailView):
"""
View and manage user aliases.
"""
@@ -286,12 +303,15 @@ class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
context_object_name = 'user_object'
extra_context = {"title": _("Note aliases")}
+ table_class = AliasTable
+ context_table_name = "aliases"
+
+ def get_table_data(self):
+ return self.object.note.alias.filter(PermissionBackend.filter_queryset(self.request, Alias, "view")).distinct() \
+ .order_by('normalized_name')
+
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
- note = context['object'].note
- context["aliases"] = AliasTable(
- note.alias.filter(PermissionBackend.filter_queryset(self.request, Alias, "view")).distinct()
- .order_by('normalized_name').all())
context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_alias", Alias(
note=context["object"].note,
name="",
@@ -326,12 +346,15 @@ class PictureUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin, Det
"""Save image to note"""
image = form.cleaned_data['image']
- # Rename as a PNG or GIF
- extension = image.name.split(".")[-1]
- if extension == "gif":
- image.name = "{}_pic.gif".format(self.object.note.pk)
+ if image is None:
+ image = "pic/default.png"
else:
- image.name = "{}_pic.png".format(self.object.note.pk)
+ # Rename as a PNG or GIF
+ extension = image.name.split(".")[-1]
+ if extension == "gif":
+ image.name = "{}_pic.gif".format(self.object.note.pk)
+ else:
+ image.name = "{}_pic.png".format(self.object.note.pk)
# Save
self.object.note.display_image = image
@@ -407,10 +430,15 @@ class ClubListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
if "search" in self.request.GET:
pattern = self.request.GET["search"]
+ # Check if this is a valid regex. If not, we won't check regex
+ valid_regex = is_regex(pattern)
+ suffix = "__iregex" if valid_regex else "__istartswith"
+ prefix = "^" if valid_regex else ""
+
qs = qs.filter(
- Q(name__iregex=pattern)
- | Q(note__alias__name__iregex=pattern)
- | Q(note__alias__normalized_name__iregex=Alias.normalize(pattern))
+ Q(**{f"name{suffix}": prefix + pattern})
+ | Q(**{f"note__alias__name{suffix}": prefix + pattern})
+ | Q(**{f"note__alias__normalized_name{suffix}": prefix + Alias.normalize(pattern)})
)
return qs
@@ -507,7 +535,7 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
return context
-class ClubAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
+class ClubAliasView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableMixin, DetailView):
"""
Manage aliases of a club.
"""
@@ -516,11 +544,16 @@ class ClubAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
context_object_name = 'club'
extra_context = {"title": _("Note aliases")}
+ table_class = AliasTable
+ context_table_name = "aliases"
+
+ def get_table_data(self):
+ return self.object.note.alias.filter(
+ PermissionBackend.filter_queryset(self.request, Alias, "view")).distinct()
+
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
- note = context['object'].note
- context["aliases"] = AliasTable(note.alias.filter(
- PermissionBackend.filter_queryset(self.request, Alias, "view")).distinct().all())
+
context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_alias", Alias(
note=context["object"].note,
name="",
@@ -824,8 +857,8 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
ret = super().form_valid(form)
- member_role = Role.objects.filter(Q(name="Adhérent BDE") | Q(name="Membre de club")).all() \
- if club.name == "BDE" else Role.objects.filter(Q(name="Adhérent Kfet") | Q(name="Membre de club")).all() \
+ member_role = Role.objects.filter(Q(name="Adhérent⋅e BDE") | Q(name="Membre de club")).all() \
+ if club.name == "BDE" else Role.objects.filter(Q(name="Adhérent⋅e Kfet") | Q(name="Membre de club")).all() \
if club.name == "Kfet"else Role.objects.filter(name="Membre de club").all()
# Set the same roles as before
if old_membership:
@@ -861,7 +894,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
membership.refresh_from_db()
if old_membership.exists():
membership.roles.set(old_membership.get().roles.all())
- membership.roles.set(Role.objects.filter(Q(name="Adhérent Kfet") | Q(name="Membre de club")).all())
+ membership.roles.set(Role.objects.filter(Q(name="Adhérent⋅e Kfet") | Q(name="Membre de club")).all())
membership.save()
return ret
@@ -909,10 +942,15 @@ class ClubMembersListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableV
if 'search' in self.request.GET:
pattern = self.request.GET['search']
+
+ # Check if this is a valid regex. If not, we won't check regex
+ valid_regex = is_regex(pattern)
+ suffix = "__iregex" if valid_regex else "__istartswith"
+ prefix = "^" if valid_regex else ""
qs = qs.filter(
- Q(user__first_name__iregex='^' + pattern)
- | Q(user__last_name__iregex='^' + pattern)
- | Q(user__note__alias__normalized_name__iregex='^' + Alias.normalize(pattern))
+ Q(**{f"user__first_name{suffix}": prefix + pattern})
+ | Q(**{f"user__last_name{suffix}": prefix + pattern})
+ | Q(**{f"user__note__alias__normalized_name{suffix}": prefix + Alias.normalize(pattern)})
)
only_active = "only_active" not in self.request.GET or self.request.GET["only_active"] != '0'
diff --git a/apps/note/__init__.py b/apps/note/__init__.py
index d3b96f09..828d8ed2 100644
--- a/apps/note/__init__.py
+++ b/apps/note/__init__.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
+# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
default_app_config = 'note.apps.NoteConfig'
diff --git a/apps/note/admin.py b/apps/note/admin.py
index 8d081d90..9bd8add1 100644
--- a/apps/note/admin.py
+++ b/apps/note/admin.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
+# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib import admin
diff --git a/apps/note/api/serializers.py b/apps/note/api/serializers.py
index a374fc33..980d992d 100644
--- a/apps/note/api/serializers.py
+++ b/apps/note/api/serializers.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
+# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.conf import settings
diff --git a/apps/note/api/urls.py b/apps/note/api/urls.py
index d15e8241..0522ebea 100644
--- a/apps/note/api/urls.py
+++ b/apps/note/api/urls.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
+# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from .views import NotePolymorphicViewSet, AliasViewSet, ConsumerViewSet, \
diff --git a/apps/note/api/views.py b/apps/note/api/views.py
index 32ed1837..5acda17c 100644
--- a/apps/note/api/views.py
+++ b/apps/note/api/views.py
@@ -1,16 +1,16 @@
-# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
+# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
-import re
from django.conf import settings
from django.db.models import Q
from django.core.exceptions import ValidationError
from django_filters.rest_framework import DjangoFilterBackend
-from rest_framework.filters import OrderingFilter, SearchFilter
-from rest_framework import viewsets
+from rest_framework.filters import OrderingFilter
+from rest_framework import status, viewsets
from rest_framework.response import Response
-from rest_framework import status
-from api.viewsets import ReadProtectedModelViewSet, ReadOnlyProtectedModelViewSet
+from api.filters import RegexSafeSearchFilter
+from api.viewsets import ReadProtectedModelViewSet, ReadOnlyProtectedModelViewSet, \
+ is_regex
from permission.backends import PermissionBackend
from .serializers import NotePolymorphicSerializer, AliasSerializer, ConsumerSerializer, \
@@ -29,7 +29,7 @@ class NotePolymorphicViewSet(ReadProtectedModelViewSet):
"""
queryset = Note.objects.order_by('id')
serializer_class = NotePolymorphicSerializer
- filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
+ filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter, OrderingFilter]
filterset_fields = ['alias__name', 'polymorphic_ctype', 'is_active', 'balance', 'last_negative', 'created_at', ]
search_fields = ['$alias__normalized_name', '$alias__name', '$polymorphic_ctype__model',
'$noteuser__user__last_name', '$noteuser__user__first_name', '$noteuser__user__email',
@@ -48,10 +48,14 @@ class NotePolymorphicViewSet(ReadProtectedModelViewSet):
.distinct()
alias = self.request.query_params.get("alias", ".*")
+ # Check if this is a valid regex. If not, we won't check regex
+ valid_regex = is_regex(alias)
+ suffix = '__iregex' if valid_regex else '__istartswith'
+ alias_prefix = '^' if valid_regex else ''
queryset = queryset.filter(
- Q(alias__name__iregex="^" + alias)
- | Q(alias__normalized_name__iregex="^" + Alias.normalize(alias))
- | Q(alias__normalized_name__iregex="^" + alias.lower())
+ Q(**{f"alias__name{suffix}": alias_prefix + alias})
+ | Q(**{f"alias__normalized_name{suffix}": alias_prefix + Alias.normalize(alias)})
+ | Q(**{f"alias__normalized_name{suffix}": alias_prefix + alias.lower()})
)
return queryset.order_by("id")
@@ -65,7 +69,7 @@ class TrustViewSet(ReadProtectedModelViewSet):
"""
queryset = Trust.objects
serializer_class = TrustSerializer
- filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
+ filter_backends = [RegexSafeSearchFilter, DjangoFilterBackend, OrderingFilter]
search_fields = ['$trusting__alias__name', '$trusting__alias__normalized_name',
'$trusted__alias__name', '$trusted__alias__normalized_name']
filterset_fields = ['trusting', 'trusting__noteuser__user', 'trusted', 'trusted__noteuser__user']
@@ -91,11 +95,11 @@ class AliasViewSet(ReadProtectedModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `Alias` objects, serialize it to JSON with the given serializer,
- then render it on /api/note/aliases/
+ then render it on /api/note/alias/
"""
queryset = Alias.objects
serializer_class = AliasSerializer
- filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
+ filter_backends = [RegexSafeSearchFilter, DjangoFilterBackend, OrderingFilter]
search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ]
filterset_fields = ['name', 'normalized_name', 'note', 'note__noteuser__user',
'note__noteclub__club', 'note__polymorphic_ctype__model', ]
@@ -126,18 +130,22 @@ class AliasViewSet(ReadProtectedModelViewSet):
alias = self.request.query_params.get("alias", None)
if alias:
+ # Check if this is a valid regex. If not, we won't check regex
+ valid_regex = is_regex(alias)
+ suffix = '__iregex' if valid_regex else '__istartswith'
+ alias_prefix = '^' if valid_regex else ''
queryset = queryset.filter(
- name__iregex="^" + alias
+ **{f"name{suffix}": alias_prefix + alias}
).union(
queryset.filter(
- Q(normalized_name__iregex="^" + Alias.normalize(alias))
- & ~Q(name__iregex="^" + alias)
+ Q(**{f"normalized_name{suffix}": alias_prefix + Alias.normalize(alias)})
+ & ~Q(**{f"name{suffix}": alias_prefix + alias})
),
all=True).union(
queryset.filter(
- Q(normalized_name__iregex="^" + alias.lower())
- & ~Q(normalized_name__iregex="^" + Alias.normalize(alias))
- & ~Q(name__iregex="^" + alias)
+ Q(**{f"normalized_name{suffix}": "^" + alias.lower()})
+ & ~Q(**{f"normalized_name{suffix}": "^" + Alias.normalize(alias)})
+ & ~Q(**{f"name{suffix}": "^" + alias})
),
all=True)
@@ -147,7 +155,7 @@ class AliasViewSet(ReadProtectedModelViewSet):
class ConsumerViewSet(ReadOnlyProtectedModelViewSet):
queryset = Alias.objects
serializer_class = ConsumerSerializer
- filter_backends = [SearchFilter, OrderingFilter, DjangoFilterBackend]
+ filter_backends = [RegexSafeSearchFilter, OrderingFilter, DjangoFilterBackend]
search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ]
filterset_fields = ['name', 'normalized_name', 'note', 'note__noteuser__user',
'note__noteclub__club', 'note__polymorphic_ctype__model', ]
@@ -166,11 +174,7 @@ class ConsumerViewSet(ReadOnlyProtectedModelViewSet):
alias = self.request.query_params.get("alias", None)
# Check if this is a valid regex. If not, we won't check regex
- try:
- re.compile(alias)
- valid_regex = True
- except (re.error, TypeError):
- valid_regex = False
+ valid_regex = is_regex(alias)
suffix = '__iregex' if valid_regex else '__istartswith'
alias_prefix = '^' if valid_regex else ''
queryset = queryset.prefetch_related('note')
@@ -198,7 +202,7 @@ class TemplateCategoryViewSet(ReadProtectedModelViewSet):
"""
queryset = TemplateCategory.objects.order_by('name')
serializer_class = TemplateCategorySerializer
- filter_backends = [DjangoFilterBackend, SearchFilter]
+ filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
filterset_fields = ['name', 'templates', 'templates__name']
search_fields = ['$name', '$templates__name', ]
@@ -211,7 +215,7 @@ class TransactionTemplateViewSet(viewsets.ModelViewSet):
"""
queryset = TransactionTemplate.objects.order_by('name')
serializer_class = TransactionTemplateSerializer
- filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
+ filter_backends = [RegexSafeSearchFilter, DjangoFilterBackend, OrderingFilter]
filterset_fields = ['name', 'amount', 'display', 'category', 'category__name', ]
search_fields = ['$name', '$category__name', ]
ordering_fields = ['amount', ]
@@ -225,7 +229,7 @@ class TransactionViewSet(ReadProtectedModelViewSet):
"""
queryset = Transaction.objects.order_by('-created_at')
serializer_class = TransactionPolymorphicSerializer
- filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
+ filter_backends = [RegexSafeSearchFilter, DjangoFilterBackend, OrderingFilter]
filterset_fields = ['source', 'source_alias', 'source__alias__name', 'source__alias__normalized_name',
'destination', 'destination_alias', 'destination__alias__name',
'destination__alias__normalized_name', 'quantity', 'polymorphic_ctype', 'amount',
diff --git a/apps/note/apps.py b/apps/note/apps.py
index 435fedf8..3d9c8583 100644
--- a/apps/note/apps.py
+++ b/apps/note/apps.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
+# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.apps import AppConfig
diff --git a/apps/note/forms.py b/apps/note/forms.py
index f496843a..aa722b57 100644
--- a/apps/note/forms.py
+++ b/apps/note/forms.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
+# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from datetime import datetime
diff --git a/apps/note/migrations/0002_create_special_notes.py b/apps/note/migrations/0002_create_special_notes.py
index 12fa8583..07935d54 100644
--- a/apps/note/migrations/0002_create_special_notes.py
+++ b/apps/note/migrations/0002_create_special_notes.py
@@ -18,6 +18,7 @@ def create_special_notes(apps, schema_editor):
class Migration(migrations.Migration):
dependencies = [
('note', '0001_initial'),
+ ('logs', '0001_initial'),
]
operations = [
diff --git a/apps/note/models/__init__.py b/apps/note/models/__init__.py
index ab5d4ff1..4beb8112 100644
--- a/apps/note/models/__init__.py
+++ b/apps/note/models/__init__.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
+# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from .notes import Alias, Note, NoteClub, NoteSpecial, NoteUser, Trust
diff --git a/apps/note/signals.py b/apps/note/signals.py
index 1ef51476..6ca702b9 100644
--- a/apps/note/signals.py
+++ b/apps/note/signals.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
+# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.utils import timezone
diff --git a/apps/note/static/note/js/consos.js b/apps/note/static/note/js/consos.js
index 9ee543f7..4f096207 100644
--- a/apps/note/static/note/js/consos.js
+++ b/apps/note/static/note/js/consos.js
@@ -1,4 +1,4 @@
-// Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
+// Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
// SPDX-License-Identifier: GPL-3.0-or-later
// When a transaction is performed, lock the interface to prevent spam clicks.
diff --git a/apps/note/tables.py b/apps/note/tables.py
index 243a8da6..3ca2d1d4 100644
--- a/apps/note/tables.py
+++ b/apps/note/tables.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
+# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import html
diff --git a/apps/note/templates/note/mails/negative_balance.html b/apps/note/templates/note/mails/negative_balance.html
index 8c869a54..04b2dd6a 100644
--- a/apps/note/templates/note/mails/negative_balance.html
+++ b/apps/note/templates/note/mails/negative_balance.html
@@ -22,8 +22,8 @@
- Par ailleurs, le BDE ne sert pas d'alcool aux adhérents dont le solde
- est inférieur à 0 € depuis plus de 24h.
+ Par ailleurs, le BDE ne sert pas d'alcool aux adhérent⋅es dont le solde
+ est inférieur à 0 €.
@@ -43,4 +43,4 @@
{% trans "Mail generated by the Note Kfet on the" %} {% now "j F Y à H:i:s" %}