1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2025-06-21 18:08:21 +02:00

Compare commits

..

2 Commits

Author SHA1 Message Date
7322d55789 Fix #113. Fix regex in views. 2024-07-19 20:00:33 +02:00
1a258dfe9e Parse input of search filters to prevent errors based on invalid regex, fixes #113
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2024-07-19 19:59:30 +02:00
24 changed files with 234 additions and 513 deletions

View File

@ -1,9 +1,9 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from api.viewsets import ReadProtectedModelViewSet
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import SearchFilter from api.filters import RegexSafeSearchFilter
from api.viewsets import ReadProtectedModelViewSet
from .serializers import ActivitySerializer, ActivityTypeSerializer, EntrySerializer, GuestSerializer from .serializers import ActivitySerializer, ActivityTypeSerializer, EntrySerializer, GuestSerializer
from ..models import Activity, ActivityType, Entry, Guest from ..models import Activity, ActivityType, Entry, Guest
@ -29,7 +29,7 @@ class ActivityViewSet(ReadProtectedModelViewSet):
""" """
queryset = Activity.objects.order_by('id') queryset = Activity.objects.order_by('id')
serializer_class = ActivitySerializer serializer_class = ActivitySerializer
filter_backends = [DjangoFilterBackend, SearchFilter] filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
filterset_fields = ['name', 'description', 'activity_type', 'location', 'creater', 'organizer', 'attendees_club', filterset_fields = ['name', 'description', 'activity_type', 'location', 'creater', 'organizer', 'attendees_club',
'date_start', 'date_end', 'valid', 'open', ] 'date_start', 'date_end', 'valid', 'open', ]
search_fields = ['$name', '$description', '$location', '$creater__last_name', '$creater__first_name', search_fields = ['$name', '$description', '$location', '$creater__last_name', '$creater__first_name',
@ -47,7 +47,7 @@ class GuestViewSet(ReadProtectedModelViewSet):
""" """
queryset = Guest.objects.order_by('id') queryset = Guest.objects.order_by('id')
serializer_class = GuestSerializer serializer_class = GuestSerializer
filter_backends = [DjangoFilterBackend, SearchFilter] filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
filterset_fields = ['activity', 'activity__name', 'last_name', 'first_name', 'inviter', 'inviter__alias__name', filterset_fields = ['activity', 'activity__name', 'last_name', 'first_name', 'inviter', 'inviter__alias__name',
'inviter__alias__normalized_name', ] 'inviter__alias__normalized_name', ]
search_fields = ['$activity__name', '$last_name', '$first_name', '$inviter__user__email', '$inviter__alias__name', search_fields = ['$activity__name', '$last_name', '$first_name', '$inviter__user__email', '$inviter__alias__name',
@ -62,7 +62,7 @@ class EntryViewSet(ReadProtectedModelViewSet):
""" """
queryset = Entry.objects.order_by('id') queryset = Entry.objects.order_by('id')
serializer_class = EntrySerializer serializer_class = EntrySerializer
filter_backends = [DjangoFilterBackend, SearchFilter] filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
filterset_fields = ['activity', 'time', 'note', 'guest', ] filterset_fields = ['activity', 'time', 'note', 'guest', ]
search_fields = ['$activity__name', '$note__user__email', '$note__alias__name', '$note__alias__normalized_name', search_fields = ['$activity__name', '$note__user__email', '$note__alias__name', '$note__alias__normalized_name',
'$guest__last_name', '$guest__first_name', ] '$guest__last_name', '$guest__first_name', ]

View File

@ -19,6 +19,7 @@ from django.views.decorators.cache import cache_page
from django.views.generic import DetailView, TemplateView, UpdateView from django.views.generic import DetailView, TemplateView, UpdateView
from django.views.generic.list import ListView from django.views.generic.list import ListView
from django_tables2.views import MultiTableMixin from django_tables2.views import MultiTableMixin
from api.viewsets import is_regex
from note.models import Alias, NoteSpecial, NoteUser from note.models import Alias, NoteSpecial, NoteUser
from permission.backends import PermissionBackend from permission.backends import PermissionBackend
from permission.views import ProtectQuerysetMixin, ProtectedCreateView from permission.views import ProtectQuerysetMixin, ProtectedCreateView
@ -212,13 +213,16 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
if "search" in self.request.GET and self.request.GET["search"]: if "search" in self.request.GET and self.request.GET["search"]:
pattern = 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( guest_qs = guest_qs.filter(
Q(first_name__iregex=pattern) Q(**{f"first_name{suffix}": pattern})
| Q(last_name__iregex=pattern) | Q(**{f"last_name{suffix}": pattern})
| Q(inviter__alias__name__iregex=pattern) | Q(**{f"inviter__alias__name{suffix}": pattern})
| Q(inviter__alias__normalized_name__iregex=Alias.normalize(pattern)) | Q(**{f"inviter__alias__normalized_name{suffix}": Alias.normalize(pattern)})
) )
else: else:
guest_qs = guest_qs.none() guest_qs = guest_qs.none()
@ -250,11 +254,15 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
if "search" in self.request.GET and self.request.GET["search"]: if "search" in self.request.GET and self.request.GET["search"]:
pattern = 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( note_qs = note_qs.filter(
Q(note__noteuser__user__first_name__iregex=pattern) Q(**{f"note__noteuser__user__first_name{suffix}": pattern})
| Q(note__noteuser__user__last_name__iregex=pattern) | Q(**{f"note__noteuser__user__last_name{suffix}": pattern})
| Q(name__iregex=pattern) | Q(**{f"name{suffix}": pattern})
| Q(normalized_name__iregex=Alias.normalize(pattern)) | Q(**{f"normalized_name{suffix}": Alias.normalize(pattern)})
) )
else: else:
note_qs = note_qs.none() note_qs = note_qs.none()

42
apps/api/filters.py Normal file
View File

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

View File

@ -12,11 +12,12 @@ from django.contrib.contenttypes.models import ContentType
from django.db.models.fields.files import ImageFieldFile from django.db.models.fields.files import ImageFieldFile
from django.test import TestCase from django.test import TestCase
from django_filters.rest_framework import DjangoFilterBackend 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 member.models import Membership, Club
from note.models import NoteClub, NoteUser, Alias, Note from note.models import NoteClub, NoteUser, Alias, Note
from permission.models import PermissionMask, Permission, Role from permission.models import PermissionMask, Permission, Role
from phonenumbers import PhoneNumber
from rest_framework.filters import SearchFilter, OrderingFilter
from .viewsets import ContentTypeViewSet, UserViewSet from .viewsets import ContentTypeViewSet, UserViewSet
@ -87,7 +88,7 @@ class TestAPI(TestCase):
resp = self.client.get(url + f"?ordering=-{field}") resp = self.client.get(url + f"?ordering=-{field}")
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
if SearchFilter in backends: if RegexSafeSearchFilter in backends:
# Basic search # Basic search
for field in viewset.search_fields: for field in viewset.search_fields:
obj = self.fix_note_object(obj, field) obj = self.fix_note_object(obj, field)

View File

@ -1,19 +1,29 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
import re
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from django.db.models import Q from django.db.models import Q
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from rest_framework.filters import SearchFilter
from rest_framework.viewsets import ReadOnlyModelViewSet, ModelViewSet from rest_framework.viewsets import ReadOnlyModelViewSet, ModelViewSet
from permission.backends import PermissionBackend from permission.backends import PermissionBackend
from note.models import Alias from note.models import Alias
from .filters import RegexSafeSearchFilter
from .serializers import UserSerializer, ContentTypeSerializer from .serializers import UserSerializer, ContentTypeSerializer
def is_regex(pattern):
try:
re.compile(pattern)
return True
except (re.error, TypeError):
return False
class ReadProtectedModelViewSet(ModelViewSet): class ReadProtectedModelViewSet(ModelViewSet):
""" """
Protect a ModelViewSet by filtering the objects that the user cannot see. 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: if "search" in self.request.GET:
pattern = 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 ""
# Filter with different rules # Filter with different rules
# We use union-all to keep each filter rule sorted in result # We use union-all to keep each filter rule sorted in result
queryset = queryset.filter( queryset = queryset.filter(
# Match without normalization # Match without normalization
note__alias__name__iregex="^" + pattern Q(**{f"note__alias__name{suffix}": prefix + pattern})
).union( ).union(
queryset.filter( queryset.filter(
# Match with normalization # Match with normalization
Q(note__alias__normalized_name__iregex="^" + Alias.normalize(pattern)) Q(**{f"note__alias__normalized_name{suffix}": prefix + Alias.normalize(pattern)})
& ~Q(note__alias__name__iregex="^" + pattern) & ~Q(**{f"note__alias__name{suffix}": prefix + pattern})
), ),
all=True, all=True,
).union( ).union(
queryset.filter( queryset.filter(
# Match on lower pattern # Match on lower pattern
Q(note__alias__normalized_name__iregex="^" + pattern.lower()) Q(**{f"note__alias__normalized_name{suffix}": prefix + pattern.lower()})
& ~Q(note__alias__normalized_name__iregex="^" + Alias.normalize(pattern)) & ~Q(**{f"note__alias__normalized_name{suffix}": prefix + Alias.normalize(pattern)})
& ~Q(note__alias__name__iregex="^" + pattern) & ~Q(**{f"note__alias__name{suffix}": prefix + pattern})
), ),
all=True, all=True,
).union( ).union(
queryset.filter( queryset.filter(
# Match on firstname or lastname # Match on firstname or lastname
(Q(last_name__iregex="^" + pattern) | Q(first_name__iregex="^" + pattern)) (Q(**{f"last_name{suffix}": prefix + pattern}) | Q(**{f"first_name{suffix}": prefix + pattern}))
& ~Q(note__alias__normalized_name__iregex="^" + pattern.lower()) & ~Q(**{f"note__alias__normalized_name{suffix}": prefix + pattern.lower()})
& ~Q(note__alias__normalized_name__iregex="^" + Alias.normalize(pattern)) & ~Q(**{f"note__alias__normalized_name{suffix}": prefix + Alias.normalize(pattern)})
& ~Q(note__alias__name__iregex="^" + pattern) & ~Q(**{f"note__alias__name{suffix}": prefix + pattern})
), ),
all=True, all=True,
) )
@ -107,6 +121,6 @@ class ContentTypeViewSet(ReadOnlyModelViewSet):
""" """
queryset = ContentType.objects.order_by('id') queryset = ContentType.objects.order_by('id')
serializer_class = ContentTypeSerializer serializer_class = ContentTypeSerializer
filter_backends = [DjangoFilterBackend, SearchFilter] filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
filterset_fields = ['id', 'app_label', 'model', ] filterset_fields = ['id', 'app_label', 'model', ]
search_fields = ['$app_label', '$model', ] search_fields = ['$app_label', '$model', ]

View File

@ -2,7 +2,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django_filters.rest_framework import DjangoFilterBackend 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 api.viewsets import ReadProtectedModelViewSet
from .serializers import ProfileSerializer, ClubSerializer, MembershipSerializer from .serializers import ProfileSerializer, ClubSerializer, MembershipSerializer
@ -17,7 +18,7 @@ class ProfileViewSet(ReadProtectedModelViewSet):
""" """
queryset = Profile.objects.order_by('id') queryset = Profile.objects.order_by('id')
serializer_class = ProfileSerializer serializer_class = ProfileSerializer
filter_backends = [DjangoFilterBackend, SearchFilter] filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
filterset_fields = ['user', 'user__first_name', 'user__last_name', 'user__username', 'user__email', 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", 'user__note__alias__name', 'user__note__alias__normalized_name', 'phone_number', "section",
'department', 'promotion', 'address', 'paid', 'ml_events_registration', 'ml_sport_registration', 'department', 'promotion', 'address', 'paid', 'ml_events_registration', 'ml_sport_registration',
@ -34,7 +35,7 @@ class ClubViewSet(ReadProtectedModelViewSet):
""" """
queryset = Club.objects.order_by('id') queryset = Club.objects.order_by('id')
serializer_class = ClubSerializer serializer_class = ClubSerializer
filter_backends = [DjangoFilterBackend, SearchFilter] filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
filterset_fields = ['name', 'email', 'note__alias__name', 'note__alias__normalized_name', 'parent_club', filterset_fields = ['name', 'email', 'note__alias__name', 'note__alias__normalized_name', 'parent_club',
'parent_club__name', 'require_memberships', 'membership_fee_paid', 'membership_fee_unpaid', 'parent_club__name', 'require_memberships', 'membership_fee_paid', 'membership_fee_unpaid',
'membership_duration', 'membership_start', 'membership_end', ] 'membership_duration', 'membership_start', 'membership_end', ]
@ -49,7 +50,7 @@ class MembershipViewSet(ReadProtectedModelViewSet):
""" """
queryset = Membership.objects.order_by('id') queryset = Membership.objects.order_by('id')
serializer_class = MembershipSerializer 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', 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__username', 'user__last_name', 'user__first_name', 'user__email',
'user__note__alias__name', 'user__note__alias__normalized_name', 'user__note__alias__name', 'user__note__alias__normalized_name',

View File

@ -1,18 +0,0 @@
# Generated by Django 2.2.28 on 2024-08-07 12:09
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'),
),
]

View File

@ -18,6 +18,7 @@ from django.views.generic import DetailView, UpdateView, TemplateView
from django.views.generic.edit import FormMixin from django.views.generic.edit import FormMixin
from django_tables2.views import SingleTableView from django_tables2.views import SingleTableView
from rest_framework.authtoken.models import Token from rest_framework.authtoken.models import Token
from api.viewsets import is_regex
from note.models import Alias, NoteClub, NoteUser, Trust from note.models import Alias, NoteClub, NoteUser, Trust
from note.models.transactions import Transaction, SpecialTransaction from note.models.transactions import Transaction, SpecialTransaction
from note.tables import HistoryTable, AliasTable, TrustTable, TrustedTable 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"]: if "search" in self.request.GET and self.request.GET["search"]:
pattern = 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( qs = qs.filter(
username__iregex="^" + pattern Q(**{f"username{suffix}": prefix + pattern})
).union( ).union(
qs.filter( qs.filter(
(Q(alias__iregex="^" + pattern) (Q(**{f"alias{suffix}": prefix + pattern})
| Q(normalized_alias__iregex="^" + Alias.normalize(pattern)) | Q(**{f"normalized_alias{suffix}": prefix + Alias.normalize(pattern)})
| Q(last_name__iregex="^" + pattern) | Q(**{f"last_name{suffix}": prefix + pattern})
| Q(first_name__iregex="^" + pattern) | Q(**{f"first_name{suffix}": prefix + pattern})
| Q(email__istartswith=pattern)) | Q(email__istartswith=pattern))
& ~Q(username__iregex="^" + pattern) & ~Q(**{f"username{suffix}": prefix + pattern})
), all=True) ), all=True)
else: else:
qs = qs.none() qs = qs.none()
@ -410,10 +415,15 @@ class ClubListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
if "search" in self.request.GET: if "search" in self.request.GET:
pattern = 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( qs = qs.filter(
Q(name__iregex=pattern) Q(**{f"name{suffix}": prefix + pattern})
| Q(note__alias__name__iregex=pattern) | Q(**{f"note__alias__name{suffix}": prefix + pattern})
| Q(note__alias__normalized_name__iregex=Alias.normalize(pattern)) | Q(**{f"note__alias__normalized_name{suffix}": prefix + Alias.normalize(pattern)})
) )
return qs return qs
@ -912,10 +922,15 @@ class ClubMembersListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableV
if 'search' in self.request.GET: if 'search' in self.request.GET:
pattern = 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( qs = qs.filter(
Q(user__first_name__iregex='^' + pattern) Q(**{f"user__first_name{suffix}": prefix + pattern})
| Q(user__last_name__iregex='^' + pattern) | Q(**{f"user__last_name{suffix}": prefix + pattern})
| Q(user__note__alias__normalized_name__iregex='^' + Alias.normalize(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' only_active = "only_active" not in self.request.GET or self.request.GET["only_active"] != '0'

View File

@ -1,16 +1,16 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
import re
from django.conf import settings from django.conf import settings
from django.db.models import Q from django.db.models import Q
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import OrderingFilter, SearchFilter from rest_framework.filters import OrderingFilter
from rest_framework import viewsets from rest_framework import status, viewsets
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework import status from api.filters import RegexSafeSearchFilter
from api.viewsets import ReadProtectedModelViewSet, ReadOnlyProtectedModelViewSet from api.viewsets import ReadProtectedModelViewSet, ReadOnlyProtectedModelViewSet, \
is_regex
from permission.backends import PermissionBackend from permission.backends import PermissionBackend
from .serializers import NotePolymorphicSerializer, AliasSerializer, ConsumerSerializer, \ from .serializers import NotePolymorphicSerializer, AliasSerializer, ConsumerSerializer, \
@ -29,7 +29,7 @@ class NotePolymorphicViewSet(ReadProtectedModelViewSet):
""" """
queryset = Note.objects.order_by('id') queryset = Note.objects.order_by('id')
serializer_class = NotePolymorphicSerializer 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', ] filterset_fields = ['alias__name', 'polymorphic_ctype', 'is_active', 'balance', 'last_negative', 'created_at', ]
search_fields = ['$alias__normalized_name', '$alias__name', '$polymorphic_ctype__model', search_fields = ['$alias__normalized_name', '$alias__name', '$polymorphic_ctype__model',
'$noteuser__user__last_name', '$noteuser__user__first_name', '$noteuser__user__email', '$noteuser__user__last_name', '$noteuser__user__first_name', '$noteuser__user__email',
@ -48,10 +48,14 @@ class NotePolymorphicViewSet(ReadProtectedModelViewSet):
.distinct() .distinct()
alias = self.request.query_params.get("alias", ".*") 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( queryset = queryset.filter(
Q(alias__name__iregex="^" + alias) Q(**{f"alias__name{suffix}": alias_prefix + alias})
| Q(alias__normalized_name__iregex="^" + Alias.normalize(alias)) | Q(**{f"alias__normalized_name{suffix}": alias_prefix + Alias.normalize(alias)})
| Q(alias__normalized_name__iregex="^" + alias.lower()) | Q(**{f"alias__normalized_name{suffix}": alias_prefix + alias.lower()})
) )
return queryset.order_by("id") return queryset.order_by("id")
@ -65,7 +69,7 @@ class TrustViewSet(ReadProtectedModelViewSet):
""" """
queryset = Trust.objects queryset = Trust.objects
serializer_class = TrustSerializer serializer_class = TrustSerializer
filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter] filter_backends = [RegexSafeSearchFilter, DjangoFilterBackend, OrderingFilter]
search_fields = ['$trusting__alias__name', '$trusting__alias__normalized_name', search_fields = ['$trusting__alias__name', '$trusting__alias__normalized_name',
'$trusted__alias__name', '$trusted__alias__normalized_name'] '$trusted__alias__name', '$trusted__alias__normalized_name']
filterset_fields = ['trusting', 'trusting__noteuser__user', 'trusted', 'trusted__noteuser__user'] filterset_fields = ['trusting', 'trusting__noteuser__user', 'trusted', 'trusted__noteuser__user']
@ -91,11 +95,11 @@ class AliasViewSet(ReadProtectedModelViewSet):
""" """
REST API View set. REST API View set.
The djangorestframework plugin will get all `Alias` objects, serialize it to JSON with the given serializer, 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 queryset = Alias.objects
serializer_class = AliasSerializer serializer_class = AliasSerializer
filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter] filter_backends = [RegexSafeSearchFilter, DjangoFilterBackend, OrderingFilter]
search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ] search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ]
filterset_fields = ['name', 'normalized_name', 'note', 'note__noteuser__user', filterset_fields = ['name', 'normalized_name', 'note', 'note__noteuser__user',
'note__noteclub__club', 'note__polymorphic_ctype__model', ] 'note__noteclub__club', 'note__polymorphic_ctype__model', ]
@ -126,18 +130,22 @@ class AliasViewSet(ReadProtectedModelViewSet):
alias = self.request.query_params.get("alias", None) alias = self.request.query_params.get("alias", None)
if alias: 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( queryset = queryset.filter(
name__iregex="^" + alias **{f"name{suffix}": alias_prefix + alias}
).union( ).union(
queryset.filter( queryset.filter(
Q(normalized_name__iregex="^" + Alias.normalize(alias)) Q(**{f"normalized_name{suffix}": alias_prefix + Alias.normalize(alias)})
& ~Q(name__iregex="^" + alias) & ~Q(**{f"name{suffix}": alias_prefix + alias})
), ),
all=True).union( all=True).union(
queryset.filter( queryset.filter(
Q(normalized_name__iregex="^" + alias.lower()) Q(**{f"normalized_name{suffix}": "^" + alias.lower()})
& ~Q(normalized_name__iregex="^" + Alias.normalize(alias)) & ~Q(**{f"normalized_name{suffix}": "^" + Alias.normalize(alias)})
& ~Q(name__iregex="^" + alias) & ~Q(**{f"name{suffix}": "^" + alias})
), ),
all=True) all=True)
@ -147,7 +155,7 @@ class AliasViewSet(ReadProtectedModelViewSet):
class ConsumerViewSet(ReadOnlyProtectedModelViewSet): class ConsumerViewSet(ReadOnlyProtectedModelViewSet):
queryset = Alias.objects queryset = Alias.objects
serializer_class = ConsumerSerializer serializer_class = ConsumerSerializer
filter_backends = [SearchFilter, OrderingFilter, DjangoFilterBackend] filter_backends = [RegexSafeSearchFilter, OrderingFilter, DjangoFilterBackend]
search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ] search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ]
filterset_fields = ['name', 'normalized_name', 'note', 'note__noteuser__user', filterset_fields = ['name', 'normalized_name', 'note', 'note__noteuser__user',
'note__noteclub__club', 'note__polymorphic_ctype__model', ] 'note__noteclub__club', 'note__polymorphic_ctype__model', ]
@ -166,11 +174,7 @@ class ConsumerViewSet(ReadOnlyProtectedModelViewSet):
alias = self.request.query_params.get("alias", None) alias = self.request.query_params.get("alias", None)
# Check if this is a valid regex. If not, we won't check regex # Check if this is a valid regex. If not, we won't check regex
try: valid_regex = is_regex(alias)
re.compile(alias)
valid_regex = True
except (re.error, TypeError):
valid_regex = False
suffix = '__iregex' if valid_regex else '__istartswith' suffix = '__iregex' if valid_regex else '__istartswith'
alias_prefix = '^' if valid_regex else '' alias_prefix = '^' if valid_regex else ''
queryset = queryset.prefetch_related('note') queryset = queryset.prefetch_related('note')
@ -207,7 +211,7 @@ class TemplateCategoryViewSet(ReadProtectedModelViewSet):
""" """
queryset = TemplateCategory.objects.order_by('name') queryset = TemplateCategory.objects.order_by('name')
serializer_class = TemplateCategorySerializer serializer_class = TemplateCategorySerializer
filter_backends = [DjangoFilterBackend, SearchFilter] filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
filterset_fields = ['name', 'templates', 'templates__name'] filterset_fields = ['name', 'templates', 'templates__name']
search_fields = ['$name', '$templates__name', ] search_fields = ['$name', '$templates__name', ]
@ -220,7 +224,7 @@ class TransactionTemplateViewSet(viewsets.ModelViewSet):
""" """
queryset = TransactionTemplate.objects.order_by('name') queryset = TransactionTemplate.objects.order_by('name')
serializer_class = TransactionTemplateSerializer serializer_class = TransactionTemplateSerializer
filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter] filter_backends = [RegexSafeSearchFilter, DjangoFilterBackend, OrderingFilter]
filterset_fields = ['name', 'amount', 'display', 'category', 'category__name', ] filterset_fields = ['name', 'amount', 'display', 'category', 'category__name', ]
search_fields = ['$name', '$category__name', ] search_fields = ['$name', '$category__name', ]
ordering_fields = ['amount', ] ordering_fields = ['amount', ]
@ -234,7 +238,7 @@ class TransactionViewSet(ReadProtectedModelViewSet):
""" """
queryset = Transaction.objects.order_by('-created_at') queryset = Transaction.objects.order_by('-created_at')
serializer_class = TransactionPolymorphicSerializer 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', filterset_fields = ['source', 'source_alias', 'source__alias__name', 'source__alias__normalized_name',
'destination', 'destination_alias', 'destination__alias__name', 'destination', 'destination_alias', 'destination__alias__name',
'destination__alias__normalized_name', 'quantity', 'polymorphic_ctype', 'amount', 'destination__alias__normalized_name', 'quantity', 'polymorphic_ctype', 'amount',

View File

@ -1,65 +0,0 @@
{% load pretty_money %}
{% load i18n %}
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>[Note Kfet] Récapitulatif de trésorerie</title>
</head>
<body>
<h1>
Récapitulatif de trésorerie au {{ summary.date|date:"d/m/Y" }} à {{ summary.date|date:"H:i:s" }} :
</h1>
<h2>
Tous les utilisateur⋅rices :
</h2>
<ul>
<li>Positifs : {{ summary.total_positive_user }} soit {{ summary.balance_positive_user / 100 }} €</li>
<li>Neutres : {{ summary.total_zero_user }}</li>
<li>Négatifs : {{ summary.total_negative_user }} soit {{ summary.balance_negative_user / 100 }} €</li>
</ul>
<h2>
Les {{ summary.total_positive_user_bde + summary.total_zero_user_bde + summary.total_negative_user_bde }} adhérent⋅es BDE :
</h2>
<ul>
<li>Positifs : {{ summary.total_positive_user_bde }} soit {{ summary.balance_positive_user_bde / 100 }} €</li>
<li>Neutres : {{ summary.total_zero_user_bde }}</li>
<li>Négatifs : {{ summary.total_negative_user_bde }} soit {{ summary.balance_negative_user_bde / 100 }} €</li>
</ul>
<h2>
Clubs :
</h2>
<ul>
<li>Positifs : {{ summary.total_positive_club }} soit {{ summary.balance_positive_club / 100 }} €</li>
<li>Neutres : {{ summary.total_zero_club }}</li>
<li>Négatifs : {{ summary.total_negative_club }} soit {{ summary.balance_negative_club / 100 }} €</li>
</ul>
<h2>
Clubs hors BDE / Kfet et club dont le nom fini par "- BDE" :
</h2>
<ul>
<li>Positifs : {{ summary.total_positive_club_nbde }} soit {{ summary.balance_positive_club_nbde / 100 }} €</li>
<li>Neutres : {{ summary.total_zero_club_nbde }}</li>
<li>Négatifs : {{ summary.total_negative_club_nbde }} soit {{ summary.balance_negative_club_nbde / 100 }} €</li>
</ul>
<h2>
Progression :
</h2>
<ul>
<li>Ceci correspond à une différence de {{ balance_difference_user / 100 }} € pour les utilisateur⋅rices</li>
<li>Ceci correspond à une différence de {{ balance_difference_club / 100 }} € pour les clubs</li>
</ul>
--
<p>
Le BDE<br>
{% trans "Mail generated by the Note Kfet on the" %} {% now "j F Y à H:i:s" %}
</p>
</body>
</html>

View File

@ -1,33 +0,0 @@
{% load pretty_money %}
{% load i18n %}
Récapitulatif de trésorerie au {{ summary.date|date:"d/m/Y" }} à {{ summary.date|date:"H:i:s" }} :
Tous les utilisateur⋅rices :
- Positifs : {{ summary.total_positive_user }} soit {{ summary.balance_positive_user / 100 }} €
- Neutres : {{ summary.total_zero_user }}
- Négatifs : {{ summary.total_negative_user }} soit {{ summary.balance_negative_user / 100 }} €
Les {{ summary.total_positive_user_bde + summary.total_zero_user_bde + summary.total_negative_user_bde }} adhérent⋅es BDE :
- Positifs : {{ summary.total_positive_user_bde }} soit {{ summary.balance_positive_user_bde / 100 }} €
- Neutres : {{ summary.total_zero_user_bde }}
- Négatifs : {{ summary.total_negative_user_bde }} soit {{ summary.balance_negative_user_bde /100 }} €
Clubs :
- Positifs : {{ summary.total_positive_club }} soit {{ summary.balance_positive_club / 100 }} €
- Neutres : {{ summary.total_zero_club }}
- Négatifs : {{ summary.total_negative_club }} soit {{ summary.balance_negative_club / 100 }} €
Clubs hors BDE / Kfet et club dont le nom fini par "- BDE" :
- Positifs : {{ summary.total_positive_club_nbde }} soit {{ summary.balance_positive_club_nbde / 100 }} €
- Neutres : {{ summary.total_zero_club_nbde }}
- Négatifs : {{ summary.total_negative_club_nbde }} soit {{ summary.balance_negative_club_nbde / 100 }} €
Progression :
- Ceci correspond à une différence de {{ balance_difference_user / 100 }} € pour les utilisateur⋅rices
- Ceci correspond à une différence de {{ balance_difference_club / 100 }} € pour les clubs
--
Le BDE
{% trans "Mail generated by the Note Kfet on the" %} {% now "j F Y à H:i:s" %}

View File

@ -13,6 +13,7 @@ from django.views.generic import CreateView, UpdateView, DetailView
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django_tables2 import SingleTableView from django_tables2 import SingleTableView
from activity.models import Entry from activity.models import Entry
from api.viewsets import is_regex
from permission.backends import PermissionBackend from permission.backends import PermissionBackend
from permission.views import ProtectQuerysetMixin from permission.views import ProtectQuerysetMixin
from note_kfet.inputs import AmountInput from note_kfet.inputs import AmountInput
@ -89,11 +90,15 @@ class TransactionTemplateListView(ProtectQuerysetMixin, LoginRequiredMixin, Sing
qs = super().get_queryset().distinct() qs = super().get_queryset().distinct()
if "search" in self.request.GET: if "search" in self.request.GET:
pattern = 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"
qs = qs.filter( qs = qs.filter(
Q(name__iregex=pattern) Q(**{f"name{suffix}": pattern})
| Q(destination__club__name__iregex=pattern) | Q(**{f"destination__club__name{suffix}": pattern})
| Q(category__name__iregex=pattern) | Q(**{f"category__name{suffix}": pattern})
| Q(description__iregex=pattern) | Q(**{f"description{suffix}": pattern})
) )
qs = qs.order_by('-display', 'category__name', 'destination__club__name', 'name') qs = qs.order_by('-display', 'category__name', 'destination__club__name', 'name')
@ -223,7 +228,10 @@ class TransactionSearchView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView
if "type" in data and data["type"]: if "type" in data and data["type"]:
transactions = transactions.filter(polymorphic_ctype__in=data["type"]) transactions = transactions.filter(polymorphic_ctype__in=data["type"])
if "reason" in data and data["reason"]: if "reason" in data and data["reason"]:
transactions = transactions.filter(reason__iregex=data["reason"]) # Check if this is a valid regex. If not, we won't check regex
valid_regex = is_regex(data["reason"])
suffix = "__iregex" if valid_regex else "__istartswith"
transactions = transactions.filter(Q(**{f"reason{suffix}": data["reason"]}))
if "valid" in data and data["valid"]: if "valid" in data and data["valid"]:
transactions = transactions.filter(valid=data["valid"]) transactions = transactions.filter(valid=data["valid"])
if "amount_gte" in data and data["amount_gte"]: if "amount_gte" in data and data["amount_gte"]:

View File

@ -1,9 +1,9 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from api.viewsets import ReadOnlyProtectedModelViewSet
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import SearchFilter from api.filters import RegexSafeSearchFilter
from api.viewsets import ReadOnlyProtectedModelViewSet
from .serializers import PermissionSerializer, RoleSerializer from .serializers import PermissionSerializer, RoleSerializer
from ..models import Permission, Role from ..models import Permission, Role
@ -17,9 +17,9 @@ class PermissionViewSet(ReadOnlyProtectedModelViewSet):
""" """
queryset = Permission.objects.order_by('id') queryset = Permission.objects.order_by('id')
serializer_class = PermissionSerializer serializer_class = PermissionSerializer
filter_backends = [DjangoFilterBackend, SearchFilter] filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
filterset_fields = ['model', 'type', 'query', 'mask', 'field', 'permanent', ] filterset_fields = ['model', 'type', 'query', 'mask', 'field', 'permanent', ]
search_fields = ['$model__name', '$query', '$description', ] search_fields = ['$model__model', '$query', '$description', ]
class RoleViewSet(ReadOnlyProtectedModelViewSet): class RoleViewSet(ReadOnlyProtectedModelViewSet):
@ -30,6 +30,6 @@ class RoleViewSet(ReadOnlyProtectedModelViewSet):
""" """
queryset = Role.objects.order_by('id') queryset = Role.objects.order_by('id')
serializer_class = RoleSerializer serializer_class = RoleSerializer
filter_backends = [DjangoFilterBackend, SearchFilter] filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
filterset_fields = ['name', 'permissions', 'for_club', 'memberships__user', ] filterset_fields = ['name', 'permissions', 'for_club', 'memberships__user', ]
search_fields = ['$name', '$for_club__name', ] search_fields = ['$name', '$for_club__name', ]

View File

@ -16,6 +16,7 @@ from django.views import View
from django.views.generic import CreateView, TemplateView, DetailView from django.views.generic import CreateView, TemplateView, DetailView
from django.views.generic.edit import FormMixin from django.views.generic.edit import FormMixin
from django_tables2 import SingleTableView from django_tables2 import SingleTableView
from api.viewsets import is_regex
from member.forms import ProfileForm from member.forms import ProfileForm
from member.models import Membership, Club from member.models import Membership, Club
from note.models import SpecialTransaction, Alias from note.models import SpecialTransaction, Alias
@ -192,11 +193,16 @@ class FutureUserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableVi
if "search" in self.request.GET and self.request.GET["search"]: if "search" in self.request.GET and self.request.GET["search"]:
pattern = 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_username = "__iregex" if valid_regex else "__icontains"
suffix = "__iregex" if valid_regex else "__istartswith"
prefix = "^" if valid_regex else ""
qs = qs.filter( qs = qs.filter(
Q(first_name__iregex=pattern) Q(**{f"first_name{suffix}": pattern})
| Q(last_name__iregex=pattern) | Q(**{f"last_name{suffix}": pattern})
| Q(profile__section__iregex=pattern) | Q(**{f"profile__section{suffix}": pattern})
| Q(username__iregex="^" + pattern) | Q(**{f"username{suffix_username}": prefix + pattern})
) )
return qs return qs

View File

@ -5,13 +5,13 @@ from django.contrib import admin
from note_kfet.admin import admin_site from note_kfet.admin import admin_site
from .forms import ProductForm from .forms import ProductForm
from .models import Invoice, NoteSummary, Product, RemittanceType, Remittance, SogeCredit from .models import RemittanceType, Remittance, SogeCredit, Invoice, Product
@admin.register(RemittanceType, site=admin_site) @admin.register(RemittanceType, site=admin_site)
class RemittanceTypeAdmin(admin.ModelAdmin): class RemittanceTypeAdmin(admin.ModelAdmin):
""" """
Admin customisation for RemittanceType Admin customisation for RemiitanceType
""" """
list_display = ('note', ) list_display = ('note', )
@ -55,19 +55,3 @@ class InvoiceAdmin(admin.ModelAdmin):
""" """
list_display = ('object', 'id', 'bde', 'name', 'date', 'acquitted',) list_display = ('object', 'id', 'bde', 'name', 'date', 'acquitted',)
inlines = (ProductInline,) inlines = (ProductInline,)
@admin.register(NoteSummary, site=admin_site)
class NoteSummaryAdmin(admin.ModelAdmin):
"""
Admin customisation for NoteSummary
"""
list_display = (
'date', 'total_positive_user', 'balance_positive_user', 'total_positive_user_bde',
'balance_positive_user_bde', 'total_zero_user', 'total_zero_user_bde', 'total_negative_user',
'balance_negative_user', 'total_negative_user_bde', 'balance_negative_user_bde',
'total_vnegative_user', 'balance_vnegative_user', 'total_vnegative_user_bde',
'balance_vnegative_user_bde', 'total_positive_club', 'balance_positive_club',
'total_positive_club_nbde', 'balance_positive_club_nbde', 'total_zero_club', 'total_zero_club_nbde',
'total_negative_club', 'balance_negative_club', 'total_negative_club_nbde', 'balance_negative_club_nbde',
)

View File

@ -2,7 +2,7 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import SearchFilter from api.filters import RegexSafeSearchFilter
from api.viewsets import ReadProtectedModelViewSet from api.viewsets import ReadProtectedModelViewSet
from .serializers import InvoiceSerializer, ProductSerializer, RemittanceTypeSerializer, RemittanceSerializer, \ from .serializers import InvoiceSerializer, ProductSerializer, RemittanceTypeSerializer, RemittanceSerializer, \
@ -18,7 +18,7 @@ class InvoiceViewSet(ReadProtectedModelViewSet):
""" """
queryset = Invoice.objects.order_by('id') queryset = Invoice.objects.order_by('id')
serializer_class = InvoiceSerializer serializer_class = InvoiceSerializer
filter_backends = [DjangoFilterBackend, SearchFilter] filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
filterset_fields = ['bde', 'object', 'description', 'name', 'address', 'date', 'acquitted', 'locked', ] filterset_fields = ['bde', 'object', 'description', 'name', 'address', 'date', 'acquitted', 'locked', ]
search_fields = ['$object', '$description', '$name', '$address', ] search_fields = ['$object', '$description', '$name', '$address', ]
@ -31,7 +31,7 @@ class ProductViewSet(ReadProtectedModelViewSet):
""" """
queryset = Product.objects.order_by('invoice_id', 'id') queryset = Product.objects.order_by('invoice_id', 'id')
serializer_class = ProductSerializer serializer_class = ProductSerializer
filter_backends = [DjangoFilterBackend, SearchFilter] filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
filterset_fields = ['invoice', 'designation', 'quantity', 'amount', ] filterset_fields = ['invoice', 'designation', 'quantity', 'amount', ]
search_fields = ['$designation', '$invoice__object', ] search_fields = ['$designation', '$invoice__object', ]
@ -44,7 +44,7 @@ class RemittanceTypeViewSet(ReadProtectedModelViewSet):
""" """
queryset = RemittanceType.objects.order_by('id') queryset = RemittanceType.objects.order_by('id')
serializer_class = RemittanceTypeSerializer serializer_class = RemittanceTypeSerializer
filter_backends = [DjangoFilterBackend, SearchFilter] filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
filterset_fields = ['note', ] filterset_fields = ['note', ]
search_fields = ['$note__special_type', ] search_fields = ['$note__special_type', ]
@ -57,7 +57,7 @@ class RemittanceViewSet(ReadProtectedModelViewSet):
""" """
queryset = Remittance.objects.order_by('id') queryset = Remittance.objects.order_by('id')
serializer_class = RemittanceSerializer serializer_class = RemittanceSerializer
filter_backends = [DjangoFilterBackend, SearchFilter] filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
filterset_fields = ['date', 'remittance_type', 'comment', 'closed', 'transaction_proxies__transaction', ] filterset_fields = ['date', 'remittance_type', 'comment', 'closed', 'transaction_proxies__transaction', ]
search_fields = ['$remittance_type__note__special_type', '$comment', ] search_fields = ['$remittance_type__note__special_type', '$comment', ]
@ -70,7 +70,7 @@ class SogeCreditViewSet(ReadProtectedModelViewSet):
""" """
queryset = SogeCredit.objects.order_by('id') queryset = SogeCredit.objects.order_by('id')
serializer_class = SogeCreditSerializer serializer_class = SogeCreditSerializer
filter_backends = [DjangoFilterBackend, SearchFilter] filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
filterset_fields = ['user', 'user__last_name', 'user__first_name', 'user__email', 'user__note__alias__name', filterset_fields = ['user', 'user__last_name', 'user__first_name', 'user__email', 'user__note__alias__name',
'user__note__alias__normalized_name', 'transactions', 'credit_transaction', ] 'user__note__alias__normalized_name', 'transactions', 'credit_transaction', ]
search_fields = ['$user__last_name', '$user__first_name', '$user__email', '$user__note__alias__name', search_fields = ['$user__last_name', '$user__first_name', '$user__email', '$user__note__alias__name',

View File

@ -1,49 +0,0 @@
# Generated by Django 2.2.28 on 2024-08-07 12:09
import datetime
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('treasury', '0008_auto_20240322_0045'),
]
operations = [
migrations.CreateModel(
name='NoteSummary',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date', models.DateField(default=datetime.date.today, verbose_name='Date')),
('total_positive_user', models.PositiveIntegerField(verbose_name='Total positive user')),
('balance_positive_user', models.PositiveIntegerField(verbose_name='Balance positive user')),
('total_positive_user_bde', models.PositiveIntegerField(verbose_name='Total positive user BDE')),
('balance_positive_user_bde', models.PositiveIntegerField(verbose_name='Balance positive user BDE')),
('total_zero_user', models.PositiveIntegerField(verbose_name='Total zero user')),
('total_zero_user_bde', models.PositiveIntegerField(verbose_name='Total zero user BDE')),
('total_negative_user', models.PositiveIntegerField(verbose_name='Total negative user')),
('balance_negative_user', models.PositiveIntegerField(verbose_name='Balance negative user')),
('total_negative_user_bde', models.PositiveIntegerField(verbose_name='Total negative user BDE')),
('balance_negative_user_bde', models.PositiveIntegerField(verbose_name='Balance negative user BDE')),
('total_vnegative_user', models.PositiveIntegerField(verbose_name='Total very negative user')),
('balance_vnegative_user', models.PositiveIntegerField(verbose_name='Balance very negative user')),
('total_vnegative_user_bde', models.PositiveIntegerField(verbose_name='Total very negative user BDE')),
('balance_vnegative_user_bde', models.PositiveIntegerField(verbose_name='Balance very negative user BDE')),
('total_positive_club', models.PositiveIntegerField(verbose_name='Total positive club')),
('balance_positive_club', models.PositiveIntegerField(verbose_name='Balance positive club')),
('total_positive_club_nbde', models.PositiveIntegerField(verbose_name='Total positive club nbde')),
('balance_positive_club_nbde', models.PositiveIntegerField(verbose_name='Balance positive club nbde')),
('total_zero_club', models.PositiveIntegerField(verbose_name='Total zero club')),
('total_zero_club_nbde', models.PositiveIntegerField(verbose_name='Total zero club nbde')),
('total_negative_club', models.PositiveIntegerField(verbose_name='Total negative club')),
('balance_negative_club', models.PositiveIntegerField(verbose_name='Balance negative club')),
('total_negative_club_nbde', models.PositiveIntegerField(verbose_name='Total negative club nbde')),
('balance_negative_club_nbde', models.PositiveIntegerField(verbose_name='Balance negative club nbde')),
],
options={
'verbose_name': 'Summary',
'verbose_name_plural': 'Summaries',
},
),
]

View File

@ -460,117 +460,3 @@ class SogeCredit(models.Model):
self.credit_transaction._force_save = True self.credit_transaction._force_save = True
self.credit_transaction.save() self.credit_transaction.save()
super().delete(**kwargs) super().delete(**kwargs)
class NoteSummary(models.Model):
"""
Summary of every notes
"""
date = models.DateField(
default=date.today,
verbose_name=_("Date"),
)
total_positive_user = models.PositiveIntegerField(
verbose_name=_("Total positive user"),
)
balance_positive_user = models.PositiveIntegerField(
verbose_name=_("Balance positive user"),
)
total_positive_user_bde = models.PositiveIntegerField(
verbose_name=_("Total positive user BDE"),
)
balance_positive_user_bde = models.PositiveIntegerField(
verbose_name=_("Balance positive user BDE"),
)
total_zero_user = models.PositiveIntegerField(
verbose_name=_("Total zero user"),
)
total_zero_user_bde = models.PositiveIntegerField(
verbose_name=_("Total zero user BDE"),
)
total_negative_user = models.PositiveIntegerField(
verbose_name=_("Total negative user"),
)
balance_negative_user = models.PositiveIntegerField(
verbose_name=_("Balance negative user"),
)
total_negative_user_bde = models.PositiveIntegerField(
verbose_name=_("Total negative user BDE"),
)
balance_negative_user_bde = models.PositiveIntegerField(
verbose_name=_("Balance negative user BDE"),
)
total_vnegative_user = models.PositiveIntegerField(
verbose_name=_("Total very negative user"),
)
balance_vnegative_user = models.PositiveIntegerField(
verbose_name=_("Balance very negative user"),
)
total_vnegative_user_bde = models.PositiveIntegerField(
verbose_name=_("Total very negative user BDE"),
)
balance_vnegative_user_bde = models.PositiveIntegerField(
verbose_name=_("Balance very negative user BDE"),
)
total_positive_club = models.PositiveIntegerField(
verbose_name=_("Total positive club"),
)
balance_positive_club = models.PositiveIntegerField(
verbose_name=_("Balance positive club"),
)
total_positive_club_nbde = models.PositiveIntegerField(
verbose_name=_("Total positive club nbde"),
)
balance_positive_club_nbde = models.PositiveIntegerField(
verbose_name=_("Balance positive club nbde"),
)
total_zero_club = models.PositiveIntegerField(
verbose_name=_("Total zero club"),
)
total_zero_club_nbde = models.PositiveIntegerField(
verbose_name=_("Total zero club nbde"),
)
total_negative_club = models.PositiveIntegerField(
verbose_name=_("Total negative club"),
)
balance_negative_club = models.PositiveIntegerField(
verbose_name=_("Balance negative club"),
)
total_negative_club_nbde = models.PositiveIntegerField(
verbose_name=_("Total negative club nbde"),
)
balance_negative_club_nbde = models.PositiveIntegerField(
verbose_name=_("Balance negative club nbde"),
)
class Meta:
verbose_name = _("Summary")
verbose_name_plural = _("Summaries")
def __str__(self):
return "Note summary of {date}".format(date=self.date)

View File

@ -20,6 +20,7 @@ from django.views.generic import UpdateView, DetailView
from django.views.generic.base import View, TemplateView from django.views.generic.base import View, TemplateView
from django.views.generic.edit import BaseFormView, DeleteView from django.views.generic.edit import BaseFormView, DeleteView
from django_tables2 import SingleTableView from django_tables2 import SingleTableView
from api.viewsets import is_regex
from note.models import SpecialTransaction, NoteSpecial, Alias from note.models import SpecialTransaction, NoteSpecial, Alias
from note_kfet.settings.base import BASE_DIR from note_kfet.settings.base import BASE_DIR
from permission.backends import PermissionBackend from permission.backends import PermissionBackend
@ -411,11 +412,16 @@ class SogeCreditListView(LoginRequiredMixin, ProtectQuerysetMixin, SingleTableVi
if "search" in self.request.GET: if "search" in self.request.GET:
pattern = self.request.GET["search"] pattern = self.request.GET["search"]
if pattern: if pattern:
# Check if this is a valid regex. If not, we won't check regex
valid_regex = is_regex(pattern)
suffix_alias = "__iregex" if valid_regex else "__icontains"
suffix = "__iregex" if valid_regex else "__istartswith"
prefix = "^" if valid_regex else ""
qs = qs.filter( qs = qs.filter(
Q(user__first_name__iregex=pattern) Q(**{f"user__first_name{suffix}": pattern})
| Q(user__last_name__iregex=pattern) | Q(**{f"user__last_name{suffix}": pattern})
| Q(user__note__alias__name__iregex="^" + pattern) | Q(**{f"user__note__alias__name{suffix_alias}": prefix + pattern})
| Q(user__note__alias__normalized_name__iregex="^" + Alias.normalize(pattern)) | Q(**{f"user__note__alias__normalized_name{suffix_alias}": prefix + Alias.normalize(pattern)})
) )
if "valid" not in self.request.GET or not self.request.GET["valid"]: if "valid" not in self.request.GET or not self.request.GET["valid"]:

View File

@ -2,7 +2,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django_filters.rest_framework import DjangoFilterBackend 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 api.viewsets import ReadProtectedModelViewSet
from .serializers import WEIClubSerializer, BusSerializer, BusTeamSerializer, WEIRoleSerializer, \ from .serializers import WEIClubSerializer, BusSerializer, BusTeamSerializer, WEIRoleSerializer, \
@ -18,7 +19,7 @@ class WEIClubViewSet(ReadProtectedModelViewSet):
""" """
queryset = WEIClub.objects.order_by('id') queryset = WEIClub.objects.order_by('id')
serializer_class = WEIClubSerializer serializer_class = WEIClubSerializer
filter_backends = [DjangoFilterBackend, SearchFilter] filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
filterset_fields = ['name', 'year', 'date_start', 'date_end', 'email', 'note__alias__name', filterset_fields = ['name', 'year', 'date_start', 'date_end', 'email', 'note__alias__name',
'note__alias__normalized_name', 'parent_club', 'parent_club__name', 'require_memberships', 'note__alias__normalized_name', 'parent_club', 'parent_club__name', 'require_memberships',
'membership_fee_paid', 'membership_fee_unpaid', 'membership_duration', 'membership_start', 'membership_fee_paid', 'membership_fee_unpaid', 'membership_duration', 'membership_start',
@ -34,7 +35,7 @@ class BusViewSet(ReadProtectedModelViewSet):
""" """
queryset = Bus.objects.order_by('id') queryset = Bus.objects.order_by('id')
serializer_class = BusSerializer serializer_class = BusSerializer
filter_backends = [DjangoFilterBackend, SearchFilter] filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
filterset_fields = ['name', 'wei', 'description', ] filterset_fields = ['name', 'wei', 'description', ]
search_fields = ['$name', '$wei__name', '$description', ] search_fields = ['$name', '$wei__name', '$description', ]
@ -47,7 +48,7 @@ class BusTeamViewSet(ReadProtectedModelViewSet):
""" """
queryset = BusTeam.objects.order_by('id') queryset = BusTeam.objects.order_by('id')
serializer_class = BusTeamSerializer serializer_class = BusTeamSerializer
filter_backends = [DjangoFilterBackend, SearchFilter] filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
filterset_fields = ['name', 'bus', 'color', 'description', 'bus__wei', ] filterset_fields = ['name', 'bus', 'color', 'description', 'bus__wei', ]
search_fields = ['$name', '$bus__name', '$bus__wei__name', '$description', ] search_fields = ['$name', '$bus__name', '$bus__wei__name', '$description', ]
@ -60,7 +61,7 @@ class WEIRoleViewSet(ReadProtectedModelViewSet):
""" """
queryset = WEIRole.objects.order_by('id') queryset = WEIRole.objects.order_by('id')
serializer_class = WEIRoleSerializer serializer_class = WEIRoleSerializer
filter_backends = [DjangoFilterBackend, SearchFilter] filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
filterset_fields = ['name', 'permissions', 'memberships', ] filterset_fields = ['name', 'permissions', 'memberships', ]
search_fields = ['$name', ] search_fields = ['$name', ]
@ -73,7 +74,7 @@ class WEIRegistrationViewSet(ReadProtectedModelViewSet):
""" """
queryset = WEIRegistration.objects.order_by('id') queryset = WEIRegistration.objects.order_by('id')
serializer_class = WEIRegistrationSerializer serializer_class = WEIRegistrationSerializer
filter_backends = [DjangoFilterBackend, SearchFilter] filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
filterset_fields = ['user', 'user__username', 'user__first_name', 'user__last_name', 'user__email', filterset_fields = ['user', 'user__username', 'user__first_name', 'user__last_name', 'user__email',
'user__note__alias__name', 'user__note__alias__normalized_name', 'wei', 'wei__name', 'user__note__alias__name', 'user__note__alias__normalized_name', 'wei', 'wei__name',
'wei__email', 'wei__year', 'soge_credit', 'caution_check', 'birth_date', 'gender', 'wei__email', 'wei__year', 'soge_credit', 'caution_check', 'birth_date', 'gender',
@ -92,7 +93,7 @@ class WEIMembershipViewSet(ReadProtectedModelViewSet):
""" """
queryset = WEIMembership.objects.order_by('id') queryset = WEIMembership.objects.order_by('id')
serializer_class = WEIMembershipSerializer serializer_class = WEIMembershipSerializer
filter_backends = [DjangoFilterBackend, OrderingFilter, SearchFilter] filter_backends = [DjangoFilterBackend, OrderingFilter, RegexSafeSearchFilter]
filterset_fields = ['club__name', 'club__email', 'club__note__alias__name', filterset_fields = ['club__name', 'club__email', 'club__note__alias__name',
'club__note__alias__normalized_name', 'user__username', 'user__last_name', 'club__note__alias__normalized_name', 'user__username', 'user__last_name',
'user__first_name', 'user__email', 'user__note__alias__name', 'user__first_name', 'user__email', 'user__note__alias__name',

View File

@ -23,6 +23,7 @@ from django.views.generic import DetailView, UpdateView, RedirectView, TemplateV
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.generic.edit import BaseFormView, DeleteView from django.views.generic.edit import BaseFormView, DeleteView
from django_tables2 import SingleTableView from django_tables2 import SingleTableView
from api.viewsets import is_regex
from member.models import Membership, Club from member.models import Membership, Club
from note.models import Transaction, NoteClub, Alias, SpecialTransaction, NoteSpecial from note.models import Transaction, NoteClub, Alias, SpecialTransaction, NoteSpecial
from note.tables import HistoryTable from note.tables import HistoryTable
@ -219,13 +220,18 @@ class WEIMembershipsView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableVi
if not pattern: if not pattern:
return qs.none() return qs.none()
# Check if this is a valid regex. If not, we won't check regex
valid_regex = is_regex(pattern)
suffix_alias = "__iregex" if valid_regex else "__istartswith"
suffix = "__iregex" if valid_regex else "__icontains"
prefix = "^" if valid_regex else ""
qs = qs.filter( qs = qs.filter(
Q(user__first_name__iregex=pattern) Q(**{f"user__first_name{suffix}": pattern})
| Q(user__last_name__iregex=pattern) | Q(**{f"user__last_name{suffix}": pattern})
| Q(user__note__alias__name__iregex="^" + pattern) | Q(**{f"user__note__alias__name{suffix_alias}": prefix + pattern})
| Q(user__note__alias__normalized_name__iregex="^" + Alias.normalize(pattern)) | Q(**{f"user__note__alias__normalized_name{suffix_alias}": prefix + Alias.normalize(pattern)})
| Q(bus__name__iregex=pattern) | Q(**{f"bus__name{suffix}": pattern})
| Q(team__name__iregex=pattern) | Q(**{f"team__name{suffix}": pattern})
) )
return qs return qs
@ -255,11 +261,16 @@ class WEIRegistrationsView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTable
pattern = self.request.GET.get("search", "") pattern = self.request.GET.get("search", "")
if pattern: if pattern:
# Check if this is a valid regex. If not, we won't check regex
valid_regex = is_regex(pattern)
suffix_alias = "__iregex" if valid_regex else "__istartswith"
suffix = "__iregex" if valid_regex else "__icontains"
prefix = "^" if valid_regex else ""
qs = qs.filter( qs = qs.filter(
Q(user__first_name__iregex=pattern) Q(**{f"user__first_name{suffix}": pattern})
| Q(user__last_name__iregex=pattern) | Q(**{f"user__last_name{suffix}": pattern})
| Q(user__note__alias__name__iregex="^" + pattern) | Q(**{f"user__note__alias__name{suffix_alias}": prefix + pattern})
| Q(user__note__alias__normalized_name__iregex="^" + Alias.normalize(pattern)) | Q(**{f"user__note__alias__normalized_name{suffix_alias}": prefix + Alias.normalize(pattern)})
) )
return qs return qs

View File

@ -1701,8 +1701,6 @@ msgstr "Historique des transactions récentes"
#: apps/note/templates/note/mails/negative_balance.txt:25 #: apps/note/templates/note/mails/negative_balance.txt:25
#: apps/note/templates/note/mails/negative_notes_report.html:46 #: apps/note/templates/note/mails/negative_notes_report.html:46
#: apps/note/templates/note/mails/negative_notes_report.txt:13 #: apps/note/templates/note/mails/negative_notes_report.txt:13
#: apps/note/templates/note/mails/summary_notes_report.html:62
#: apps/note/templates/note/mails/summary_ntoes_report.txt:33
#: apps/note/templates/note/mails/weekly_report.html:51 #: apps/note/templates/note/mails/weekly_report.html:51
#: apps/note/templates/note/mails/weekly_report.txt:32 #: apps/note/templates/note/mails/weekly_report.txt:32
#: apps/registration/templates/registration/mails/email_validation_email.html:40 #: apps/registration/templates/registration/mails/email_validation_email.html:40
@ -2264,7 +2262,6 @@ msgid "Address"
msgstr "Adresse" msgstr "Adresse"
#: apps/treasury/models.py:69 apps/treasury/models.py:202 #: apps/treasury/models.py:69 apps/treasury/models.py:202
#: apps/treasury/models.py:472
msgid "Date" msgid "Date"
msgstr "Date" msgstr "Date"
@ -2383,102 +2380,6 @@ msgstr ""
"Cet·te utilisateur·ice n'a pas assez d'argent pour payer les adhésions avec sa " "Cet·te utilisateur·ice n'a pas assez d'argent pour payer les adhésions avec sa "
"note. Merci de lui demander de recharger sa note avant d'invalider ce crédit." "note. Merci de lui demander de recharger sa note avant d'invalider ce crédit."
#: apps/treasury/models.py:476
msgid "Total positive user"
msgstr "Nombre d'utilisateur⋅rices en positif"
#: apps/treasury/models.py:480
msgid "Balance positive user"
msgstr "Solde des utilisateur⋅rices en positif"
#: apps/treasury/models.py:484
msgid "Total positive user BDE"
msgstr "Nombre d'adhérent⋅es au BDE en positif"
#: apps/treasury/models.py:488
msgid "Balance positive user BDE"
msgstr "Solde des adhérent⋅es au BDE en positif"
#: apps/treasury/models.py:492
msgid "Total zero user"
msgstr "Nombre d'utilisateur⋅rices à zéro"
#: apps/treasury/models.py:496
msgid "Total zero user BDE"
msgstr "Nombre d'adhérent⋅es au BDE à zéro"
#: apps/treasury/models.py:500
msgid "Total negative user"
msgstr "Nombre d'utilisateur⋅rices en négatif"
#: apps/treasury/models.py:504
msgid "Balance negative user"
msgstr "Solde des utilisateur⋅rices en négatif"
#: apps/treasury/models.py:508
msgid "Total negative user BDE"
msgstr "Nombre d'adhérent⋅es au BDE en négatif"
#: apps/treasury/models.py:512
msgid "Balance negative user BDE"
msgstr "Solde des adhérent⋅es au BDE en négatif"
#: apps/treasury/models.py:516
msgid "Total very negative user"
msgstr "Nombre d'utilisateur⋅rices en négatif sévère"
#: apps/treasury/models.py:520
msgid "Balance very negative user"
msgstr "Solde des utilisateur⋅rices en négatif sévère"
#: apps/treasury/models.py:524
msgid "Total very negative user BDE"
msgstr "Nombre d'adhérent⋅es au BDE en négatif sévère"
#: apps/treasury/models.py:528
msgid "Balance very negative user BDE"
msgstr "Solde des adhérent⋅es au BDE en négatif sévère"
#: apps/treasury/models.py:532
msgid "Total positive club"
msgstr "Nombre de clubs en positif"
#: apps/treasury/models.py:536
msgid "Balance positive club"
msgstr "Solde des clubs en positif"
#: apps/treasury/models.py:540
msgid "Total positive club nbde"
msgstr "Nombre de clubs non-BDE en positif"
#: apps/treasury/models.py:544
msgid "Balance positive club nbde"
msgstr "Solde des clubs non-BDE en positif"
#: apps/treasury/models.py:548
msgid "Total zero club"
msgstr "Nombre de clubs à zéro"
#: apps/treasury/models.py:552
msgid "Total zero club nbde"
msgstr "Nombre de clubs non-BDE à zéro"
#: apps/treasury/models.py:556
msgid "Total negative club"
msgstr "Nombre de clubs en négatif"
#: apps/treasury/models.py:560
msgid "Balance negative club"
msgstr "Solde des clubs en négatif"
#: apps/treasury/models.py:564
msgid "Total negative club nbde"
msgstr "Nombre de clubs non-BDE en négatif"
#: apps/treasury/models.py:568
msgid "Balance negative club nbde"
msgstr "Solde des clubs non-BDE en négatif"
#: apps/treasury/tables.py:20 #: apps/treasury/tables.py:20
msgid "Invoice #{:d}" msgid "Invoice #{:d}"
msgstr "Facture n°{:d}" msgstr "Facture n°{:d}"

View File

@ -19,8 +19,6 @@ MAILTO=notekfet2020@lists.crans.org
00 5 * * 2 root cd /var/www/note_kfet && env/bin/python manage.py send_mail_to_negative_balances --spam --negative-amount 1 -v 0 00 5 * * 2 root cd /var/www/note_kfet && env/bin/python manage.py send_mail_to_negative_balances --spam --negative-amount 1 -v 0
# Envoyer le rapport mensuel aux trésoriers et respos info # Envoyer le rapport mensuel aux trésoriers et respos info
00 8 * * 5 root cd /var/www/note_kfet && env/bin/python manage.py send_mail_to_negative_balances --report --add-years 1 -v 0 00 8 * * 5 root cd /var/www/note_kfet && env/bin/python manage.py send_mail_to_negative_balances --report --add-years 1 -v 0
# Envoyer le recap de tresorerie
00 8 * * 5 root cd /var/www/note_kfet && env/bin/python manage.py send_summary_notes_report --negative-amount 2000
# Envoyer les rapports aux gens # Envoyer les rapports aux gens
55 6 * * * root cd /var/www/note_kfet && env/bin/python manage.py send_reports -v 0 55 6 * * * root cd /var/www/note_kfet && env/bin/python manage.py send_reports -v 0
# Mettre à jour les boutons mis en avant # Mettre à jour les boutons mis en avant