mirror of
https://gitlab.crans.org/bde/nk20
synced 2024-11-26 18:37:12 +00:00
Merge branch 'no-api-error' into 'main'
fix #113 Closes #113 See merge request bde/nk20!253
This commit is contained in:
commit
ac27218851
@ -1,9 +1,9 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from api.viewsets import ReadProtectedModelViewSet
|
||||
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 ..models import Activity, ActivityType, Entry, Guest
|
||||
@ -29,7 +29,7 @@ class ActivityViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
queryset = Activity.objects.order_by('id')
|
||||
serializer_class = ActivitySerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||
filterset_fields = ['name', 'description', 'activity_type', 'location', 'creater', 'organizer', 'attendees_club',
|
||||
'date_start', 'date_end', 'valid', 'open', ]
|
||||
search_fields = ['$name', '$description', '$location', '$creater__last_name', '$creater__first_name',
|
||||
@ -47,7 +47,7 @@ class GuestViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
queryset = Guest.objects.order_by('id')
|
||||
serializer_class = GuestSerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||
filterset_fields = ['activity', 'activity__name', 'last_name', 'first_name', 'inviter', 'inviter__alias__name',
|
||||
'inviter__alias__normalized_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')
|
||||
serializer_class = EntrySerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||
filterset_fields = ['activity', 'time', 'note', 'guest', ]
|
||||
search_fields = ['$activity__name', '$note__user__email', '$note__alias__name', '$note__alias__normalized_name',
|
||||
'$guest__last_name', '$guest__first_name', ]
|
||||
|
@ -19,6 +19,7 @@ from django.views.decorators.cache import cache_page
|
||||
from django.views.generic import DetailView, TemplateView, UpdateView
|
||||
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 permission.backends import PermissionBackend
|
||||
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"]:
|
||||
pattern = self.request.GET["search"]
|
||||
if pattern[0] != "^":
|
||||
pattern = "^" + pattern
|
||||
|
||||
# Check if this is a valid regex. If not, we won't check regex
|
||||
valid_regex = is_regex(pattern)
|
||||
suffix = "__iregex" if valid_regex else "__istartswith"
|
||||
pattern = "^" + pattern if valid_regex and pattern[0] != "^" else pattern
|
||||
guest_qs = guest_qs.filter(
|
||||
Q(first_name__iregex=pattern)
|
||||
| Q(last_name__iregex=pattern)
|
||||
| Q(inviter__alias__name__iregex=pattern)
|
||||
| Q(inviter__alias__normalized_name__iregex=Alias.normalize(pattern))
|
||||
Q(**{f"first_name{suffix}": pattern})
|
||||
| Q(**{f"last_name{suffix}": pattern})
|
||||
| Q(**{f"inviter__alias__name{suffix}": pattern})
|
||||
| Q(**{f"inviter__alias__normalized_name{suffix}": Alias.normalize(pattern)})
|
||||
)
|
||||
else:
|
||||
guest_qs = guest_qs.none()
|
||||
@ -250,11 +254,15 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
|
||||
|
||||
if "search" in self.request.GET and self.request.GET["search"]:
|
||||
pattern = self.request.GET["search"]
|
||||
|
||||
# Check if this is a valid regex. If not, we won't check regex
|
||||
valid_regex = is_regex(pattern)
|
||||
suffix = "__iregex" if valid_regex else "__icontains"
|
||||
note_qs = note_qs.filter(
|
||||
Q(note__noteuser__user__first_name__iregex=pattern)
|
||||
| Q(note__noteuser__user__last_name__iregex=pattern)
|
||||
| Q(name__iregex=pattern)
|
||||
| Q(normalized_name__iregex=Alias.normalize(pattern))
|
||||
Q(**{f"note__noteuser__user__first_name{suffix}": pattern})
|
||||
| Q(**{f"note__noteuser__user__last_name{suffix}": pattern})
|
||||
| Q(**{f"name{suffix}": pattern})
|
||||
| Q(**{f"normalized_name{suffix}": Alias.normalize(pattern)})
|
||||
)
|
||||
else:
|
||||
note_qs = note_qs.none()
|
||||
|
42
apps/api/filters.py
Normal file
42
apps/api/filters.py
Normal 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
|
@ -12,11 +12,12 @@ from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models.fields.files import ImageFieldFile
|
||||
from django.test import TestCase
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from phonenumbers import PhoneNumber
|
||||
from rest_framework.filters import OrderingFilter
|
||||
from api.filters import RegexSafeSearchFilter
|
||||
from member.models import Membership, Club
|
||||
from note.models import NoteClub, NoteUser, Alias, Note
|
||||
from permission.models import PermissionMask, Permission, Role
|
||||
from phonenumbers import PhoneNumber
|
||||
from rest_framework.filters import SearchFilter, OrderingFilter
|
||||
|
||||
from .viewsets import ContentTypeViewSet, UserViewSet
|
||||
|
||||
@ -87,7 +88,7 @@ class TestAPI(TestCase):
|
||||
resp = self.client.get(url + f"?ordering=-{field}")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
if SearchFilter in backends:
|
||||
if RegexSafeSearchFilter in backends:
|
||||
# Basic search
|
||||
for field in viewset.search_fields:
|
||||
obj = self.fix_note_object(obj, field)
|
||||
|
@ -1,19 +1,29 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import re
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from django.db.models import Q
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from rest_framework.filters import SearchFilter
|
||||
from rest_framework.viewsets import ReadOnlyModelViewSet, ModelViewSet
|
||||
from permission.backends import PermissionBackend
|
||||
from note.models import Alias
|
||||
|
||||
from .filters import RegexSafeSearchFilter
|
||||
from .serializers import UserSerializer, ContentTypeSerializer
|
||||
|
||||
|
||||
def is_regex(pattern):
|
||||
try:
|
||||
re.compile(pattern)
|
||||
return True
|
||||
except (re.error, TypeError):
|
||||
return False
|
||||
|
||||
|
||||
class ReadProtectedModelViewSet(ModelViewSet):
|
||||
"""
|
||||
Protect a ModelViewSet by filtering the objects that the user cannot see.
|
||||
@ -60,34 +70,38 @@ class UserViewSet(ReadProtectedModelViewSet):
|
||||
|
||||
if "search" in self.request.GET:
|
||||
pattern = self.request.GET["search"]
|
||||
# Check if this is a valid regex. If not, we won't check regex
|
||||
valid_regex = is_regex(pattern)
|
||||
suffix = "__iregex" if valid_regex else "__istartswith"
|
||||
prefix = "^" if valid_regex else ""
|
||||
|
||||
# Filter with different rules
|
||||
# We use union-all to keep each filter rule sorted in result
|
||||
queryset = queryset.filter(
|
||||
# Match without normalization
|
||||
note__alias__name__iregex="^" + pattern
|
||||
Q(**{f"note__alias__name{suffix}": prefix + pattern})
|
||||
).union(
|
||||
queryset.filter(
|
||||
# Match with normalization
|
||||
Q(note__alias__normalized_name__iregex="^" + Alias.normalize(pattern))
|
||||
& ~Q(note__alias__name__iregex="^" + pattern)
|
||||
Q(**{f"note__alias__normalized_name{suffix}": prefix + Alias.normalize(pattern)})
|
||||
& ~Q(**{f"note__alias__name{suffix}": prefix + pattern})
|
||||
),
|
||||
all=True,
|
||||
).union(
|
||||
queryset.filter(
|
||||
# Match on lower pattern
|
||||
Q(note__alias__normalized_name__iregex="^" + pattern.lower())
|
||||
& ~Q(note__alias__normalized_name__iregex="^" + Alias.normalize(pattern))
|
||||
& ~Q(note__alias__name__iregex="^" + pattern)
|
||||
Q(**{f"note__alias__normalized_name{suffix}": prefix + pattern.lower()})
|
||||
& ~Q(**{f"note__alias__normalized_name{suffix}": prefix + Alias.normalize(pattern)})
|
||||
& ~Q(**{f"note__alias__name{suffix}": prefix + pattern})
|
||||
),
|
||||
all=True,
|
||||
).union(
|
||||
queryset.filter(
|
||||
# Match on firstname or lastname
|
||||
(Q(last_name__iregex="^" + pattern) | Q(first_name__iregex="^" + pattern))
|
||||
& ~Q(note__alias__normalized_name__iregex="^" + pattern.lower())
|
||||
& ~Q(note__alias__normalized_name__iregex="^" + Alias.normalize(pattern))
|
||||
& ~Q(note__alias__name__iregex="^" + pattern)
|
||||
(Q(**{f"last_name{suffix}": prefix + pattern}) | Q(**{f"first_name{suffix}": prefix + pattern}))
|
||||
& ~Q(**{f"note__alias__normalized_name{suffix}": prefix + pattern.lower()})
|
||||
& ~Q(**{f"note__alias__normalized_name{suffix}": prefix + Alias.normalize(pattern)})
|
||||
& ~Q(**{f"note__alias__name{suffix}": prefix + pattern})
|
||||
),
|
||||
all=True,
|
||||
)
|
||||
@ -107,6 +121,6 @@ class ContentTypeViewSet(ReadOnlyModelViewSet):
|
||||
"""
|
||||
queryset = ContentType.objects.order_by('id')
|
||||
serializer_class = ContentTypeSerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||
filterset_fields = ['id', 'app_label', 'model', ]
|
||||
search_fields = ['$app_label', '$model', ]
|
||||
|
@ -2,7 +2,8 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework.filters import OrderingFilter, SearchFilter
|
||||
from rest_framework.filters import OrderingFilter
|
||||
from api.filters import RegexSafeSearchFilter
|
||||
from api.viewsets import ReadProtectedModelViewSet
|
||||
|
||||
from .serializers import ProfileSerializer, ClubSerializer, MembershipSerializer
|
||||
@ -17,7 +18,7 @@ class ProfileViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
queryset = Profile.objects.order_by('id')
|
||||
serializer_class = ProfileSerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||
filterset_fields = ['user', 'user__first_name', 'user__last_name', 'user__username', 'user__email',
|
||||
'user__note__alias__name', 'user__note__alias__normalized_name', 'phone_number', "section",
|
||||
'department', 'promotion', 'address', 'paid', 'ml_events_registration', 'ml_sport_registration',
|
||||
@ -34,7 +35,7 @@ class ClubViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
queryset = Club.objects.order_by('id')
|
||||
serializer_class = ClubSerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||
filterset_fields = ['name', 'email', 'note__alias__name', 'note__alias__normalized_name', 'parent_club',
|
||||
'parent_club__name', 'require_memberships', 'membership_fee_paid', 'membership_fee_unpaid',
|
||||
'membership_duration', 'membership_start', 'membership_end', ]
|
||||
@ -49,7 +50,7 @@ class MembershipViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
queryset = Membership.objects.order_by('id')
|
||||
serializer_class = MembershipSerializer
|
||||
filter_backends = [DjangoFilterBackend, OrderingFilter, SearchFilter]
|
||||
filter_backends = [DjangoFilterBackend, OrderingFilter, RegexSafeSearchFilter]
|
||||
filterset_fields = ['club__name', 'club__email', 'club__note__alias__name', 'club__note__alias__normalized_name',
|
||||
'user__username', 'user__last_name', 'user__first_name', 'user__email',
|
||||
'user__note__alias__name', 'user__note__alias__normalized_name',
|
||||
|
@ -18,6 +18,7 @@ from django.views.generic import DetailView, UpdateView, TemplateView
|
||||
from django.views.generic.edit import FormMixin
|
||||
from django_tables2.views import SingleTableView
|
||||
from rest_framework.authtoken.models import Token
|
||||
from api.viewsets import is_regex
|
||||
from note.models import Alias, NoteClub, NoteUser, Trust
|
||||
from note.models.transactions import Transaction, SpecialTransaction
|
||||
from note.tables import HistoryTable, AliasTable, TrustTable, TrustedTable
|
||||
@ -219,16 +220,20 @@ class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
||||
if "search" in self.request.GET and self.request.GET["search"]:
|
||||
pattern = self.request.GET["search"]
|
||||
|
||||
# Check if this is a valid regex. If not, we won't check regex
|
||||
valid_regex = is_regex(pattern)
|
||||
suffix = "__iregex" if valid_regex else "__istartswith"
|
||||
prefix = "^" if valid_regex else ""
|
||||
qs = qs.filter(
|
||||
username__iregex="^" + pattern
|
||||
Q(**{f"username{suffix}": prefix + pattern})
|
||||
).union(
|
||||
qs.filter(
|
||||
(Q(alias__iregex="^" + pattern)
|
||||
| Q(normalized_alias__iregex="^" + Alias.normalize(pattern))
|
||||
| Q(last_name__iregex="^" + pattern)
|
||||
| Q(first_name__iregex="^" + pattern)
|
||||
(Q(**{f"alias{suffix}": prefix + pattern})
|
||||
| Q(**{f"normalized_alias{suffix}": prefix + Alias.normalize(pattern)})
|
||||
| Q(**{f"last_name{suffix}": prefix + pattern})
|
||||
| Q(**{f"first_name{suffix}": prefix + pattern})
|
||||
| Q(email__istartswith=pattern))
|
||||
& ~Q(username__iregex="^" + pattern)
|
||||
& ~Q(**{f"username{suffix}": prefix + pattern})
|
||||
), all=True)
|
||||
else:
|
||||
qs = qs.none()
|
||||
@ -410,10 +415,15 @@ class ClubListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
||||
if "search" in self.request.GET:
|
||||
pattern = self.request.GET["search"]
|
||||
|
||||
# Check if this is a valid regex. If not, we won't check regex
|
||||
valid_regex = is_regex(pattern)
|
||||
suffix = "__iregex" if valid_regex else "__istartswith"
|
||||
prefix = "^" if valid_regex else ""
|
||||
|
||||
qs = qs.filter(
|
||||
Q(name__iregex=pattern)
|
||||
| Q(note__alias__name__iregex=pattern)
|
||||
| Q(note__alias__normalized_name__iregex=Alias.normalize(pattern))
|
||||
Q(**{f"name{suffix}": prefix + pattern})
|
||||
| Q(**{f"note__alias__name{suffix}": prefix + pattern})
|
||||
| Q(**{f"note__alias__normalized_name{suffix}": prefix + Alias.normalize(pattern)})
|
||||
)
|
||||
|
||||
return qs
|
||||
@ -912,10 +922,15 @@ class ClubMembersListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableV
|
||||
|
||||
if 'search' in self.request.GET:
|
||||
pattern = self.request.GET['search']
|
||||
|
||||
# Check if this is a valid regex. If not, we won't check regex
|
||||
valid_regex = is_regex(pattern)
|
||||
suffix = "__iregex" if valid_regex else "__istartswith"
|
||||
prefix = "^" if valid_regex else ""
|
||||
qs = qs.filter(
|
||||
Q(user__first_name__iregex='^' + pattern)
|
||||
| Q(user__last_name__iregex='^' + pattern)
|
||||
| Q(user__note__alias__normalized_name__iregex='^' + Alias.normalize(pattern))
|
||||
Q(**{f"user__first_name{suffix}": prefix + pattern})
|
||||
| Q(**{f"user__last_name{suffix}": prefix + pattern})
|
||||
| Q(**{f"user__note__alias__normalized_name{suffix}": prefix + Alias.normalize(pattern)})
|
||||
)
|
||||
|
||||
only_active = "only_active" not in self.request.GET or self.request.GET["only_active"] != '0'
|
||||
|
@ -1,16 +1,16 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
import re
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.models import Q
|
||||
from django.core.exceptions import ValidationError
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework.filters import OrderingFilter, SearchFilter
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.filters import OrderingFilter
|
||||
from rest_framework import status, viewsets
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from api.viewsets import ReadProtectedModelViewSet, ReadOnlyProtectedModelViewSet
|
||||
from api.filters import RegexSafeSearchFilter
|
||||
from api.viewsets import ReadProtectedModelViewSet, ReadOnlyProtectedModelViewSet, \
|
||||
is_regex
|
||||
from permission.backends import PermissionBackend
|
||||
|
||||
from .serializers import NotePolymorphicSerializer, AliasSerializer, ConsumerSerializer, \
|
||||
@ -29,7 +29,7 @@ class NotePolymorphicViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
queryset = Note.objects.order_by('id')
|
||||
serializer_class = NotePolymorphicSerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
||||
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter, OrderingFilter]
|
||||
filterset_fields = ['alias__name', 'polymorphic_ctype', 'is_active', 'balance', 'last_negative', 'created_at', ]
|
||||
search_fields = ['$alias__normalized_name', '$alias__name', '$polymorphic_ctype__model',
|
||||
'$noteuser__user__last_name', '$noteuser__user__first_name', '$noteuser__user__email',
|
||||
@ -48,10 +48,14 @@ class NotePolymorphicViewSet(ReadProtectedModelViewSet):
|
||||
.distinct()
|
||||
|
||||
alias = self.request.query_params.get("alias", ".*")
|
||||
# Check if this is a valid regex. If not, we won't check regex
|
||||
valid_regex = is_regex(alias)
|
||||
suffix = '__iregex' if valid_regex else '__istartswith'
|
||||
alias_prefix = '^' if valid_regex else ''
|
||||
queryset = queryset.filter(
|
||||
Q(alias__name__iregex="^" + alias)
|
||||
| Q(alias__normalized_name__iregex="^" + Alias.normalize(alias))
|
||||
| Q(alias__normalized_name__iregex="^" + alias.lower())
|
||||
Q(**{f"alias__name{suffix}": alias_prefix + alias})
|
||||
| Q(**{f"alias__normalized_name{suffix}": alias_prefix + Alias.normalize(alias)})
|
||||
| Q(**{f"alias__normalized_name{suffix}": alias_prefix + alias.lower()})
|
||||
)
|
||||
|
||||
return queryset.order_by("id")
|
||||
@ -65,7 +69,7 @@ class TrustViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
queryset = Trust.objects
|
||||
serializer_class = TrustSerializer
|
||||
filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
|
||||
filter_backends = [RegexSafeSearchFilter, DjangoFilterBackend, OrderingFilter]
|
||||
search_fields = ['$trusting__alias__name', '$trusting__alias__normalized_name',
|
||||
'$trusted__alias__name', '$trusted__alias__normalized_name']
|
||||
filterset_fields = ['trusting', 'trusting__noteuser__user', 'trusted', 'trusted__noteuser__user']
|
||||
@ -91,11 +95,11 @@ class AliasViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `Alias` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/note/aliases/
|
||||
then render it on /api/note/alias/
|
||||
"""
|
||||
queryset = Alias.objects
|
||||
serializer_class = AliasSerializer
|
||||
filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
|
||||
filter_backends = [RegexSafeSearchFilter, DjangoFilterBackend, OrderingFilter]
|
||||
search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ]
|
||||
filterset_fields = ['name', 'normalized_name', 'note', 'note__noteuser__user',
|
||||
'note__noteclub__club', 'note__polymorphic_ctype__model', ]
|
||||
@ -126,18 +130,22 @@ class AliasViewSet(ReadProtectedModelViewSet):
|
||||
|
||||
alias = self.request.query_params.get("alias", None)
|
||||
if alias:
|
||||
# Check if this is a valid regex. If not, we won't check regex
|
||||
valid_regex = is_regex(alias)
|
||||
suffix = '__iregex' if valid_regex else '__istartswith'
|
||||
alias_prefix = '^' if valid_regex else ''
|
||||
queryset = queryset.filter(
|
||||
name__iregex="^" + alias
|
||||
**{f"name{suffix}": alias_prefix + alias}
|
||||
).union(
|
||||
queryset.filter(
|
||||
Q(normalized_name__iregex="^" + Alias.normalize(alias))
|
||||
& ~Q(name__iregex="^" + alias)
|
||||
Q(**{f"normalized_name{suffix}": alias_prefix + Alias.normalize(alias)})
|
||||
& ~Q(**{f"name{suffix}": alias_prefix + alias})
|
||||
),
|
||||
all=True).union(
|
||||
queryset.filter(
|
||||
Q(normalized_name__iregex="^" + alias.lower())
|
||||
& ~Q(normalized_name__iregex="^" + Alias.normalize(alias))
|
||||
& ~Q(name__iregex="^" + alias)
|
||||
Q(**{f"normalized_name{suffix}": "^" + alias.lower()})
|
||||
& ~Q(**{f"normalized_name{suffix}": "^" + Alias.normalize(alias)})
|
||||
& ~Q(**{f"name{suffix}": "^" + alias})
|
||||
),
|
||||
all=True)
|
||||
|
||||
@ -147,7 +155,7 @@ class AliasViewSet(ReadProtectedModelViewSet):
|
||||
class ConsumerViewSet(ReadOnlyProtectedModelViewSet):
|
||||
queryset = Alias.objects
|
||||
serializer_class = ConsumerSerializer
|
||||
filter_backends = [SearchFilter, OrderingFilter, DjangoFilterBackend]
|
||||
filter_backends = [RegexSafeSearchFilter, OrderingFilter, DjangoFilterBackend]
|
||||
search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ]
|
||||
filterset_fields = ['name', 'normalized_name', 'note', 'note__noteuser__user',
|
||||
'note__noteclub__club', 'note__polymorphic_ctype__model', ]
|
||||
@ -166,11 +174,7 @@ class ConsumerViewSet(ReadOnlyProtectedModelViewSet):
|
||||
|
||||
alias = self.request.query_params.get("alias", None)
|
||||
# Check if this is a valid regex. If not, we won't check regex
|
||||
try:
|
||||
re.compile(alias)
|
||||
valid_regex = True
|
||||
except (re.error, TypeError):
|
||||
valid_regex = False
|
||||
valid_regex = is_regex(alias)
|
||||
suffix = '__iregex' if valid_regex else '__istartswith'
|
||||
alias_prefix = '^' if valid_regex else ''
|
||||
queryset = queryset.prefetch_related('note')
|
||||
@ -207,7 +211,7 @@ class TemplateCategoryViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
queryset = TemplateCategory.objects.order_by('name')
|
||||
serializer_class = TemplateCategorySerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||
filterset_fields = ['name', 'templates', 'templates__name']
|
||||
search_fields = ['$name', '$templates__name', ]
|
||||
|
||||
@ -220,7 +224,7 @@ class TransactionTemplateViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
queryset = TransactionTemplate.objects.order_by('name')
|
||||
serializer_class = TransactionTemplateSerializer
|
||||
filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
|
||||
filter_backends = [RegexSafeSearchFilter, DjangoFilterBackend, OrderingFilter]
|
||||
filterset_fields = ['name', 'amount', 'display', 'category', 'category__name', ]
|
||||
search_fields = ['$name', '$category__name', ]
|
||||
ordering_fields = ['amount', ]
|
||||
@ -234,7 +238,7 @@ class TransactionViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
queryset = Transaction.objects.order_by('-created_at')
|
||||
serializer_class = TransactionPolymorphicSerializer
|
||||
filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
|
||||
filter_backends = [RegexSafeSearchFilter, DjangoFilterBackend, OrderingFilter]
|
||||
filterset_fields = ['source', 'source_alias', 'source__alias__name', 'source__alias__normalized_name',
|
||||
'destination', 'destination_alias', 'destination__alias__name',
|
||||
'destination__alias__normalized_name', 'quantity', 'polymorphic_ctype', 'amount',
|
||||
|
@ -13,6 +13,7 @@ from django.views.generic import CreateView, UpdateView, DetailView
|
||||
from django.urls import reverse_lazy
|
||||
from django_tables2 import SingleTableView
|
||||
from activity.models import Entry
|
||||
from api.viewsets import is_regex
|
||||
from permission.backends import PermissionBackend
|
||||
from permission.views import ProtectQuerysetMixin
|
||||
from note_kfet.inputs import AmountInput
|
||||
@ -89,11 +90,15 @@ class TransactionTemplateListView(ProtectQuerysetMixin, LoginRequiredMixin, Sing
|
||||
qs = super().get_queryset().distinct()
|
||||
if "search" in self.request.GET:
|
||||
pattern = self.request.GET["search"]
|
||||
|
||||
# Check if this is a valid regex. If not, we won't check regex
|
||||
valid_regex = is_regex(pattern)
|
||||
suffix = "__iregex" if valid_regex else "__icontains"
|
||||
qs = qs.filter(
|
||||
Q(name__iregex=pattern)
|
||||
| Q(destination__club__name__iregex=pattern)
|
||||
| Q(category__name__iregex=pattern)
|
||||
| Q(description__iregex=pattern)
|
||||
Q(**{f"name{suffix}": pattern})
|
||||
| Q(**{f"destination__club__name{suffix}": pattern})
|
||||
| Q(**{f"category__name{suffix}": pattern})
|
||||
| Q(**{f"description{suffix}": pattern})
|
||||
)
|
||||
|
||||
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"]:
|
||||
transactions = transactions.filter(polymorphic_ctype__in=data["type"])
|
||||
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"]:
|
||||
transactions = transactions.filter(valid=data["valid"])
|
||||
if "amount_gte" in data and data["amount_gte"]:
|
||||
|
@ -1,9 +1,9 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from api.viewsets import ReadOnlyProtectedModelViewSet
|
||||
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 ..models import Permission, Role
|
||||
@ -17,9 +17,9 @@ class PermissionViewSet(ReadOnlyProtectedModelViewSet):
|
||||
"""
|
||||
queryset = Permission.objects.order_by('id')
|
||||
serializer_class = PermissionSerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||
filterset_fields = ['model', 'type', 'query', 'mask', 'field', 'permanent', ]
|
||||
search_fields = ['$model__name', '$query', '$description', ]
|
||||
search_fields = ['$model__model', '$query', '$description', ]
|
||||
|
||||
|
||||
class RoleViewSet(ReadOnlyProtectedModelViewSet):
|
||||
@ -30,6 +30,6 @@ class RoleViewSet(ReadOnlyProtectedModelViewSet):
|
||||
"""
|
||||
queryset = Role.objects.order_by('id')
|
||||
serializer_class = RoleSerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||
filterset_fields = ['name', 'permissions', 'for_club', 'memberships__user', ]
|
||||
search_fields = ['$name', '$for_club__name', ]
|
||||
|
@ -16,6 +16,7 @@ from django.views import View
|
||||
from django.views.generic import CreateView, TemplateView, DetailView
|
||||
from django.views.generic.edit import FormMixin
|
||||
from django_tables2 import SingleTableView
|
||||
from api.viewsets import is_regex
|
||||
from member.forms import ProfileForm
|
||||
from member.models import Membership, Club
|
||||
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"]:
|
||||
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(
|
||||
Q(first_name__iregex=pattern)
|
||||
| Q(last_name__iregex=pattern)
|
||||
| Q(profile__section__iregex=pattern)
|
||||
| Q(username__iregex="^" + pattern)
|
||||
Q(**{f"first_name{suffix}": pattern})
|
||||
| Q(**{f"last_name{suffix}": pattern})
|
||||
| Q(**{f"profile__section{suffix}": pattern})
|
||||
| Q(**{f"username{suffix_username}": prefix + pattern})
|
||||
)
|
||||
|
||||
return qs
|
||||
|
@ -2,7 +2,7 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
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 InvoiceSerializer, ProductSerializer, RemittanceTypeSerializer, RemittanceSerializer, \
|
||||
@ -18,7 +18,7 @@ class InvoiceViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
queryset = Invoice.objects.order_by('id')
|
||||
serializer_class = InvoiceSerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||
filterset_fields = ['bde', 'object', 'description', 'name', 'address', 'date', 'acquitted', 'locked', ]
|
||||
search_fields = ['$object', '$description', '$name', '$address', ]
|
||||
|
||||
@ -31,7 +31,7 @@ class ProductViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
queryset = Product.objects.order_by('invoice_id', 'id')
|
||||
serializer_class = ProductSerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||
filterset_fields = ['invoice', 'designation', 'quantity', 'amount', ]
|
||||
search_fields = ['$designation', '$invoice__object', ]
|
||||
|
||||
@ -44,7 +44,7 @@ class RemittanceTypeViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
queryset = RemittanceType.objects.order_by('id')
|
||||
serializer_class = RemittanceTypeSerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||
filterset_fields = ['note', ]
|
||||
search_fields = ['$note__special_type', ]
|
||||
|
||||
@ -57,7 +57,7 @@ class RemittanceViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
queryset = Remittance.objects.order_by('id')
|
||||
serializer_class = RemittanceSerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||
filterset_fields = ['date', 'remittance_type', 'comment', 'closed', 'transaction_proxies__transaction', ]
|
||||
search_fields = ['$remittance_type__note__special_type', '$comment', ]
|
||||
|
||||
@ -70,7 +70,7 @@ class SogeCreditViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
queryset = SogeCredit.objects.order_by('id')
|
||||
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',
|
||||
'user__note__alias__normalized_name', 'transactions', 'credit_transaction', ]
|
||||
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.edit import BaseFormView, DeleteView
|
||||
from django_tables2 import SingleTableView
|
||||
from api.viewsets import is_regex
|
||||
from note.models import SpecialTransaction, NoteSpecial, Alias
|
||||
from note_kfet.settings.base import BASE_DIR
|
||||
from permission.backends import PermissionBackend
|
||||
@ -411,11 +412,16 @@ class SogeCreditListView(LoginRequiredMixin, ProtectQuerysetMixin, SingleTableVi
|
||||
if "search" in self.request.GET:
|
||||
pattern = self.request.GET["search"]
|
||||
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(
|
||||
Q(user__first_name__iregex=pattern)
|
||||
| Q(user__last_name__iregex=pattern)
|
||||
| Q(user__note__alias__name__iregex="^" + pattern)
|
||||
| Q(user__note__alias__normalized_name__iregex="^" + Alias.normalize(pattern))
|
||||
Q(**{f"user__first_name{suffix}": pattern})
|
||||
| Q(**{f"user__last_name{suffix}": pattern})
|
||||
| Q(**{f"user__note__alias__name{suffix_alias}": prefix + 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"]:
|
||||
|
@ -2,7 +2,8 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework.filters import OrderingFilter, SearchFilter
|
||||
from rest_framework.filters import OrderingFilter
|
||||
from api.filters import RegexSafeSearchFilter
|
||||
from api.viewsets import ReadProtectedModelViewSet
|
||||
|
||||
from .serializers import WEIClubSerializer, BusSerializer, BusTeamSerializer, WEIRoleSerializer, \
|
||||
@ -18,7 +19,7 @@ class WEIClubViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
queryset = WEIClub.objects.order_by('id')
|
||||
serializer_class = WEIClubSerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||
filterset_fields = ['name', 'year', 'date_start', 'date_end', 'email', 'note__alias__name',
|
||||
'note__alias__normalized_name', 'parent_club', 'parent_club__name', 'require_memberships',
|
||||
'membership_fee_paid', 'membership_fee_unpaid', 'membership_duration', 'membership_start',
|
||||
@ -34,7 +35,7 @@ class BusViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
queryset = Bus.objects.order_by('id')
|
||||
serializer_class = BusSerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||
filterset_fields = ['name', 'wei', 'description', ]
|
||||
search_fields = ['$name', '$wei__name', '$description', ]
|
||||
|
||||
@ -47,7 +48,7 @@ class BusTeamViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
queryset = BusTeam.objects.order_by('id')
|
||||
serializer_class = BusTeamSerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||
filterset_fields = ['name', 'bus', 'color', 'description', 'bus__wei', ]
|
||||
search_fields = ['$name', '$bus__name', '$bus__wei__name', '$description', ]
|
||||
|
||||
@ -60,7 +61,7 @@ class WEIRoleViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
queryset = WEIRole.objects.order_by('id')
|
||||
serializer_class = WEIRoleSerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||
filterset_fields = ['name', 'permissions', 'memberships', ]
|
||||
search_fields = ['$name', ]
|
||||
|
||||
@ -73,7 +74,7 @@ class WEIRegistrationViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
queryset = WEIRegistration.objects.order_by('id')
|
||||
serializer_class = WEIRegistrationSerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||
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',
|
||||
'wei__email', 'wei__year', 'soge_credit', 'caution_check', 'birth_date', 'gender',
|
||||
@ -92,7 +93,7 @@ class WEIMembershipViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
queryset = WEIMembership.objects.order_by('id')
|
||||
serializer_class = WEIMembershipSerializer
|
||||
filter_backends = [DjangoFilterBackend, OrderingFilter, SearchFilter]
|
||||
filter_backends = [DjangoFilterBackend, OrderingFilter, RegexSafeSearchFilter]
|
||||
filterset_fields = ['club__name', 'club__email', 'club__note__alias__name',
|
||||
'club__note__alias__normalized_name', 'user__username', 'user__last_name',
|
||||
'user__first_name', 'user__email', 'user__note__alias__name',
|
||||
|
@ -23,6 +23,7 @@ from django.views.generic import DetailView, UpdateView, RedirectView, TemplateV
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic.edit import BaseFormView, DeleteView
|
||||
from django_tables2 import SingleTableView
|
||||
from api.viewsets import is_regex
|
||||
from member.models import Membership, Club
|
||||
from note.models import Transaction, NoteClub, Alias, SpecialTransaction, NoteSpecial
|
||||
from note.tables import HistoryTable
|
||||
@ -219,13 +220,18 @@ class WEIMembershipsView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableVi
|
||||
if not pattern:
|
||||
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(
|
||||
Q(user__first_name__iregex=pattern)
|
||||
| Q(user__last_name__iregex=pattern)
|
||||
| Q(user__note__alias__name__iregex="^" + pattern)
|
||||
| Q(user__note__alias__normalized_name__iregex="^" + Alias.normalize(pattern))
|
||||
| Q(bus__name__iregex=pattern)
|
||||
| Q(team__name__iregex=pattern)
|
||||
Q(**{f"user__first_name{suffix}": pattern})
|
||||
| Q(**{f"user__last_name{suffix}": pattern})
|
||||
| Q(**{f"user__note__alias__name{suffix_alias}": prefix + pattern})
|
||||
| Q(**{f"user__note__alias__normalized_name{suffix_alias}": prefix + Alias.normalize(pattern)})
|
||||
| Q(**{f"bus__name{suffix}": pattern})
|
||||
| Q(**{f"team__name{suffix}": pattern})
|
||||
)
|
||||
|
||||
return qs
|
||||
@ -255,11 +261,16 @@ class WEIRegistrationsView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTable
|
||||
pattern = self.request.GET.get("search", "")
|
||||
|
||||
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(
|
||||
Q(user__first_name__iregex=pattern)
|
||||
| Q(user__last_name__iregex=pattern)
|
||||
| Q(user__note__alias__name__iregex="^" + pattern)
|
||||
| Q(user__note__alias__normalized_name__iregex="^" + Alias.normalize(pattern))
|
||||
Q(**{f"user__first_name{suffix}": pattern})
|
||||
| Q(**{f"user__last_name{suffix}": pattern})
|
||||
| Q(**{f"user__note__alias__name{suffix_alias}": prefix + pattern})
|
||||
| Q(**{f"user__note__alias__normalized_name{suffix_alias}": prefix + Alias.normalize(pattern)})
|
||||
)
|
||||
|
||||
return qs
|
||||
|
Loading…
Reference in New Issue
Block a user