mirror of https://gitlab.crans.org/bde/nk20
Compare commits
13 Commits
de7b91db2a
...
7322d55789
Author | SHA1 | Date |
---|---|---|
korenstin | 7322d55789 | |
Yohann D'ANELLO | 1a258dfe9e | |
korenstin | b8f81048a5 | |
korenstin | af819f45a1 | |
korenstin | 076d065ffa | |
korenstin | 2da77d9c17 | |
korenstin | 01584d6330 | |
korenstin | 4c0a5922c4 | |
korenstin | f90b28fc7c | |
korenstin | 925e0f26f5 | |
quark | c912383f86 | |
quark | 32830e43fd | |
korenstin | 11c6a6fa7a |
|
@ -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', ]
|
||||||
|
|
|
@ -46,4 +46,4 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
</h3>
|
</h3>
|
||||||
{% render_table table %}
|
{% render_table table %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -17,7 +17,9 @@ from django.utils.translation import gettext_lazy as _
|
||||||
from django.views import View
|
from django.views import View
|
||||||
from django.views.decorators.cache import cache_page
|
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_tables2.views import SingleTableView
|
from django.views.generic.list import ListView
|
||||||
|
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
|
||||||
|
@ -57,27 +59,40 @@ class ActivityCreateView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||||
return reverse_lazy('activity:activity_detail', kwargs={"pk": self.object.pk})
|
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.
|
Displays all Activities, and classify if they are on-going or upcoming ones.
|
||||||
"""
|
"""
|
||||||
model = Activity
|
model = Activity
|
||||||
table_class = ActivityTable
|
tables = [ActivityTable, ActivityTable]
|
||||||
ordering = ('-date_start',)
|
|
||||||
extra_context = {"title": _("Activities")}
|
extra_context = {"title": _("Activities")}
|
||||||
|
|
||||||
def get_queryset(self, **kwargs):
|
def get_queryset(self, **kwargs):
|
||||||
return super().get_queryset(**kwargs).distinct()
|
return super().get_queryset(**kwargs).distinct()
|
||||||
|
|
||||||
|
def get_tables(self):
|
||||||
|
tables = super().get_tables()
|
||||||
|
|
||||||
|
tables[0].prefix = "all-"
|
||||||
|
tables[1].prefix = "upcoming-"
|
||||||
|
return tables
|
||||||
|
|
||||||
|
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):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
upcoming_activities = Activity.objects.filter(date_end__gt=timezone.now())
|
tables = context["tables"]
|
||||||
context['upcoming'] = ActivityTable(
|
for name, table in zip(["table", "upcoming"], tables):
|
||||||
data=upcoming_activities.filter(PermissionBackend.filter_queryset(self.request, Activity, "view")),
|
context[name] = table
|
||||||
prefix='upcoming-',
|
|
||||||
order_by='date_start',
|
|
||||||
)
|
|
||||||
|
|
||||||
started_activities = self.get_queryset().filter(open=True, valid=True).distinct().all()
|
started_activities = self.get_queryset().filter(open=True, valid=True).distinct().all()
|
||||||
context["started_activities"] = started_activities
|
context["started_activities"] = started_activities
|
||||||
|
@ -198,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()
|
||||||
|
@ -236,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()
|
||||||
|
|
|
@ -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
|
|
@ -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)
|
||||||
|
|
|
@ -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', ]
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -138,6 +138,9 @@ class ImageForm(forms.Form):
|
||||||
|
|
||||||
return cleaned_data
|
return cleaned_data
|
||||||
|
|
||||||
|
def is_valid(self):
|
||||||
|
return super().is_valid() or super().clean().get('image') is None
|
||||||
|
|
||||||
|
|
||||||
class ClubForm(forms.ModelForm):
|
class ClubForm(forms.ModelForm):
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
@ -151,7 +154,7 @@ class ClubForm(forms.ModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Club
|
model = Club
|
||||||
fields = '__all__'
|
exclude = ("add_registration_form",)
|
||||||
widgets = {
|
widgets = {
|
||||||
"membership_fee_paid": AmountInput(),
|
"membership_fee_paid": AmountInput(),
|
||||||
"membership_fee_unpaid": AmountInput(),
|
"membership_fee_unpaid": AmountInput(),
|
||||||
|
|
|
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -259,6 +259,11 @@ class Club(models.Model):
|
||||||
help_text=_('Maximal date of a membership, after which members must renew it.'),
|
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:
|
class Meta:
|
||||||
verbose_name = _("club")
|
verbose_name = _("club")
|
||||||
verbose_name_plural = _("clubs")
|
verbose_name_plural = _("clubs")
|
||||||
|
|
|
@ -14,6 +14,9 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
<form method="post" enctype="multipart/form-data" id="formUpload">
|
<form method="post" enctype="multipart/form-data" id="formUpload">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ form |crispy }}
|
{{ form |crispy }}
|
||||||
|
{% if user.note.display_image != "pic/default.png" %}
|
||||||
|
<input type="submit" class="btn btn-primary" value="{% trans "Remove" %}">
|
||||||
|
{% endif %}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<!-- MODAL TO CROP THE IMAGE -->
|
<!-- MODAL TO CROP THE IMAGE -->
|
||||||
|
|
|
@ -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()
|
||||||
|
@ -326,12 +331,15 @@ class PictureUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin, Det
|
||||||
"""Save image to note"""
|
"""Save image to note"""
|
||||||
image = form.cleaned_data['image']
|
image = form.cleaned_data['image']
|
||||||
|
|
||||||
# Rename as a PNG or GIF
|
if image is None:
|
||||||
extension = image.name.split(".")[-1]
|
image = "pic/default.png"
|
||||||
if extension == "gif":
|
|
||||||
image.name = "{}_pic.gif".format(self.object.note.pk)
|
|
||||||
else:
|
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
|
# Save
|
||||||
self.object.note.display_image = image
|
self.object.note.display_image = image
|
||||||
|
@ -407,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
|
||||||
|
@ -909,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'
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Par ailleurs, le BDE ne sert pas d'alcool aux adhérents dont le solde
|
Par ailleurs, le BDE ne sert pas d'alcool aux adhérents dont le solde
|
||||||
est inférieur à 0 € depuis plus de 24h.
|
est inférieur à 0 €.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
|
@ -43,4 +43,4 @@
|
||||||
{% trans "Mail generated by the Note Kfet on the" %} {% now "j F Y à H:i:s" %}
|
{% trans "Mail generated by the Note Kfet on the" %} {% now "j F Y à H:i:s" %}
|
||||||
</p>
|
</p>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -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"]:
|
||||||
|
|
|
@ -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', ]
|
||||||
|
|
|
@ -2591,12 +2591,12 @@
|
||||||
"note",
|
"note",
|
||||||
"transaction"
|
"transaction"
|
||||||
],
|
],
|
||||||
"query": "[\"OR\", {\"source__balance__gte\": {\"F\": [\"SUB\", [\"MUL\", [\"F\", \"amount\"], [\"F\", \"quantity\"]], 2000]}}, {\"valid\": false}]",
|
"query": "[\"OR\", {\"source__balance__gte\": 0}, [\"AND\", [\"NOT\", {\"recurrenttransaction__template__category__name\": \"Alcool\"}], {\"source__balance__gte\": {\"F\": [\"SUB\", [\"MUL\", [\"F\", \"amount\"], [\"F\", \"quantity\"]], 2000]}}], {\"valid\": false}]",
|
||||||
"type": "add",
|
"type": "add",
|
||||||
"mask": 2,
|
"mask": 2,
|
||||||
"field": "",
|
"field": "",
|
||||||
"permanent": false,
|
"permanent": false,
|
||||||
"description": "Créer une transaction quelconque tant que la source reste au-dessus de -20 €"
|
"description": "Créer une transaction quelconque tant que la source reste positive s'il s'agit d'alcool, sinon au-dessus de -20€"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -5,7 +5,6 @@ from django import forms
|
||||||
from django.contrib.auth.forms import UserCreationForm
|
from django.contrib.auth.forms import UserCreationForm
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
# from member.models import Club
|
|
||||||
from note.models import NoteSpecial, Alias
|
from note.models import NoteSpecial, Alias
|
||||||
from note_kfet.inputs import AmountInput
|
from note_kfet.inputs import AmountInput
|
||||||
|
|
||||||
|
@ -115,12 +114,3 @@ class ValidationForm(forms.Form):
|
||||||
required=False,
|
required=False,
|
||||||
initial=True,
|
initial=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
# If the bda exists
|
|
||||||
# if Club.objects.filter(name__iexact="bda").exists():
|
|
||||||
# The user can join the bda club at the inscription
|
|
||||||
# join_bda = forms.BooleanField(
|
|
||||||
# label=_("Join BDA Club"),
|
|
||||||
# required=False,
|
|
||||||
# initial=True,
|
|
||||||
# )
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# 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 django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
@ -15,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
|
||||||
|
@ -191,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
|
||||||
|
@ -238,9 +245,8 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
|
||||||
fee += bde.membership_fee_paid if user.profile.paid else bde.membership_fee_unpaid
|
fee += bde.membership_fee_paid if user.profile.paid else bde.membership_fee_unpaid
|
||||||
kfet = Club.objects.get(name="Kfet")
|
kfet = Club.objects.get(name="Kfet")
|
||||||
fee += kfet.membership_fee_paid if user.profile.paid else kfet.membership_fee_unpaid
|
fee += kfet.membership_fee_paid if user.profile.paid else kfet.membership_fee_unpaid
|
||||||
if Club.objects.filter(name__iexact="BDA").exists():
|
for club in Club.objects.filter(add_registration_form=True):
|
||||||
bda = Club.objects.get(name__iexact="BDA")
|
fee += club.membership_fee_paid if user.profile.paid else club.membership_fee_unpaid
|
||||||
fee += bda.membership_fee_paid if user.profile.paid else bda.membership_fee_unpaid
|
|
||||||
ctx["total_fee"] = "{:.02f}".format(fee / 100, )
|
ctx["total_fee"] = "{:.02f}".format(fee / 100, )
|
||||||
|
|
||||||
# ctx["declare_soge_account"] = SogeCredit.objects.filter(user=user).exists()
|
# ctx["declare_soge_account"] = SogeCredit.objects.filter(user=user).exists()
|
||||||
|
@ -249,6 +255,16 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
|
||||||
|
|
||||||
def get_form(self, form_class=None):
|
def get_form(self, form_class=None):
|
||||||
form = super().get_form(form_class)
|
form = super().get_form(form_class)
|
||||||
|
|
||||||
|
# add clubs that are in registration form
|
||||||
|
for club in Club.objects.filter(add_registration_form=True).order_by("name"):
|
||||||
|
form_join_club = forms.BooleanField(
|
||||||
|
label=_("Join %(club)s Club") % {'club': club.name},
|
||||||
|
required=False,
|
||||||
|
initial=False,
|
||||||
|
)
|
||||||
|
form.fields.update({f"join_{club.id}": form_join_club})
|
||||||
|
|
||||||
user = self.get_object()
|
user = self.get_object()
|
||||||
form.fields["last_name"].initial = user.last_name
|
form.fields["last_name"].initial = user.last_name
|
||||||
form.fields["first_name"].initial = user.first_name
|
form.fields["first_name"].initial = user.first_name
|
||||||
|
@ -266,11 +282,6 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
|
||||||
form.add_error(None, _("An alias with a similar name already exists."))
|
form.add_error(None, _("An alias with a similar name already exists."))
|
||||||
return self.form_invalid(form)
|
return self.form_invalid(form)
|
||||||
|
|
||||||
# Check if BDA exist to propose membership at regisration
|
|
||||||
bda_exists = False
|
|
||||||
if Club.objects.filter(name__iexact="BDA").exists():
|
|
||||||
bda_exists = True
|
|
||||||
|
|
||||||
# Get form data
|
# Get form data
|
||||||
# soge = form.cleaned_data["soge"]
|
# soge = form.cleaned_data["soge"]
|
||||||
credit_type = form.cleaned_data["credit_type"]
|
credit_type = form.cleaned_data["credit_type"]
|
||||||
|
@ -280,8 +291,9 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
|
||||||
bank = form.cleaned_data["bank"]
|
bank = form.cleaned_data["bank"]
|
||||||
join_bde = form.cleaned_data["join_bde"]
|
join_bde = form.cleaned_data["join_bde"]
|
||||||
join_kfet = form.cleaned_data["join_kfet"]
|
join_kfet = form.cleaned_data["join_kfet"]
|
||||||
if bda_exists:
|
|
||||||
join_bda = form.cleaned_data["join_bda"]
|
clubs_registration = Club.objects.filter(add_registration_form=True).order_by("name")
|
||||||
|
join_clubs = [(club, form.cleaned_data[f"join_{club.id}"]) for club in clubs_registration]
|
||||||
|
|
||||||
# if soge:
|
# if soge:
|
||||||
# # If Société Générale pays the inscription, the user automatically joins the two clubs.
|
# # If Société Générale pays the inscription, the user automatically joins the two clubs.
|
||||||
|
@ -303,11 +315,12 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
|
||||||
kfet_fee = kfet.membership_fee_paid if user.profile.paid else kfet.membership_fee_unpaid
|
kfet_fee = kfet.membership_fee_paid if user.profile.paid else kfet.membership_fee_unpaid
|
||||||
# Add extra fee for the full membership
|
# Add extra fee for the full membership
|
||||||
fee += kfet_fee if join_kfet else 0
|
fee += kfet_fee if join_kfet else 0
|
||||||
if bda_exists:
|
clubs_fee = dict()
|
||||||
bda = Club.objects.get(name__iexact="BDA")
|
for club, join_club in join_clubs:
|
||||||
bda_fee = bda.membership_fee_paid if user.profile.paid else bda.membership_fee_unpaid
|
club_fee = club.membership_fee_paid if user.profile.paid else club.membership_fee_unpaid
|
||||||
# Add extra fee for the bda membership
|
# Add extra fee for the club membership
|
||||||
fee += bda_fee if join_bda else 0
|
clubs_fee[club] = club_fee
|
||||||
|
fee += club_fee if join_club else 0
|
||||||
|
|
||||||
# # If the bank pays, then we don't credit now. Treasurers will validate the transaction
|
# # If the bank pays, then we don't credit now. Treasurers will validate the transaction
|
||||||
# # and credit the note later.
|
# # and credit the note later.
|
||||||
|
@ -387,17 +400,18 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
|
||||||
membership.roles.add(Role.objects.get(name="Adhérent Kfet"))
|
membership.roles.add(Role.objects.get(name="Adhérent Kfet"))
|
||||||
membership.save()
|
membership.save()
|
||||||
|
|
||||||
if bda_exists and join_bda:
|
for club, join_club in join_clubs:
|
||||||
# Create membership for the user to the BDA starting today
|
if join_club:
|
||||||
membership = Membership(
|
# Create membership for the user to the BDA starting today
|
||||||
club=bda,
|
membership = Membership(
|
||||||
user=user,
|
club=club,
|
||||||
fee=bda_fee,
|
user=user,
|
||||||
)
|
fee=clubs_fee[club],
|
||||||
membership.save()
|
)
|
||||||
membership.refresh_from_db()
|
membership.save()
|
||||||
membership.roles.add(Role.objects.get(name="Membre de club"))
|
membership.refresh_from_db()
|
||||||
membership.save()
|
membership.roles.add(Role.objects.get(name="Membre de club"))
|
||||||
|
membership.save()
|
||||||
|
|
||||||
# if soge:
|
# if soge:
|
||||||
# soge_credit = SogeCredit.objects.get(user=user)
|
# soge_credit = SogeCredit.objects.get(user=user)
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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"]:
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -114,7 +114,7 @@ msgstr "Lieu où l'activité est organisée, par exemple la Kfet."
|
||||||
msgid "type"
|
msgid "type"
|
||||||
msgstr "type"
|
msgstr "type"
|
||||||
|
|
||||||
#: apps/activity/models.py:89 apps/logs/models.py:22 apps/member/models.py:313
|
#: apps/activity/models.py:89 apps/logs/models.py:22 apps/member/models.py:318
|
||||||
#: apps/note/models/notes.py:148 apps/treasury/models.py:293
|
#: apps/note/models/notes.py:148 apps/treasury/models.py:293
|
||||||
#: apps/wei/models.py:171 apps/wei/templates/wei/attribute_bus_1A.html:13
|
#: apps/wei/models.py:171 apps/wei/templates/wei/attribute_bus_1A.html:13
|
||||||
#: apps/wei/templates/wei/survey.html:15
|
#: apps/wei/templates/wei/survey.html:15
|
||||||
|
@ -247,6 +247,7 @@ msgid "The validation of the activity is pending."
|
||||||
msgstr "La validation de cette activité est en attente."
|
msgstr "La validation de cette activité est en attente."
|
||||||
|
|
||||||
#: apps/activity/tables.py:43 apps/treasury/tables.py:107
|
#: apps/activity/tables.py:43 apps/treasury/tables.py:107
|
||||||
|
#: apps/member/templates/member/picture_update.html:18
|
||||||
msgid "Remove"
|
msgid "Remove"
|
||||||
msgstr "Supprimer"
|
msgstr "Supprimer"
|
||||||
|
|
||||||
|
@ -262,13 +263,13 @@ msgstr "supprimer"
|
||||||
msgid "Type"
|
msgid "Type"
|
||||||
msgstr "Type"
|
msgstr "Type"
|
||||||
|
|
||||||
#: apps/activity/tables.py:84 apps/member/forms.py:193
|
#: apps/activity/tables.py:84 apps/member/forms.py:196
|
||||||
#: apps/registration/forms.py:92 apps/treasury/forms.py:131
|
#: apps/registration/forms.py:92 apps/treasury/forms.py:131
|
||||||
#: apps/wei/forms/registration.py:104
|
#: apps/wei/forms/registration.py:104
|
||||||
msgid "Last name"
|
msgid "Last name"
|
||||||
msgstr "Nom de famille"
|
msgstr "Nom de famille"
|
||||||
|
|
||||||
#: apps/activity/tables.py:86 apps/member/forms.py:198
|
#: apps/activity/tables.py:86 apps/member/forms.py:201
|
||||||
#: apps/note/templates/note/transaction_form.html:138
|
#: apps/note/templates/note/transaction_form.html:138
|
||||||
#: apps/registration/forms.py:97 apps/treasury/forms.py:133
|
#: apps/registration/forms.py:97 apps/treasury/forms.py:133
|
||||||
#: apps/wei/forms/registration.py:109
|
#: apps/wei/forms/registration.py:109
|
||||||
|
@ -400,37 +401,37 @@ msgstr "Inviter"
|
||||||
msgid "Create new activity"
|
msgid "Create new activity"
|
||||||
msgstr "Créer une nouvelle activité"
|
msgstr "Créer une nouvelle activité"
|
||||||
|
|
||||||
#: apps/activity/views.py:67 note_kfet/templates/base.html:90
|
#: apps/activity/views.py:68 note_kfet/templates/base.html:90
|
||||||
msgid "Activities"
|
msgid "Activities"
|
||||||
msgstr "Activités"
|
msgstr "Activités"
|
||||||
|
|
||||||
#: apps/activity/views.py:93
|
#: apps/activity/views.py:108
|
||||||
msgid "Activity detail"
|
msgid "Activity detail"
|
||||||
msgstr "Détails de l'activité"
|
msgstr "Détails de l'activité"
|
||||||
|
|
||||||
#: apps/activity/views.py:113
|
#: apps/activity/views.py:128
|
||||||
msgid "Update activity"
|
msgid "Update activity"
|
||||||
msgstr "Modifier l'activité"
|
msgstr "Modifier l'activité"
|
||||||
|
|
||||||
#: apps/activity/views.py:140
|
#: apps/activity/views.py:155
|
||||||
msgid "Invite guest to the activity \"{}\""
|
msgid "Invite guest to the activity \"{}\""
|
||||||
msgstr "Invitation pour l'activité « {} »"
|
msgstr "Invitation pour l'activité « {} »"
|
||||||
|
|
||||||
#: apps/activity/views.py:178
|
#: apps/activity/views.py:193
|
||||||
msgid "You are not allowed to display the entry interface for this activity."
|
msgid "You are not allowed to display the entry interface for this activity."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Vous n'êtes pas autorisé·e à afficher l'interface des entrées pour cette "
|
"Vous n'êtes pas autorisé·e à afficher l'interface des entrées pour cette "
|
||||||
"activité."
|
"activité."
|
||||||
|
|
||||||
#: apps/activity/views.py:181
|
#: apps/activity/views.py:196
|
||||||
msgid "This activity does not support activity entries."
|
msgid "This activity does not support activity entries."
|
||||||
msgstr "Cette activité ne requiert pas d'entrées."
|
msgstr "Cette activité ne requiert pas d'entrées."
|
||||||
|
|
||||||
#: apps/activity/views.py:184
|
#: apps/activity/views.py:199
|
||||||
msgid "This activity is closed."
|
msgid "This activity is closed."
|
||||||
msgstr "Cette activité est fermée."
|
msgstr "Cette activité est fermée."
|
||||||
|
|
||||||
#: apps/activity/views.py:280
|
#: apps/activity/views.py:295
|
||||||
msgid "Entry for activity \"{}\""
|
msgid "Entry for activity \"{}\""
|
||||||
msgstr "Entrées pour l'activité « {} »"
|
msgstr "Entrées pour l'activité « {} »"
|
||||||
|
|
||||||
|
@ -507,11 +508,11 @@ msgstr "cotisation pour adhérer (normalien·ne élève)"
|
||||||
msgid "membership fee (unpaid students)"
|
msgid "membership fee (unpaid students)"
|
||||||
msgstr "cotisation pour adhérer (normalien·ne étudiant·e)"
|
msgstr "cotisation pour adhérer (normalien·ne étudiant·e)"
|
||||||
|
|
||||||
#: apps/member/admin.py:65 apps/member/models.py:325
|
#: apps/member/admin.py:65 apps/member/models.py:330
|
||||||
msgid "roles"
|
msgid "roles"
|
||||||
msgstr "rôles"
|
msgstr "rôles"
|
||||||
|
|
||||||
#: apps/member/admin.py:66 apps/member/models.py:339
|
#: apps/member/admin.py:66 apps/member/models.py:344
|
||||||
msgid "fee"
|
msgid "fee"
|
||||||
msgstr "cotisation"
|
msgstr "cotisation"
|
||||||
|
|
||||||
|
@ -563,29 +564,29 @@ msgid "This image cannot be loaded."
|
||||||
msgstr "Cette image ne peut pas être chargée."
|
msgstr "Cette image ne peut pas être chargée."
|
||||||
|
|
||||||
#: apps/member/forms.py:148 apps/member/views.py:102
|
#: apps/member/forms.py:148 apps/member/views.py:102
|
||||||
#: apps/registration/forms.py:34 apps/registration/views.py:266
|
#: apps/registration/forms.py:34 apps/registration/views.py:276
|
||||||
msgid "An alias with a similar name already exists."
|
msgid "An alias with a similar name already exists."
|
||||||
msgstr "Un alias avec un nom similaire existe déjà."
|
msgstr "Un alias avec un nom similaire existe déjà."
|
||||||
|
|
||||||
#: apps/member/forms.py:172
|
#: apps/member/forms.py:175
|
||||||
msgid "Inscription paid by Société Générale"
|
msgid "Inscription paid by Société Générale"
|
||||||
msgstr "Inscription payée par la Société générale"
|
msgstr "Inscription payée par la Société générale"
|
||||||
|
|
||||||
#: apps/member/forms.py:174
|
#: apps/member/forms.py:177
|
||||||
msgid "Check this case if the Société Générale paid the inscription."
|
msgid "Check this case if the Société Générale paid the inscription."
|
||||||
msgstr "Cochez cette case si la Société Générale a payé l'inscription."
|
msgstr "Cochez cette case si la Société Générale a payé l'inscription."
|
||||||
|
|
||||||
#: apps/member/forms.py:179 apps/registration/forms.py:79
|
#: apps/member/forms.py:182 apps/registration/forms.py:79
|
||||||
#: apps/wei/forms/registration.py:91
|
#: apps/wei/forms/registration.py:91
|
||||||
msgid "Credit type"
|
msgid "Credit type"
|
||||||
msgstr "Type de rechargement"
|
msgstr "Type de rechargement"
|
||||||
|
|
||||||
#: apps/member/forms.py:180 apps/registration/forms.py:80
|
#: apps/member/forms.py:183 apps/registration/forms.py:80
|
||||||
#: apps/wei/forms/registration.py:92
|
#: apps/wei/forms/registration.py:92
|
||||||
msgid "No credit"
|
msgid "No credit"
|
||||||
msgstr "Pas de rechargement"
|
msgstr "Pas de rechargement"
|
||||||
|
|
||||||
#: apps/member/forms.py:182
|
#: apps/member/forms.py:185
|
||||||
msgid "You can credit the note of the user."
|
msgid "You can credit the note of the user."
|
||||||
msgstr "Vous pouvez créditer la note de l'utilisateur·ice avant l'adhésion."
|
msgstr "Vous pouvez créditer la note de l'utilisateur·ice avant l'adhésion."
|
||||||
|
|
||||||
|
@ -594,17 +595,17 @@ msgstr "Vous pouvez créditer la note de l'utilisateur·ice avant l'adhésion."
|
||||||
msgid "Credit amount"
|
msgid "Credit amount"
|
||||||
msgstr "Montant à créditer"
|
msgstr "Montant à créditer"
|
||||||
|
|
||||||
#: apps/member/forms.py:203 apps/note/templates/note/transaction_form.html:144
|
#: apps/member/forms.py:206 apps/note/templates/note/transaction_form.html:144
|
||||||
#: apps/registration/forms.py:102 apps/treasury/forms.py:135
|
#: apps/registration/forms.py:102 apps/treasury/forms.py:135
|
||||||
#: apps/wei/forms/registration.py:114
|
#: apps/wei/forms/registration.py:114
|
||||||
msgid "Bank"
|
msgid "Bank"
|
||||||
msgstr "Banque"
|
msgstr "Banque"
|
||||||
|
|
||||||
#: apps/member/forms.py:230
|
#: apps/member/forms.py:233
|
||||||
msgid "User"
|
msgid "User"
|
||||||
msgstr "Utilisateur·ice"
|
msgstr "Utilisateur·ice"
|
||||||
|
|
||||||
#: apps/member/forms.py:244
|
#: apps/member/forms.py:247
|
||||||
msgid "Roles"
|
msgid "Roles"
|
||||||
msgstr "Rôles"
|
msgstr "Rôles"
|
||||||
|
|
||||||
|
@ -852,46 +853,50 @@ msgstr ""
|
||||||
"Date maximale d'une fin d'adhésion, après laquelle les adhérent·e·s doivent la "
|
"Date maximale d'une fin d'adhésion, après laquelle les adhérent·e·s doivent la "
|
||||||
"renouveler."
|
"renouveler."
|
||||||
|
|
||||||
#: apps/member/models.py:263 apps/member/models.py:319
|
#: apps/member/models.py:263
|
||||||
|
msgid "add to registration form"
|
||||||
|
msgstr "ajouter au formulaire d'inscription"
|
||||||
|
|
||||||
|
#: apps/member/models.py:268 apps/member/models.py:324
|
||||||
#: apps/note/models/notes.py:176
|
#: apps/note/models/notes.py:176
|
||||||
msgid "club"
|
msgid "club"
|
||||||
msgstr "club"
|
msgstr "club"
|
||||||
|
|
||||||
#: apps/member/models.py:264
|
#: apps/member/models.py:269
|
||||||
msgid "clubs"
|
msgid "clubs"
|
||||||
msgstr "clubs"
|
msgstr "clubs"
|
||||||
|
|
||||||
#: apps/member/models.py:330
|
#: apps/member/models.py:335
|
||||||
msgid "membership starts on"
|
msgid "membership starts on"
|
||||||
msgstr "l'adhésion commence le"
|
msgstr "l'adhésion commence le"
|
||||||
|
|
||||||
#: apps/member/models.py:334
|
#: apps/member/models.py:339
|
||||||
msgid "membership ends on"
|
msgid "membership ends on"
|
||||||
msgstr "l'adhésion finit le"
|
msgstr "l'adhésion finit le"
|
||||||
|
|
||||||
#: apps/member/models.py:343 apps/note/models/transactions.py:385
|
#: apps/member/models.py:348 apps/note/models/transactions.py:385
|
||||||
msgid "membership"
|
msgid "membership"
|
||||||
msgstr "adhésion"
|
msgstr "adhésion"
|
||||||
|
|
||||||
#: apps/member/models.py:344
|
#: apps/member/models.py:349
|
||||||
msgid "memberships"
|
msgid "memberships"
|
||||||
msgstr "adhésions"
|
msgstr "adhésions"
|
||||||
|
|
||||||
#: apps/member/models.py:348
|
#: apps/member/models.py:353
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Membership of {user} for the club {club}"
|
msgid "Membership of {user} for the club {club}"
|
||||||
msgstr "Adhésion de {user} pour le club {club}"
|
msgstr "Adhésion de {user} pour le club {club}"
|
||||||
|
|
||||||
#: apps/member/models.py:367
|
#: apps/member/models.py:372
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "The role {role} does not apply to the club {club}."
|
msgid "The role {role} does not apply to the club {club}."
|
||||||
msgstr "Le rôle {role} ne s'applique pas au club {club}."
|
msgstr "Le rôle {role} ne s'applique pas au club {club}."
|
||||||
|
|
||||||
#: apps/member/models.py:376 apps/member/views.py:712
|
#: apps/member/models.py:381 apps/member/views.py:712
|
||||||
msgid "User is already a member of the club"
|
msgid "User is already a member of the club"
|
||||||
msgstr "L'utilisateur·ice est déjà membre du club"
|
msgstr "L'utilisateur·ice est déjà membre du club"
|
||||||
|
|
||||||
#: apps/member/models.py:388 apps/member/views.py:721
|
#: apps/member/models.py:393 apps/member/views.py:721
|
||||||
msgid "User is not a member of the parent club"
|
msgid "User is not a member of the parent club"
|
||||||
msgstr "L'utilisateur·ice n'est pas membre du club parent"
|
msgstr "L'utilisateur·ice n'est pas membre du club parent"
|
||||||
|
|
||||||
|
@ -1153,11 +1158,11 @@ msgstr "Introspection :"
|
||||||
msgid "Show my applications"
|
msgid "Show my applications"
|
||||||
msgstr "Voir mes applications"
|
msgstr "Voir mes applications"
|
||||||
|
|
||||||
#: apps/member/templates/member/picture_update.html:35
|
#: apps/member/templates/member/picture_update.html:38
|
||||||
msgid "Nevermind"
|
msgid "Nevermind"
|
||||||
msgstr "Annuler"
|
msgstr "Annuler"
|
||||||
|
|
||||||
#: apps/member/templates/member/picture_update.html:36
|
#: apps/member/templates/member/picture_update.html:39
|
||||||
msgid "Crop and upload"
|
msgid "Crop and upload"
|
||||||
msgstr "Recadrer et envoyer"
|
msgstr "Recadrer et envoyer"
|
||||||
|
|
||||||
|
@ -2183,18 +2188,23 @@ msgstr "Utilisateur·ice·s en attente d'inscription"
|
||||||
msgid "Registration detail"
|
msgid "Registration detail"
|
||||||
msgstr "Détails de l'inscription"
|
msgstr "Détails de l'inscription"
|
||||||
|
|
||||||
#: apps/registration/views.py:293
|
#: apps/registration/views.py:256
|
||||||
|
#, python-format
|
||||||
|
msgid "Join %(club)s Club"
|
||||||
|
msgstr "Adhérer au club %(club)s"
|
||||||
|
|
||||||
|
#: apps/registration/views.py:299
|
||||||
msgid "You must join the BDE."
|
msgid "You must join the BDE."
|
||||||
msgstr "Vous devez adhérer au BDE."
|
msgstr "Vous devez adhérer au BDE."
|
||||||
|
|
||||||
#: apps/registration/views.py:323
|
#: apps/registration/views.py:330
|
||||||
msgid ""
|
msgid ""
|
||||||
"The entered amount is not enough for the memberships, should be at least {}"
|
"The entered amount is not enough for the memberships, should be at least {}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Le montant crédité est trop faible pour adhérer, il doit être au minimum de "
|
"Le montant crédité est trop faible pour adhérer, il doit être au minimum de "
|
||||||
"{}"
|
"{}"
|
||||||
|
|
||||||
#: apps/registration/views.py:417
|
#: apps/registration/views.py:425
|
||||||
msgid "Invalidate pre-registration"
|
msgid "Invalidate pre-registration"
|
||||||
msgstr "Invalider l'inscription"
|
msgstr "Invalider l'inscription"
|
||||||
|
|
||||||
|
@ -3620,9 +3630,6 @@ msgstr ""
|
||||||
"d'adhésion. Vous devez également valider votre adresse email en suivant le "
|
"d'adhésion. Vous devez également valider votre adresse email en suivant le "
|
||||||
"lien que vous avez reçu."
|
"lien que vous avez reçu."
|
||||||
|
|
||||||
#~ msgid "Join BDA Club"
|
|
||||||
#~ msgstr "Adhérer au club BDA"
|
|
||||||
|
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
#~| msgid "People having you as a friend"
|
#~| msgid "People having you as a friend"
|
||||||
#~ msgid "You already have that person as a friend"
|
#~ msgid "You already have that person as a friend"
|
||||||
|
|
Loading…
Reference in New Issue