1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2025-07-07 07:54:02 +02:00

Compare commits

..

1 Commits

Author SHA1 Message Date
3a8445e98b Translation 2024-08-17 02:43:56 +02:00
253 changed files with 10182 additions and 11182 deletions

View File

@ -7,10 +7,25 @@ stages:
variables: variables:
GIT_SUBMODULE_STRATEGY: recursive GIT_SUBMODULE_STRATEGY: recursive
# Ubuntu 22.04 # Debian Buster
py310-django42: # py37-django22:
# stage: test
# image: debian:buster-backports
# before_script:
# - >
# apt-get update &&
# apt-get install --no-install-recommends -t buster-backports -y
# python3-django python3-django-crispy-forms
# python3-django-extensions python3-django-filters python3-django-polymorphic
# python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil
# python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache
# python3-bs4 python3-setuptools tox texlive-xetex
# script: tox -e py37-django22
# Ubuntu 20.04
py38-django22:
stage: test stage: test
image: ubuntu:22.04 image: ubuntu:20.04
before_script: before_script:
# Fix tzdata prompt # Fix tzdata prompt
- ln -sf /usr/share/zoneinfo/Europe/Paris /etc/localtime && echo Europe/Paris > /etc/timezone - ln -sf /usr/share/zoneinfo/Europe/Paris /etc/localtime && echo Europe/Paris > /etc/timezone
@ -22,12 +37,12 @@ py310-django42:
python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil
python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache
python3-bs4 python3-setuptools tox texlive-xetex python3-bs4 python3-setuptools tox texlive-xetex
script: tox -e py310-django42 script: tox -e py38-django22
# Debian Bookworm # Debian Bullseye
py311-django42: py39-django22:
stage: test stage: test
image: debian:bookworm image: debian:bullseye
before_script: before_script:
- > - >
apt-get update && apt-get update &&
@ -37,11 +52,11 @@ py311-django42:
python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil
python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache
python3-bs4 python3-setuptools tox texlive-xetex python3-bs4 python3-setuptools tox texlive-xetex
script: tox -e py311-django42 script: tox -e py39-django22
linters: linters:
stage: quality-assurance stage: quality-assurance
image: debian:bookworm image: debian:bullseye
before_script: before_script:
- apt-get update && apt-get install -y tox - apt-get update && apt-get install -y tox
script: tox -e linters script: tox -e linters

2
.gitmodules vendored
View File

@ -1,3 +1,3 @@
[submodule "apps/scripts"] [submodule "apps/scripts"]
path = apps/scripts path = apps/scripts
url = https://gitlab.crans.org/bde/nk20-scripts url = https://gitlab.crans.org/bde/nk20-scripts.git

View File

@ -55,16 +55,10 @@ Bien que cela permette de créer une instance sur toutes les distributions,
(env)$ ./manage.py makemigrations (env)$ ./manage.py makemigrations
(env)$ ./manage.py migrate (env)$ ./manage.py migrate
(env)$ ./manage.py loaddata initial (env)$ ./manage.py loaddata initial
(env)$ ./manage.py createsuperuser # Création d'un⋅e utilisateur⋅rice initial (env)$ ./manage.py createsuperuser # Création d'un utilisateur initial
``` ```
6. (Optionnel) **Création d'une clé privée OpenID Connect** 6. Enjoy :
Pour activer le support d'OpenID Connect, il faut générer une clé privée, par
exemple avec openssl (`openssl genrsa -out oidc.key 4096`), et renseigner son
emplacement dans `OIDC_RSA_PRIVATE_KEY` (par défaut `/var/secrets/oidc.key`).
7. Enjoy :
```bash ```bash
(env)$ ./manage.py runserver 0.0.0.0:8000 (env)$ ./manage.py runserver 0.0.0.0:8000
@ -234,13 +228,7 @@ Sinon vous pouvez suivre les étapes décrites ci-dessous.
(env)$ ./manage.py check # pas de bêtise qui traine (env)$ ./manage.py check # pas de bêtise qui traine
(env)$ ./manage.py migrate (env)$ ./manage.py migrate
7. **Création d'une clé privée OpenID Connect** 7. *Enjoy \o/*
Pour activer le support d'OpenID Connect, il faut générer une clé privée, par
exemple avec openssl (`openssl genrsa -out oidc.key 4096`), et renseigner son
emplacement dans `OIDC_RSA_PRIVATE_KEY` (par défaut `/var/secrets/oidc.key`).
8. *Enjoy \o/*
### Installation avec Docker ### Installation avec Docker

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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
default_app_config = 'activity.apps.ActivityConfig' default_app_config = 'activity.apps.ActivityConfig'

View File

@ -1,11 +1,11 @@
# Copyright (C) 2018-2025 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.contrib import admin from django.contrib import admin
from note_kfet.admin import admin_site from note_kfet.admin import admin_site
from .forms import GuestForm from .forms import GuestForm
from .models import Activity, ActivityType, Entry, Guest, Opener from .models import Activity, ActivityType, Entry, Guest
@admin.register(Activity, site=admin_site) @admin.register(Activity, site=admin_site)
@ -35,7 +35,7 @@ class GuestAdmin(admin.ModelAdmin):
""" """
Admin customisation for Guest Admin customisation for Guest
""" """
list_display = ('last_name', 'first_name', 'school', 'activity', 'inviter') list_display = ('last_name', 'first_name', 'activity', 'inviter')
form = GuestForm form = GuestForm
@ -45,11 +45,3 @@ class EntryAdmin(admin.ModelAdmin):
Admin customisation for Entry Admin customisation for Entry
""" """
list_display = ('note', 'activity', 'time', 'guest') list_display = ('note', 'activity', 'time', 'guest')
@admin.register(Opener, site=admin_site)
class OpenerAdmin(admin.ModelAdmin):
"""
Admin customisation for Opener
"""
list_display = ('activity', 'opener')

View File

@ -1,11 +1,9 @@
# Copyright (C) 2018-2025 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.utils.translation import gettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from rest_framework.validators import UniqueTogetherValidator
from ..models import Activity, ActivityType, Entry, Guest, GuestTransaction, Opener from ..models import Activity, ActivityType, Entry, Guest, GuestTransaction
class ActivityTypeSerializer(serializers.ModelSerializer): class ActivityTypeSerializer(serializers.ModelSerializer):
@ -61,17 +59,3 @@ class GuestTransactionSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = GuestTransaction model = GuestTransaction
fields = '__all__' fields = '__all__'
class OpenerSerializer(serializers.ModelSerializer):
"""
REST API Serializer for Openers.
The djangorestframework plugin will analyse the model `Opener` and parse all fields in the API.
"""
class Meta:
model = Opener
fields = '__all__'
validators = [UniqueTogetherValidator(
queryset=Opener.objects.all(), fields=("opener", "activity"),
message=_("This opener already exists"))]

View File

@ -1,7 +1,7 @@
# Copyright (C) 2018-2025 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 .views import ActivityTypeViewSet, ActivityViewSet, EntryViewSet, GuestViewSet, OpenerViewSet from .views import ActivityTypeViewSet, ActivityViewSet, EntryViewSet, GuestViewSet
def register_activity_urls(router, path): def register_activity_urls(router, path):
@ -12,4 +12,3 @@ def register_activity_urls(router, path):
router.register(path + '/type', ActivityTypeViewSet) router.register(path + '/type', ActivityTypeViewSet)
router.register(path + '/guest', GuestViewSet) router.register(path + '/guest', GuestViewSet)
router.register(path + '/entry', EntryViewSet) router.register(path + '/entry', EntryViewSet)
router.register(path + '/opener', OpenerViewSet)

View File

@ -1,15 +1,12 @@
# Copyright (C) 2018-2025 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.filters import RegexSafeSearchFilter
from api.viewsets import ReadProtectedModelViewSet from api.viewsets import ReadProtectedModelViewSet
from django.core.exceptions import ValidationError
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.response import Response from rest_framework.filters import SearchFilter
from rest_framework import status
from .serializers import ActivitySerializer, ActivityTypeSerializer, EntrySerializer, GuestSerializer, OpenerSerializer from .serializers import ActivitySerializer, ActivityTypeSerializer, EntrySerializer, GuestSerializer
from ..models import Activity, ActivityType, Entry, Guest, Opener from ..models import Activity, ActivityType, Entry, Guest
class ActivityTypeViewSet(ReadProtectedModelViewSet): class ActivityTypeViewSet(ReadProtectedModelViewSet):
@ -32,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, RegexSafeSearchFilter] filter_backends = [DjangoFilterBackend, SearchFilter]
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',
@ -50,10 +47,10 @@ class GuestViewSet(ReadProtectedModelViewSet):
""" """
queryset = Guest.objects.order_by('id') queryset = Guest.objects.order_by('id')
serializer_class = GuestSerializer serializer_class = GuestSerializer
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter] filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['activity', 'activity__name', 'last_name', 'first_name', 'school', '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', '$school', '$inviter__user__email', '$inviter__alias__name', search_fields = ['$activity__name', '$last_name', '$first_name', '$inviter__user__email', '$inviter__alias__name',
'$inviter__alias__normalized_name', ] '$inviter__alias__normalized_name', ]
@ -65,36 +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, RegexSafeSearchFilter] filter_backends = [DjangoFilterBackend, SearchFilter]
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', ]
class OpenerViewSet(ReadProtectedModelViewSet):
"""
REST Opener View set.
The djangorestframework plugin will get all `Opener` objects, serialize it to JSON with the given serializer,
then render it on /api/activity/opener/
"""
queryset = Opener.objects
serializer_class = OpenerSerializer
filter_backends = [RegexSafeSearchFilter, DjangoFilterBackend]
search_fields = ['$opener__alias__name', '$opener__alias__normalized_name',
'$activity__name']
filterset_fields = ['opener', 'opener__noteuser__user', 'activity']
def get_serializer_class(self):
serializer_class = self.serializer_class
if self.request.method in ['PUT', 'PATCH']:
# opener-activity can't change
serializer_class.Meta.read_only_fields = ('opener', 'acitivity',)
return serializer_class
def destroy(self, request, *args, **kwargs):
instance = self.get_object()
try:
self.perform_destroy(instance)
except ValidationError as e:
return Response({e.code: str(e)}, status.HTTP_400_BAD_REQUEST)
return Response(status=status.HTTP_204_NO_CONTENT)

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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.apps import AppConfig from django.apps import AppConfig

View File

@ -1,17 +1,16 @@
# Copyright (C) 2018-2025 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 datetime import timedelta from datetime import timedelta
from random import shuffle from random import shuffle
from bootstrap_datepicker_plus.widgets import DateTimePickerInput
from django import forms from django import forms
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from member.models import Club from member.models import Club
from note.models import Note, NoteUser from note.models import Note, NoteUser
from note_kfet.inputs import Autocomplete from note_kfet.inputs import Autocomplete, DateTimePickerInput
from note_kfet.middlewares import get_current_request from note_kfet.middlewares import get_current_request
from permission.backends import PermissionBackend from permission.backends import PermissionBackend
@ -44,7 +43,7 @@ class ActivityForm(forms.ModelForm):
class Meta: class Meta:
model = Activity model = Activity
exclude = ('creater', 'valid', 'open', 'opener', ) exclude = ('creater', 'valid', 'open', )
widgets = { widgets = {
"organizer": Autocomplete( "organizer": Autocomplete(
model=Club, model=Club,
@ -107,7 +106,7 @@ class GuestForm(forms.ModelForm):
class Meta: class Meta:
model = Guest model = Guest
fields = ('last_name', 'first_name', 'school', 'inviter', ) fields = ('last_name', 'first_name', 'inviter', )
widgets = { widgets = {
"inviter": Autocomplete( "inviter": Autocomplete(
NoteUser, NoteUser,

View File

@ -1,28 +0,0 @@
# Generated by Django 2.2.28 on 2024-08-01 12:36
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('note', '0006_trust'),
('activity', '0003_auto_20240323_1422'),
]
operations = [
migrations.CreateModel(
name='Opener',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('activity', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='opener', to='activity.Activity', verbose_name='activity')),
('opener', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='activity_responsible', to='note.Note', verbose_name='opener')),
],
options={
'verbose_name': 'opener',
'verbose_name_plural': 'openers',
'unique_together': {('opener', 'activity')},
},
),
]

View File

@ -1,24 +0,0 @@
# Generated by Django 4.2.15 on 2024-08-28 08:00
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('note', '0006_trust'),
('activity', '0004_opener'),
]
operations = [
migrations.AlterModelOptions(
name='opener',
options={'verbose_name': 'Opener', 'verbose_name_plural': 'Openers'},
),
migrations.AlterField(
model_name='opener',
name='opener',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='activity_responsible', to='note.note', verbose_name='Opener'),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 4.2.20 on 2025-03-25 09:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("activity", "0005_alter_opener_options_alter_opener_opener"),
]
operations = [
migrations.AddField(
model_name="guest",
name="school",
field=models.CharField(default="", max_length=255, verbose_name="school"),
preserve_default=False,
),
]

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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 os import os
@ -11,7 +11,7 @@ from django.db import models, transaction
from django.db.models import Q from django.db.models import Q
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from note.models import NoteUser, Transaction, Note from note.models import NoteUser, Transaction
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
@ -201,8 +201,7 @@ class Entry(models.Model):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
qs = Entry.objects.filter(~Q(pk=self.pk), activity=self.activity, note=self.note, guest=self.guest) qs = Entry.objects.filter(~Q(pk=self.pk), activity=self.activity, note=self.note, guest=self.guest)
if qs.exists(): if qs.exists():
raise ValidationError(_("Already entered on ") raise ValidationError(_("Already entered on ") + _("{:%Y-%m-%d %H:%M:%S}").format(qs.get().time, ))
+ _("{:%Y-%m-%d %H:%M:%S}").format(timezone.localtime(qs.get().time), ))
if self.guest: if self.guest:
self.note = self.guest.inviter self.note = self.guest.inviter
@ -248,11 +247,6 @@ class Guest(models.Model):
verbose_name=_("first name"), verbose_name=_("first name"),
) )
school = models.CharField(
max_length=255,
verbose_name=_("school"),
)
inviter = models.ForeignKey( inviter = models.ForeignKey(
NoteUser, NoteUser,
on_delete=models.PROTECT, on_delete=models.PROTECT,
@ -316,31 +310,3 @@ class GuestTransaction(Transaction):
@property @property
def type(self): def type(self):
return _('Invitation') return _('Invitation')
class Opener(models.Model):
"""
Allow the user to make activity entries without more rights
"""
activity = models.ForeignKey(
Activity,
on_delete=models.CASCADE,
related_name='opener',
verbose_name=_('activity')
)
opener = models.ForeignKey(
Note,
on_delete=models.CASCADE,
related_name='activity_responsible',
verbose_name=_('Opener')
)
class Meta:
verbose_name = _("Opener")
verbose_name_plural = _("Openers")
unique_together = ("opener", "activity")
def __str__(self):
return _("{opener} is opener of activity {acivity}").format(
opener=str(self.opener), acivity=str(self.activity))

View File

@ -1,57 +0,0 @@
/**
* On form submit, add a new opener
*/
function form_create_opener (e) {
// Do not submit HTML form
e.preventDefault()
// Get data and send to API
const formData = new FormData(e.target)
$.getJSON('/api/note/alias/'+formData.get('opener') + '/',
function (opener_alias) {
create_opener(formData.get('activity'), opener_alias.note)
}).fail(function (xhr, _textStatus, _error) {
errMsg(xhr.responseJSON)
})
}
/**
* Add an opener between an activity and a user
* @param activity:Integer activity id
* @param opener:Integer user note id
*/
function create_opener(activity, opener) {
$.post('/api/activity/opener/', {
activity: activity,
opener: opener,
csrfmiddlewaretoken: CSRF_TOKEN
}).done(function () {
// Reload tables
$('#opener_table').load(location.pathname + ' #opener_table')
addMsg(gettext('Opener successfully added'), 'success')
}).fail(function (xhr, _textStatus, _error) {
errMsg(xhr.responseJSON)
})
}
/**
* On click of "delete", delete the opener
* @param button_id:Integer Opener id to remove
*/
function delete_button (button_id) {
$.ajax({
url: '/api/activity/opener/' + button_id + '/',
method: 'DELETE',
headers: { 'X-CSRFTOKEN': CSRF_TOKEN }
}).done(function () {
addMsg(gettext('Opener successfully deleted'), 'success')
$('#opener_table').load(location.pathname + ' #opener_table')
}).fail(function (xhr, _textStatus, _error) {
errMsg(xhr.responseJSON)
})
}
$(document).ready(function () {
// Attach event
document.getElementById('form_opener').addEventListener('submit', form_create_opener)
})

View File

@ -1,19 +1,15 @@
# Copyright (C) 2018-2025 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 datetime
from django.utils import timezone from django.utils import timezone
from django.utils.html import escape from django.utils.html import escape
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from note_kfet.middlewares import get_current_request
import django_tables2 as tables import django_tables2 as tables
from django_tables2 import A from django_tables2 import A
from permission.backends import PermissionBackend
from note.templatetags.pretty_money import pretty_money from note.templatetags.pretty_money import pretty_money
from .models import Activity, Entry, Guest, Opener from .models import Activity, Entry, Guest
class ActivityTable(tables.Table): class ActivityTable(tables.Table):
@ -53,11 +49,11 @@ class GuestTable(tables.Table):
} }
model = Guest model = Guest
template_name = 'django_tables2/bootstrap4.html' template_name = 'django_tables2/bootstrap4.html'
fields = ("last_name", "first_name", "inviter", "school") fields = ("last_name", "first_name", "inviter", )
def render_entry(self, record): def render_entry(self, record):
if record.has_entry: if record.has_entry:
return str(_("Entered on ") + str(_("{:%Y-%m-%d %H:%M:%S}").format(timezone.localtime(record.entry.time)))) return str(_("Entered on ") + str(_("{:%Y-%m-%d %H:%M:%S}").format(record.entry.time, )))
return mark_safe('<button id="{id}" class="btn btn-danger btn-sm" onclick="remove_guest(this.id)"> ' return mark_safe('<button id="{id}" class="btn btn-danger btn-sm" onclick="remove_guest(this.id)"> '
'{delete_trans}</button>'.format(id=record.id, delete_trans=_("remove").capitalize())) '{delete_trans}</button>'.format(id=record.id, delete_trans=_("remove").capitalize()))
@ -79,9 +75,6 @@ def get_row_class(record):
c += " table-info" c += " table-info"
elif record.note.balance < 0: elif record.note.balance < 0:
c += " table-danger" c += " table-danger"
# MODE VIEUXCON=ON
if (datetime.datetime.utcnow().timestamp() - record.note.created_at.timestamp()) > 3600 * 24 * 365 * 2.5:
c += " font-weight-bold underline"
return c return c
@ -120,34 +113,3 @@ class EntryTable(tables.Table):
'data-last-name': lambda record: record.last_name, 'data-last-name': lambda record: record.last_name,
'data-first-name': lambda record: record.first_name, 'data-first-name': lambda record: record.first_name,
} }
# function delete_button(id) provided in template file
DELETE_TEMPLATE = """
<button id="{{ record.pk }}" class="btn btn-danger btn-sm" onclick="delete_button(this.id)"> {{ delete_trans }}</button>
"""
class OpenerTable(tables.Table):
class Meta:
attrs = {
'class': 'table table condensed table-striped',
'id': "opener_table"
}
model = Opener
fields = ("opener",)
template_name = 'django_tables2/bootstrap4.html'
show_header = False
opener = tables.Column(attrs={'td': {'class': 'text-center'}})
delete_col = tables.TemplateColumn(
template_code=DELETE_TEMPLATE,
extra_context={"delete_trans": _('Delete')},
attrs={
'td': {
'class': lambda record: 'col-sm-1'
+ (' d-none' if not PermissionBackend.check_perm(
get_current_request(), "activity.delete_opener", record)
else '')}},
verbose_name=_("Delete"),)

View File

@ -4,31 +4,11 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %} {% endcomment %}
{% load i18n perms %} {% load i18n perms %}
{% load render_table from django_tables2 %} {% load render_table from django_tables2 %}
{% load static django_tables2 i18n %}
{% block content %} {% block content %}
<h1 class="text-white">{{ title }}</h1> <h1 class="text-white">{{ title }}</h1>
{% include "activity/includes/activity_info.html" %} {% include "activity/includes/activity_info.html" %}
{% if activity.activity_type.manage_entries and ".change__opener"|has_perm:activity %}
<div class="card bg-white mb-3">
<h3 class="card-header text-center">
{% trans "Openers" %}
</h3>
<div class="card-body">
<form class="input-group" method="POST" id="form_opener">
{% csrf_token %}
<input type="hidden" name="activity" value="{{ object.pk }}">
{%include "autocomplete_model.html" %}
<div class="input-group-append">
<input type="submit" class="btn btn-success" value="{% trans "Add" %}">
</div>
</form>
</div>
{% render_table opener %}
</div>
{% endif %}
{% if guests.data %} {% if guests.data %}
<div class="card bg-white mb-3"> <div class="card bg-white mb-3">
<h3 class="card-header text-center"> <h3 class="card-header text-center">
@ -42,8 +22,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% endblock %} {% endblock %}
{% block extrajavascript %} {% block extrajavascript %}
<script src="{% static "activity/js/opener.js" %}"></script>
<script src="{% static "js/autocomplete_model.js" %}"></script>
<script> <script>
function remove_guest(guest_id) { function remove_guest(guest_id) {
$.ajax({ $.ajax({

View File

@ -23,19 +23,19 @@ SPDX-License-Identifier: GPL-3.0-or-later
<script> <script>
var date_end = document.getElementById("id_date_end"); var date_end = document.getElementById("id_date_end");
var date_start = document.getElementById("id_date_start"); var date_start = document.getElementById("id_date_start");
function update_date_end (){ function update_date_end (){
if(date_end.value=="" || date_end.value<date_start.value){ if(date_end.value=="" || date_end.value<date_start.value){
date_end.value = date_start.value; date_end.value = date_start.value;
}; };
}; };
function update_date_start (){ function update_date_start (){
if(date_start.value=="" || date_end.value<date_start.value){ if(date_start.value=="" || date_end.value<date_start.value){
date_start.value = date_end.value; date_start.value = date_end.value;
}; };
}; };
date_start.addEventListener('focusout', update_date_end); date_start.addEventListener('focusout', update_date_end);
date_end.addEventListener('focusout', update_date_start); date_end.addEventListener('focusout', update_date_start);

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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 datetime import timedelta from datetime import timedelta
@ -50,7 +50,6 @@ class TestActivities(TestCase):
inviter=self.user.note, inviter=self.user.note,
last_name="GUEST", last_name="GUEST",
first_name="Guest", first_name="Guest",
school="School",
) )
def test_activity_list(self): def test_activity_list(self):
@ -157,7 +156,6 @@ class TestActivities(TestCase):
inviter=self.user.note.id, inviter=self.user.note.id,
last_name="GUEST2", last_name="GUEST2",
first_name="Guest", first_name="Guest",
school="School",
)) ))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@ -169,7 +167,6 @@ class TestActivities(TestCase):
inviter=self.user.note.id, inviter=self.user.note.id,
last_name="GUEST2", last_name="GUEST2",
first_name="Guest", first_name="Guest",
school="School",
)) ))
self.assertRedirects(response, reverse("activity:activity_detail", args=(self.activity.pk,)), 302, 200) self.assertRedirects(response, reverse("activity:activity_detail", args=(self.activity.pk,)), 302, 200)
@ -203,7 +200,6 @@ class TestActivityAPI(TestAPI):
inviter=self.user.note, inviter=self.user.note,
last_name="GUEST", last_name="GUEST",
first_name="Guest", first_name="Guest",
school="School",
) )
self.entry = Entry.objects.create( self.entry = Entry.objects.create(

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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.urls import path from django.urls import path

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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 hashlib import md5 from hashlib import md5
@ -18,15 +18,14 @@ 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.views.generic.list import ListView from django.views.generic.list import ListView
from django_tables2.views import MultiTableMixin, SingleTableMixin 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
from .forms import ActivityForm, GuestForm from .forms import ActivityForm, GuestForm
from .models import Activity, Entry, Guest, Opener from .models import Activity, Entry, Guest
from .tables import ActivityTable, EntryTable, GuestTable, OpenerTable from .tables import ActivityTable, EntryTable, GuestTable
class ActivityCreateView(ProtectQuerysetMixin, ProtectedCreateView): class ActivityCreateView(ProtectQuerysetMixin, ProtectedCreateView):
@ -64,15 +63,19 @@ class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin
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
tables = [ tables = [ActivityTable, ActivityTable]
lambda data: ActivityTable(data, prefix="all-"),
lambda data: ActivityTable(data, prefix="upcoming-"),
]
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): def get_tables_data(self):
# first table = all activities, second table = upcoming # first table = all activities, second table = upcoming
return [ return [
@ -96,7 +99,7 @@ class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin
return context return context
class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, DetailView): class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
""" """
Shows details about one activity. Add guest to context Shows details about one activity. Add guest to context
""" """
@ -104,40 +107,15 @@ class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMix
context_object_name = "activity" context_object_name = "activity"
extra_context = {"title": _("Activity detail")} extra_context = {"title": _("Activity detail")}
tables = [
lambda data: GuestTable(data, prefix="guests-"),
lambda data: OpenerTable(data, prefix="opener-"),
]
def get_tables_data(self):
return [
Guest.objects.filter(activity=self.object)
.filter(PermissionBackend.filter_queryset(self.request, Guest, "view")),
self.object.opener.filter(activity=self.object)
.filter(PermissionBackend.filter_queryset(self.request, Opener, "view")),
]
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data() context = super().get_context_data()
tables = context["tables"] table = GuestTable(data=Guest.objects.filter(activity=self.object)
for name, table in zip(["guests", "opener"], tables): .filter(PermissionBackend.filter_queryset(self.request, Guest, "view")))
context[name] = table context["guests"] = table
context["activity_started"] = timezone.now() > timezone.localtime(self.object.date_start) context["activity_started"] = timezone.now() > timezone.localtime(self.object.date_start)
context["widget"] = {
"name": "opener",
"resetable": True,
"attrs": {
"class": "autocomplete form-control",
"id": "opener",
"api_url": "/api/note/alias/?note__polymorphic_ctype__model=noteuser",
"name_field": "name",
"placeholder": ""
}
}
return context return context
@ -168,7 +146,6 @@ class ActivityInviteView(ProtectQuerysetMixin, ProtectedCreateView):
activity=activity, activity=activity,
first_name="", first_name="",
last_name="", last_name="",
school="",
inviter=self.request.user.note, inviter=self.request.user.note,
) )
@ -195,14 +172,12 @@ class ActivityInviteView(ProtectQuerysetMixin, ProtectedCreateView):
return reverse_lazy('activity:activity_detail', kwargs={"pk": self.kwargs["pk"]}) return reverse_lazy('activity:activity_detail', kwargs={"pk": self.kwargs["pk"]})
class ActivityEntryView(LoginRequiredMixin, SingleTableMixin, TemplateView): class ActivityEntryView(LoginRequiredMixin, TemplateView):
""" """
Manages entry to an activity Manages entry to an activity
""" """
template_name = "activity/activity_entry.html" template_name = "activity/activity_entry.html"
table_class = EntryTable
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
""" """
Don't display the entry interface if the user has no right to see it (no right to add an entry for itself), Don't display the entry interface if the user has no right to see it (no right to add an entry for itself),
@ -237,16 +212,13 @@ class ActivityEntryView(LoginRequiredMixin, SingleTableMixin, 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] != "^":
# Check if this is a valid regex. If not, we won't check regex pattern = "^" + pattern
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(**{f"first_name{suffix}": pattern}) Q(first_name__iregex=pattern)
| Q(**{f"last_name{suffix}": pattern}) | Q(last_name__iregex=pattern)
| Q(**{f"inviter__alias__name{suffix}": pattern}) | Q(inviter__alias__name__iregex=pattern)
| Q(**{f"inviter__alias__normalized_name{suffix}": Alias.normalize(pattern)}) | Q(inviter__alias__normalized_name__iregex=Alias.normalize(pattern))
) )
else: else:
guest_qs = guest_qs.none() guest_qs = guest_qs.none()
@ -264,37 +236,25 @@ class ActivityEntryView(LoginRequiredMixin, SingleTableMixin, TemplateView):
balance=F("note__balance")) balance=F("note__balance"))
# Keep only users that have a note # Keep only users that have a note
note_qs = note_qs.filter(note__noteuser__isnull=False).exclude(note__inactivity_reason='forced') note_qs = note_qs.filter(note__noteuser__isnull=False)
if activity.activity_type.name != "Pot Vieux": # Keep only members
# Keep only members note_qs = note_qs.filter(
note_qs = note_qs.filter( note__noteuser__user__memberships__club=activity.attendees_club,
note__noteuser__user__memberships__club=activity.attendees_club, note__noteuser__user__memberships__date_start__lte=timezone.now(),
note__noteuser__user__memberships__date_start__lte=timezone.now(), note__noteuser__user__memberships__date_end__gte=timezone.now(),
note__noteuser__user__memberships__date_end__gte=timezone.now(), )
)
# Keep only valid members
# note_qs = note_qs.filter(
# note__noteuser__user__memberships__club=activity.attendees_club,
# note__noteuser__user__memberships__date_start__lte=timezone.now(),
# note__noteuser__user__memberships__date_end__gte=timezone.now(),
# )
# Filter with permission backend # Filter with permission backend
note_qs = note_qs.filter(PermissionBackend.filter_queryset(self.request, Alias, "view")) note_qs = note_qs.filter(PermissionBackend.filter_queryset(self.request, Alias, "view"))
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(**{f"note__noteuser__user__first_name{suffix}": pattern}) Q(note__noteuser__user__first_name__iregex=pattern)
| Q(**{f"note__noteuser__user__last_name{suffix}": pattern}) | Q(note__noteuser__user__last_name__iregex=pattern)
| Q(**{f"name{suffix}": pattern}) | Q(name__iregex=pattern)
| Q(**{f"normalized_name{suffix}": Alias.normalize(pattern)}) | Q(normalized_name__iregex=Alias.normalize(pattern))
) )
else: else:
note_qs = note_qs.none() note_qs = note_qs.none()
@ -306,9 +266,15 @@ class ActivityEntryView(LoginRequiredMixin, SingleTableMixin, TemplateView):
if settings.DATABASES[note_qs.db]["ENGINE"] == 'django.db.backends.postgresql' else note_qs.distinct()[:20] if settings.DATABASES[note_qs.db]["ENGINE"] == 'django.db.backends.postgresql' else note_qs.distinct()[:20]
return note_qs return note_qs
def get_table_data(self): def get_context_data(self, **kwargs):
"""
Query the list of Guest and Note to the activity and add information to makes entry with JS.
"""
context = super().get_context_data(**kwargs)
activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request, Activity, "view"))\ activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request, Activity, "view"))\
.distinct().get(pk=self.kwargs["pk"]) .distinct().get(pk=self.kwargs["pk"])
context["activity"] = activity
matched = [] matched = []
@ -321,17 +287,8 @@ class ActivityEntryView(LoginRequiredMixin, SingleTableMixin, TemplateView):
note.activity = activity note.activity = activity
matched.append(note) matched.append(note)
return matched table = EntryTable(data=matched)
context["table"] = table
def get_context_data(self, **kwargs):
"""
Query the list of Guest and Note to the activity and add information to makes entry with JS.
"""
context = super().get_context_data(**kwargs)
activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request, Activity, "view"))\
.distinct().get(pk=self.kwargs["pk"])
context["activity"] = activity
context["entries"] = Entry.objects.filter(activity=activity) context["entries"] = Entry.objects.filter(activity=activity)
@ -339,7 +296,7 @@ class ActivityEntryView(LoginRequiredMixin, SingleTableMixin, TemplateView):
context["noteuser_ctype"] = ContentType.objects.get_for_model(NoteUser).pk context["noteuser_ctype"] = ContentType.objects.get_for_model(NoteUser).pk
context["notespecial_ctype"] = ContentType.objects.get_for_model(NoteSpecial).pk context["notespecial_ctype"] = ContentType.objects.get_for_model(NoteSpecial).pk
activities_open = Activity.objects.filter(open=True, activity_type__manage_entries=True).filter( activities_open = Activity.objects.filter(open=True).filter(
PermissionBackend.filter_queryset(self.request, Activity, "view")).distinct().all() PermissionBackend.filter_queryset(self.request, Activity, "view")).distinct().all()
context["activities_open"] = [a for a in activities_open context["activities_open"] = [a for a in activities_open
if PermissionBackend.check_perm(self.request, if PermissionBackend.check_perm(self.request,
@ -373,8 +330,8 @@ X-WR-CALNAME:Kfet Calendar
NAME:Kfet Calendar NAME:Kfet Calendar
CALSCALE:GREGORIAN CALSCALE:GREGORIAN
BEGIN:VTIMEZONE BEGIN:VTIMEZONE
TZID:Europe/Paris TZID:Europe/Berlin
X-LIC-LOCATION:Europe/Paris X-LIC-LOCATION:Europe/Berlin
BEGIN:DAYLIGHT BEGIN:DAYLIGHT
TZOFFSETFROM:+0100 TZOFFSETFROM:+0100
TZOFFSETTO:+0200 TZOFFSETTO:+0200
@ -396,10 +353,10 @@ END:VTIMEZONE
DTSTAMP:{"{:%Y%m%dT%H%M%S}".format(activity.date_start)}Z DTSTAMP:{"{:%Y%m%dT%H%M%S}".format(activity.date_start)}Z
UID:{md5((activity.name + "$" + str(activity.id) + str(activity.date_start)).encode("UTF-8")).hexdigest()} UID:{md5((activity.name + "$" + str(activity.id) + str(activity.date_start)).encode("UTF-8")).hexdigest()}
SUMMARY;CHARSET=UTF-8:{self.multilines(activity.name, 75, 22)} SUMMARY;CHARSET=UTF-8:{self.multilines(activity.name, 75, 22)}
DTSTART:{"{:%Y%m%dT%H%M%S}Z".format(activity.date_start)} DTSTART;TZID=Europe/Berlin:{"{:%Y%m%dT%H%M%S}".format(activity.date_start)}
DTEND:{"{:%Y%m%dT%H%M%S}Z".format(activity.date_end)} DTEND;TZID=Europe/Berlin:{"{:%Y%m%dT%H%M%S}".format(activity.date_end)}
LOCATION:{self.multilines(activity.location, 75, 9) if activity.location else "Kfet"} LOCATION:{self.multilines(activity.location, 75, 9) if activity.location else "Kfet"}
DESCRIPTION;CHARSET=UTF-8:""" + self.multilines(activity.description.replace("\n", "\\n"), 75, 26) + f""" DESCRIPTION;CHARSET=UTF-8:""" + self.multilines(activity.description.replace("\n", "\\n"), 75, 26) + """
-- {activity.organizer.name} -- {activity.organizer.name}
END:VEVENT END:VEVENT
""" """

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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
default_app_config = 'api.apps.APIConfig' default_app_config = 'api.apps.APIConfig'

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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.apps import AppConfig from django.apps import AppConfig

View File

@ -1,42 +0,0 @@
import re
from functools import lru_cache
from rest_framework.filters import SearchFilter
class RegexSafeSearchFilter(SearchFilter):
@lru_cache
def validate_regex(self, search_term) -> bool:
try:
re.compile(search_term)
return True
except re.error:
return False
def get_search_fields(self, view, request):
"""
Ensure that given regex are valid.
If not, we consider that the user is trying to search by substring.
"""
search_fields = super().get_search_fields(view, request)
search_terms = self.get_search_terms(request)
for search_term in search_terms:
if not self.validate_regex(search_term):
# Invalid regex. We assume we don't query by regex but by substring.
search_fields = [f.replace('$', '') for f in search_fields]
break
return search_fields
def get_search_terms(self, request):
"""
Ensure that search field is a valid regex query. If not, we remove extra characters.
"""
terms = super().get_search_terms(request)
if not all(self.validate_regex(term) for term in terms):
# Invalid regex. If a ^ is prefixed to the search term, we remove it.
terms = [term[1:] if term[0] == '^' else term for term in terms]
# Same for dollars.
terms = [term[:-1] if term[-1] == '$' else term for term in terms]
return terms

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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 json import json
@ -12,12 +12,11 @@ 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
@ -88,7 +87,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 RegexSafeSearchFilter in backends: if SearchFilter in backends:
# Basic search # Basic search
for field in viewset.search_fields: for field in viewset.search_fields:
obj = self.fix_note_object(obj, field) obj = self.fix_note_object(obj, field)

View File

@ -1,9 +1,8 @@
# Copyright (C) 2018-2025 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.conf import settings from django.conf import settings
from django.conf.urls import include from django.conf.urls import url, include
from django.urls import re_path
from rest_framework import routers from rest_framework import routers
from .views import UserInformationView from .views import UserInformationView
@ -47,16 +46,12 @@ if "wei" in settings.INSTALLED_APPS:
from wei.api.urls import register_wei_urls from wei.api.urls import register_wei_urls
register_wei_urls(router, 'wei') register_wei_urls(router, 'wei')
if "wrapped" in settings.INSTALLED_APPS:
from wrapped.api.urls import register_wrapped_urls
register_wrapped_urls(router, 'wrapped')
app_name = 'api' app_name = 'api'
# Wire up our API using automatic URL routing. # Wire up our API using automatic URL routing.
# Additionally, we include login URLs for the browsable API. # Additionally, we include login URLs for the browsable API.
urlpatterns = [ urlpatterns = [
re_path('^', include(router.urls)), url('^', include(router.urls)),
re_path('^me/', UserInformationView.as_view()), url('^me/', UserInformationView.as_view()),
re_path('^api-auth/', include('rest_framework.urls', namespace='rest_framework')), url('^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
] ]

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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.contrib.auth.models import User from django.contrib.auth.models import User

View File

@ -1,29 +1,19 @@
# Copyright (C) 2018-2025 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.
@ -70,38 +60,34 @@ 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
Q(**{f"note__alias__name{suffix}": prefix + pattern}) note__alias__name__iregex="^" + pattern
).union( ).union(
queryset.filter( queryset.filter(
# Match with normalization # Match with normalization
Q(**{f"note__alias__normalized_name{suffix}": prefix + Alias.normalize(pattern)}) Q(note__alias__normalized_name__iregex="^" + Alias.normalize(pattern))
& ~Q(**{f"note__alias__name{suffix}": prefix + pattern}) & ~Q(note__alias__name__iregex="^" + pattern)
), ),
all=True, all=True,
).union( ).union(
queryset.filter( queryset.filter(
# Match on lower pattern # Match on lower pattern
Q(**{f"note__alias__normalized_name{suffix}": prefix + pattern.lower()}) Q(note__alias__normalized_name__iregex="^" + pattern.lower())
& ~Q(**{f"note__alias__normalized_name{suffix}": prefix + Alias.normalize(pattern)}) & ~Q(note__alias__normalized_name__iregex="^" + Alias.normalize(pattern))
& ~Q(**{f"note__alias__name{suffix}": prefix + pattern}) & ~Q(note__alias__name__iregex="^" + pattern)
), ),
all=True, all=True,
).union( ).union(
queryset.filter( queryset.filter(
# Match on firstname or lastname # Match on firstname or lastname
(Q(**{f"last_name{suffix}": prefix + pattern}) | Q(**{f"first_name{suffix}": prefix + pattern})) (Q(last_name__iregex="^" + pattern) | Q(first_name__iregex="^" + pattern))
& ~Q(**{f"note__alias__normalized_name{suffix}": prefix + pattern.lower()}) & ~Q(note__alias__normalized_name__iregex="^" + pattern.lower())
& ~Q(**{f"note__alias__normalized_name{suffix}": prefix + Alias.normalize(pattern)}) & ~Q(note__alias__normalized_name__iregex="^" + Alias.normalize(pattern))
& ~Q(**{f"note__alias__name{suffix}": prefix + pattern}) & ~Q(note__alias__name__iregex="^" + pattern)
), ),
all=True, all=True,
) )
@ -121,6 +107,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, RegexSafeSearchFilter] filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['id', 'app_label', 'model', ] filterset_fields = ['id', 'app_label', 'model', ]
search_fields = ['$app_label', '$model', ] search_fields = ['$app_label', '$model', ]

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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.contrib import admin from django.contrib import admin

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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 rest_framework import serializers from rest_framework import serializers

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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 .views import AllergenViewSet, BasicFoodViewSet, QRCodeViewSet, TransformedFoodViewSet from .views import AllergenViewSet, BasicFoodViewSet, QRCodeViewSet, TransformedFoodViewSet

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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 api.viewsets import ReadProtectedModelViewSet

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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 random import shuffle from random import shuffle
@ -7,8 +7,7 @@ from django import forms
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.utils import timezone from django.utils import timezone
from member.models import Club from member.models import Club
from bootstrap_datepicker_plus.widgets import DateTimePickerInput from note_kfet.inputs import Autocomplete, DateTimePickerInput
from note_kfet.inputs import Autocomplete
from note_kfet.middlewares import get_current_request from note_kfet.middlewares import get_current_request
from permission.backends import PermissionBackend from permission.backends import PermissionBackend
@ -23,6 +22,7 @@ class AddIngredientForms(forms.ModelForm):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['ingredient'].queryset = self.fields['ingredient'].queryset.filter( self.fields['ingredient'].queryset = self.fields['ingredient'].queryset.filter(
polymorphic_ctype__model='transformedfood', polymorphic_ctype__model='transformedfood',
owner_id=self.instance.owner_id,
is_ready=False, is_ready=False,
is_active=True, is_active=True,
was_eaten=False, was_eaten=False,
@ -70,11 +70,7 @@ class QRCodeForms(forms.ModelForm):
""" """
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['food_container'].queryset = self.fields['food_container'].queryset.filter( self.fields['food_container'].queryset = self.fields['food_container'].queryset.filter(is_ready=False)
is_active=True,
was_eaten=False,
polymorphic_ctype__model='transformedfood',
)
class Meta: class Meta:
model = QRCode model = QRCode

View File

@ -1,20 +0,0 @@
# Generated by Django 4.2.15 on 2024-08-28 08:00
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('food', '0004_auto_20240813_2358'),
]
operations = [
migrations.AlterField(
model_name='food',
name='polymorphic_ctype',
field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype'),
),
]

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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 datetime import timedelta from datetime import timedelta

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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 django_tables2 as tables import django_tables2 as tables

View File

@ -2,7 +2,6 @@
{% comment %} {% comment %}
SPDX-License-Identifier: GPL-3.0-or-later SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %} {% endcomment %}
{% load render_table from django_tables2 %}
{% load i18n crispy_forms_tags %} {% load i18n crispy_forms_tags %}
{% block content %} {% block content %}
@ -19,37 +18,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
{{ form|crispy }} {{ form|crispy }}
<button class="btn btn-primary" type="submit">{% trans "Submit" %}</button> <button class="btn btn-primary" type="submit">{% trans "Submit" %}</button>
</form> </form>
<div class="card-body" id="profile_infos">
<h4>{% trans "Copy constructor" %}</h4>
<table class="table">
<thead>
<tr>
<th class="orderable">
{% trans "Name" %}
</th>
<th class="orderable">
{% trans "Owner" %}
</th>
<th class="orderable">
{% trans "Arrival date" %}
</th>
<th class="orderable">
{% trans "Expiry date" %}
</th>
</tr>
</thead>
<tbody>
{% for basic in last_basic %}
<tr>
<td><a href="{% url "food:qrcode_basic_create" slug=slug %}?copy={{ basic.pk }}">{{ basic.name }}</a></td>
<td>{{ basic.owner }}</td>
<td>{{ basic.arrival_date }}</td>
<td>{{ basic.expiry_date }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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.urls import path from django.urls import path

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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.db import transaction from django.db import transaction
@ -150,7 +150,6 @@ class QRCodeBasicFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView):
return reverse('food:qrcode_view', kwargs={"slug": self.kwargs['slug']}) return reverse('food:qrcode_view', kwargs={"slug": self.kwargs['slug']})
def get_sample_object(self): def get_sample_object(self):
# We choose a club which may work or BDE else # We choose a club which may work or BDE else
owner_id = 1 owner_id = 1
for membership in self.request.user.memberships.all(): for membership in self.request.user.memberships.all():
@ -173,14 +172,6 @@ class QRCodeBasicFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView):
form.fields['is_active'].widget = HiddenInput() form.fields['is_active'].widget = HiddenInput()
form.fields['was_eaten'].widget = HiddenInput() form.fields['was_eaten'].widget = HiddenInput()
copy = self.request.GET.get('copy', None)
if copy is not None:
basic = BasicFood.objects.get(pk=copy)
for field in ['date_type', 'expiry_date', 'name', 'owner']:
form.fields[field].initial = getattr(basic, field)
for field in ['allergens']:
form.fields[field].initial = getattr(basic, field).all()
return context return context
@ -197,15 +188,14 @@ class QRCodeCreateView(ProtectQuerysetMixin, ProtectedCreateView):
qrcode = kwargs["slug"] qrcode = kwargs["slug"]
if self.model.objects.filter(qr_code_number=qrcode).count() > 0: if self.model.objects.filter(qr_code_number=qrcode).count() > 0:
return HttpResponseRedirect(reverse("food:qrcode_view", kwargs=kwargs)) return HttpResponseRedirect(reverse("food:qrcode_view", kwargs=kwargs))
elif not TransformedFood.objects.filter(is_ready=False, was_eaten=False, is_active=True).count() > 0:
return HttpResponseRedirect(reverse("food:qrcode_basic_create", kwargs=kwargs))
else: else:
return super().get(*args, **kwargs) return super().get(*args, **kwargs)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["slug"] = self.kwargs["slug"] context["slug"] = self.kwargs["slug"]
context["last_basic"] = BasicFood.objects.order_by('-pk').all()[:10]
return context return context
@transaction.atomic @transaction.atomic

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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
default_app_config = 'logs.apps.LogsConfig' default_app_config = 'logs.apps.LogsConfig'

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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 rest_framework import serializers from rest_framework import serializers

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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 .views import ChangelogViewSet from .views import ChangelogViewSet

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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.apps import AppConfig from django.apps import AppConfig

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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.conf import settings from django.conf import settings

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
@ -56,13 +56,13 @@ def save_object(sender, instance, **kwargs):
# noinspection PyProtectedMember # noinspection PyProtectedMember
previous = instance._previous previous = instance._previous
# Si un⋅e utilisateur⋅rice est connecté⋅e, on récupère l'utilisateur⋅rice courant⋅e ainsi que son adresse IP # Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP
request = get_current_request() request = get_current_request()
if request is None: if request is None:
# Si la modification n'a pas été faite via le client Web, on suppose que c'est du à `manage.py` # Si la modification n'a pas été faite via le client Web, on suppose que c'est du à `manage.py`
# On récupère alors l'utilisateur·trice connecté·e à la VM, et on récupère la note associée # On récupère alors l'utilisateur·trice connecté·e à la VM, et on récupère la note associée
# IMPORTANT : l'utilisateur⋅rice dans la VM doit être un des alias note du respo info # IMPORTANT : l'utilisateur dans la VM doit être un des alias note du respo info
ip = "127.0.0.1" ip = "127.0.0.1"
username = Alias.normalize(getpass.getuser()) username = Alias.normalize(getpass.getuser())
note = NoteUser.objects.filter(alias__normalized_name=username) note = NoteUser.objects.filter(alias__normalized_name=username)
@ -134,13 +134,13 @@ def delete_object(sender, instance, **kwargs):
if instance._meta.label_lower in EXCLUDED or hasattr(instance, "_no_signal"): if instance._meta.label_lower in EXCLUDED or hasattr(instance, "_no_signal"):
return return
# Si un⋅e utilisateur⋅rice est connecté⋅e, on récupère l'utilisateur⋅rice courant⋅e ainsi que son adresse IP # Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP
request = get_current_request() request = get_current_request()
if request is None: if request is None:
# Si la modification n'a pas été faite via le client Web, on suppose que c'est du à `manage.py` # Si la modification n'a pas été faite via le client Web, on suppose que c'est du à `manage.py`
# On récupère alors l'utilisateur·trice connecté·e à la VM, et on récupère la note associée # On récupère alors l'utilisateur·trice connecté·e à la VM, et on récupère la note associée
# IMPORTANT : l'utilisateur⋅rice dans la VM doit être un des alias note du respo info # IMPORTANT : l'utilisateur dans la VM doit être un des alias note du respo info
ip = "127.0.0.1" ip = "127.0.0.1"
username = Alias.normalize(getpass.getuser()) username = Alias.normalize(getpass.getuser())
note = NoteUser.objects.filter(alias__normalized_name=username) note = NoteUser.objects.filter(alias__normalized_name=username)

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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
default_app_config = 'member.apps.MemberConfig' default_app_config = 'member.apps.MemberConfig'

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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.contrib import admin from django.contrib import admin

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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 rest_framework import serializers from rest_framework import serializers

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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 .views import ProfileViewSet, ClubViewSet, MembershipViewSet from .views import ProfileViewSet, ClubViewSet, MembershipViewSet

View File

@ -1,9 +1,8 @@
# Copyright (C) 2018-2025 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_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import OrderingFilter from rest_framework.filters import OrderingFilter, SearchFilter
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
@ -18,7 +17,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, RegexSafeSearchFilter] filter_backends = [DjangoFilterBackend, SearchFilter]
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',
@ -35,7 +34,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, RegexSafeSearchFilter] filter_backends = [DjangoFilterBackend, SearchFilter]
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', ]
@ -50,7 +49,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, RegexSafeSearchFilter] filter_backends = [DjangoFilterBackend, OrderingFilter, SearchFilter]
filterset_fields = ['club__name', 'club__email', 'club__note__alias__name', 'club__note__alias__normalized_name', filterset_fields = ['club__name', 'club__email', 'club__note__alias__name', 'club__note__alias__normalized_name',
'user__username', 'user__last_name', 'user__first_name', 'user__email', 'user__username', 'user__last_name', 'user__first_name', 'user__email',
'user__note__alias__name', 'user__note__alias__normalized_name', 'user__note__alias__name', 'user__note__alias__normalized_name',

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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.apps import AppConfig from django.apps import AppConfig

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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 cas_server.auth import DjangoAuthUser # pragma: no cover from cas_server.auth import DjangoAuthUser # pragma: no cover

View File

@ -1,9 +1,9 @@
# Copyright (C) 2018-2025 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 io import io
from bootstrap_datepicker_plus.widgets import DatePickerInput from PIL import Image, ImageSequence
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.contrib.auth.forms import AuthenticationForm from django.contrib.auth.forms import AuthenticationForm
@ -13,9 +13,8 @@ from django.forms import CheckboxSelectMultiple
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from note.models import NoteSpecial, Alias from note.models import NoteSpecial, Alias
from note_kfet.inputs import Autocomplete, AmountInput from note_kfet.inputs import Autocomplete, AmountInput, DatePickerInput
from permission.models import PermissionMask, Role from permission.models import PermissionMask, Role
from PIL import Image, ImageSequence
from .models import Profile, Club, Membership from .models import Profile, Club, Membership
@ -23,7 +22,7 @@ from .models import Profile, Club, Membership
class CustomAuthenticationForm(AuthenticationForm): class CustomAuthenticationForm(AuthenticationForm):
permission_mask = forms.ModelChoiceField( permission_mask = forms.ModelChoiceField(
label=_("Permission mask"), label=_("Permission mask"),
queryset=PermissionMask.objects.order_by("-rank"), queryset=PermissionMask.objects.order_by("rank"),
empty_label=None, empty_label=None,
) )
@ -33,7 +32,7 @@ class UserForm(forms.ModelForm):
# Django usernames can only contain letters, numbers, @, ., +, - and _. # Django usernames can only contain letters, numbers, @, ., +, - and _.
# We want to allow users to have uncommon and unpractical usernames: # We want to allow users to have uncommon and unpractical usernames:
# That is their problem, and we have normalized aliases for us. # That is their problem, and we have normalized aliases for us.
return super()._get_validation_exclusions() | {"username"} return super()._get_validation_exclusions() + ["username"]
class Meta: class Meta:
model = User model = User
@ -44,7 +43,6 @@ class ProfileForm(forms.ModelForm):
""" """
A form for the extras field provided by the :model:`member.Profile` model. A form for the extras field provided by the :model:`member.Profile` model.
""" """
# Remove widget=forms.HiddenInput() if you want to use report frequency.
report_frequency = forms.IntegerField(required=False, initial=0, label=_("Report frequency")) report_frequency = forms.IntegerField(required=False, initial=0, label=_("Report frequency"))
last_report = forms.DateTimeField(required=False, disabled=True, label=_("Last report date")) last_report = forms.DateTimeField(required=False, disabled=True, label=_("Last report date"))
@ -77,8 +75,7 @@ class ProfileForm(forms.ModelForm):
class Meta: class Meta:
model = Profile model = Profile
fields = '__all__' fields = '__all__'
# Remove ml_[asso]_registration from exclude if the concerned association uses nk20 to manage its mailing list. exclude = ('user', 'email_confirmed', 'registration_valid', )
exclude = ('user', 'email_confirmed', 'registration_valid', 'ml_sport_registration', )
class ImageForm(forms.Form): class ImageForm(forms.Form):
@ -213,9 +210,9 @@ class MembershipForm(forms.ModelForm):
class Meta: class Meta:
model = Membership model = Membership
fields = ('user', 'date_start') fields = ('user', 'date_start')
# Le champ d'utilisateur⋅rice est remplacé par un champ d'auto-complétion. # Le champ d'utilisateur est remplacé par un champ d'auto-complétion.
# Quand des lettres sont tapées, une requête est envoyée sur l'API d'auto-complétion # Quand des lettres sont tapées, une requête est envoyée sur l'API d'auto-complétion
# et récupère les noms d'utilisateur⋅rices valides # et récupère les noms d'utilisateur valides
widgets = { widgets = {
'user': 'user':
Autocomplete( Autocomplete(

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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 hashlib import hashlib

View File

@ -1,18 +0,0 @@
# Generated by Django 2.2.28 on 2024-08-01 12:36
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('member', '0012_club_add_registration_form'),
]
operations = [
migrations.AlterField(
model_name='profile',
name='promotion',
field=models.PositiveSmallIntegerField(default=2024, help_text='Year of entry to the school (None if not ENS student)', null=True, verbose_name='promotion'),
),
]

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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 datetime import datetime
@ -295,14 +295,7 @@ class Club(models.Model):
today = datetime.date.today() today = datetime.date.today()
# Avoid any problems on February 29 while (today - self.membership_start).days >= 365:
if self.membership_start.month == 2 and self.membership_start.day == 29:
self.membership_start -= datetime.timedelta(days=1)
if self.membership_end.month == 2 and self.membership_end.day == 29:
self.membership_end += datetime.timedelta(days=1)
while today >= datetime.date(self.membership_start.year + 1,
self.membership_start.month, self.membership_start.day):
if self.membership_start: if self.membership_start:
self.membership_start = datetime.date(self.membership_start.year + 1, self.membership_start = datetime.date(self.membership_start.year + 1,
self.membership_start.month, self.membership_start.day) self.membership_start.month, self.membership_start.day)
@ -480,10 +473,10 @@ class Membership(models.Model):
if self.club.parent_club.name == "BDE": if self.club.parent_club.name == "BDE":
parent_membership.roles.set( parent_membership.roles.set(
Role.objects.filter(Q(name="Adhérent⋅e BDE") | Q(name="Membre de club")).all()) Role.objects.filter(Q(name="Adhérent BDE") | Q(name="Membre de club")).all())
elif self.club.parent_club.name == "Kfet": elif self.club.parent_club.name == "Kfet":
parent_membership.roles.set( parent_membership.roles.set(
Role.objects.filter(Q(name="Adhérent⋅e Kfet") | Q(name="Membre de club")).all()) Role.objects.filter(Q(name="Adhérent Kfet") | Q(name="Membre de club")).all())
else: else:
parent_membership.roles.set(Role.objects.filter(name="Membre de club").all()) parent_membership.roles.set(Role.objects.filter(name="Membre de club").all())
parent_membership.save() parent_membership.save()

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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 datetime import date from datetime import date
@ -42,12 +42,12 @@ class UserTable(tables.Table):
""" """
alias = tables.Column() alias = tables.Column()
section = tables.Column(accessor='profile__section', orderable=False) section = tables.Column(accessor='profile__section')
# Override the column to let replace the URL # Override the column to let replace the URL
email = tables.EmailColumn(linkify=lambda record: "mailto:{}".format(record.email)) email = tables.EmailColumn(linkify=lambda record: "mailto:{}".format(record.email))
balance = tables.Column(accessor='note__balance', verbose_name=_("Balance"), orderable=False) balance = tables.Column(accessor='note__balance', verbose_name=_("Balance"))
def render_email(self, record, value): def render_email(self, record, value):
# Replace the email by a dash if the user can't see the profile detail # Replace the email by a dash if the user can't see the profile detail

View File

@ -11,7 +11,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
{{ title }} {{ title }}
</h3> </h3>
<div class="card-body"> <div class="card-body">
<input id="searchbar" type="text" class="form-control" placeholder="Nom/prénom/note..."> <input id="searchbar" type="text" class="form-control" placeholder="Nom/prénom/note">
<div class="form-check"> <div class="form-check">
<label class="form-check-label" for="only_active"> <label class="form-check-label" for="only_active">
<input type="checkbox" class="checkboxinput form-check-input" id="only_active" <input type="checkbox" class="checkboxinput form-check-input" id="only_active"
@ -66,4 +66,4 @@ SPDX-License-Identifier: GPL-3.0-or-later
roles_obj.change(reloadTable); roles_obj.change(reloadTable);
}); });
</script> </script>
{% endblock %} {% endblock %}

View File

@ -20,14 +20,12 @@ SPDX-License-Identifier: GPL-3.0-or-later
</form> </form>
</div> </div>
<!-- MODAL TO CROP THE IMAGE --> <!-- MODAL TO CROP THE IMAGE -->
<div class="modal fade" id="modalCrop" data-backdrop="static"> <div class="modal fade" id="modalCrop">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
<div class="modal-body-wrapper" style="width: 500px; height: 500px; padding: 16px;"> <div class="modal-body">
<div class="modal-body" style="width: 100%; height: 100%; padding: 0"> <img src="" id="modal-image" style="max-width: 100%;">
<img src="" id="modal-image" style="display: block; max-width: 100%;"> </div>
</div>
</div>
<div class="modal-footer"> <div class="modal-footer">
<div class="btn-group pull-left" role="group"> <div class="btn-group pull-left" role="group">
<button type="button" class="btn btn-default" id="js-zoom-in"> <button type="button" class="btn btn-default" id="js-zoom-in">

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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 datetime import date from datetime import date

View File

@ -291,7 +291,7 @@ class TestMemberships(TestCase):
response = self.client.post(reverse("member:club_manage_roles", args=(self.membership.pk,)), data=dict( response = self.client.post(reverse("member:club_manage_roles", args=(self.membership.pk,)), data=dict(
roles=[role.id for role in Role.objects.filter( roles=[role.id for role in Role.objects.filter(
Q(name="Membre de club") | Q(name="Trésorière de club") | Q(name="Bureau de club")).all()], Q(name="Membre de club") | Q(name="Trésorier·ère de club") | Q(name="Bureau de club")).all()],
)) ))
self.assertRedirects(response, self.user.profile.get_absolute_url(), 302, 200) self.assertRedirects(response, self.user.profile.get_absolute_url(), 302, 200)
self.membership.refresh_from_db() self.membership.refresh_from_db()

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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.urls import path from django.urls import path

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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 datetime import timedelta, date from datetime import timedelta, date
@ -16,9 +16,8 @@ from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.generic import DetailView, UpdateView, TemplateView 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 MultiTableMixin, SingleTableMixin, 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
@ -26,7 +25,6 @@ from note_kfet.middlewares import _set_current_request
from permission.backends import PermissionBackend from permission.backends import PermissionBackend
from permission.models import Role from permission.models import Role
from permission.views import ProtectQuerysetMixin, ProtectedCreateView from permission.views import ProtectQuerysetMixin, ProtectedCreateView
from django import forms
from .forms import UserForm, ProfileForm, ImageForm, ClubForm, MembershipForm, \ from .forms import UserForm, ProfileForm, ImageForm, ClubForm, MembershipForm, \
CustomAuthenticationForm, MembershipRolesForm CustomAuthenticationForm, MembershipRolesForm
@ -73,24 +71,11 @@ class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
form.fields['email'].required = True form.fields['email'].required = True
form.fields['email'].help_text = _("This address must be valid.") form.fields['email'].help_text = _("This address must be valid.")
profile_form = self.profile_form(instance=context['user_object'].profile, if PermissionBackend.check_perm(self.request, "member.change_profile", context['user_object'].profile):
data=self.request.POST if self.request.POST else None) context['profile_form'] = self.profile_form(instance=context['user_object'].profile,
data=self.request.POST if self.request.POST else None)
if not self.object.profile.report_frequency: if not self.object.profile.report_frequency:
del profile_form.fields["last_report"] del context['profile_form'].fields["last_report"]
fields_to_check = list(profile_form.fields.keys())
fields_modifiable = False
# Delete the fields for which the user does not have the permission to modify
for field_name in fields_to_check:
if not PermissionBackend.check_perm(self.request, f"member.change_profile_{field_name}", context['user_object'].profile):
profile_form.fields[field_name].widget = forms.HiddenInput()
else:
fields_modifiable = True
if fields_modifiable:
context['profile_form'] = profile_form
return context return context
@ -234,20 +219,16 @@ 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(
Q(**{f"username{suffix}": prefix + pattern}) username__iregex="^" + pattern
).union( ).union(
qs.filter( qs.filter(
(Q(**{f"alias{suffix}": prefix + pattern}) (Q(alias__iregex="^" + pattern)
| Q(**{f"normalized_alias{suffix}": prefix + Alias.normalize(pattern)}) | Q(normalized_alias__iregex="^" + Alias.normalize(pattern))
| Q(**{f"last_name{suffix}": prefix + pattern}) | Q(last_name__iregex="^" + pattern)
| Q(**{f"first_name{suffix}": prefix + pattern}) | Q(first_name__iregex="^" + pattern)
| Q(email__istartswith=pattern)) | Q(email__istartswith=pattern))
& ~Q(**{f"username{suffix}": prefix + pattern}) & ~Q(username__iregex="^" + pattern)
), all=True) ), all=True)
else: else:
qs = qs.none() qs = qs.none()
@ -262,7 +243,7 @@ class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
return context return context
class ProfileTrustView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, DetailView): class ProfileTrustView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
""" """
View and manage user trust relationships View and manage user trust relationships
""" """
@ -271,25 +252,13 @@ class ProfileTrustView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin
context_object_name = 'user_object' context_object_name = 'user_object'
extra_context = {"title": _("Note friendships")} extra_context = {"title": _("Note friendships")}
tables = [
lambda data: TrustTable(data, prefix="trust-"),
lambda data: TrustedTable(data, prefix="trusted-"),
]
def get_tables_data(self):
note = self.object.note
return [
note.trusting.filter(PermissionBackend.filter_queryset(self.request, Trust, "view")).distinct(),
note.trusted.filter(PermissionBackend.filter_queryset(self.request, Trust, "view")).distinct(),
]
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
note = context['object'].note
tables = context["tables"] context["trusting"] = TrustTable(
for name, table in zip(["trusting", "trusted_by"], tables): note.trusting.filter(PermissionBackend.filter_queryset(self.request, Trust, "view")).distinct().all())
context[name] = table context["trusted_by"] = TrustedTable(
note.trusted.filter(PermissionBackend.filter_queryset(self.request, Trust, "view")).distinct().all())
context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_trust", Trust( context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_trust", Trust(
trusting=context["object"].note, trusting=context["object"].note,
trusted=context["object"].note trusted=context["object"].note
@ -308,7 +277,7 @@ class ProfileTrustView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin
return context return context
class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableMixin, DetailView): class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
""" """
View and manage user aliases. View and manage user aliases.
""" """
@ -317,15 +286,12 @@ class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableMixi
context_object_name = 'user_object' context_object_name = 'user_object'
extra_context = {"title": _("Note aliases")} extra_context = {"title": _("Note aliases")}
table_class = AliasTable
context_table_name = "aliases"
def get_table_data(self):
return self.object.note.alias.filter(PermissionBackend.filter_queryset(self.request, Alias, "view")).distinct() \
.order_by('normalized_name')
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
note = context['object'].note
context["aliases"] = AliasTable(
note.alias.filter(PermissionBackend.filter_queryset(self.request, Alias, "view")).distinct()
.order_by('normalized_name').all())
context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_alias", Alias( context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_alias", Alias(
note=context["object"].note, note=context["object"].note,
name="", name="",
@ -444,15 +410,10 @@ 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(**{f"name{suffix}": prefix + pattern}) Q(name__iregex=pattern)
| Q(**{f"note__alias__name{suffix}": prefix + pattern}) | Q(note__alias__name__iregex=pattern)
| Q(**{f"note__alias__normalized_name{suffix}": prefix + Alias.normalize(pattern)}) | Q(note__alias__normalized_name__iregex=Alias.normalize(pattern))
) )
return qs return qs
@ -549,7 +510,7 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
return context return context
class ClubAliasView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableMixin, DetailView): class ClubAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
""" """
Manage aliases of a club. Manage aliases of a club.
""" """
@ -558,16 +519,11 @@ class ClubAliasView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableMixin,
context_object_name = 'club' context_object_name = 'club'
extra_context = {"title": _("Note aliases")} extra_context = {"title": _("Note aliases")}
table_class = AliasTable
context_table_name = "aliases"
def get_table_data(self):
return self.object.note.alias.filter(
PermissionBackend.filter_queryset(self.request, Alias, "view")).distinct()
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
note = context['object'].note
context["aliases"] = AliasTable(note.alias.filter(
PermissionBackend.filter_queryset(self.request, Alias, "view")).distinct().all())
context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_alias", Alias( context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_alias", Alias(
note=context["object"].note, note=context["object"].note,
name="", name="",
@ -871,8 +827,8 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
ret = super().form_valid(form) ret = super().form_valid(form)
member_role = Role.objects.filter(Q(name="Adhérent⋅e BDE") | Q(name="Membre de club")).all() \ member_role = Role.objects.filter(Q(name="Adhérent BDE") | Q(name="Membre de club")).all() \
if club.name == "BDE" else Role.objects.filter(Q(name="Adhérent⋅e Kfet") | Q(name="Membre de club")).all() \ if club.name == "BDE" else Role.objects.filter(Q(name="Adhérent Kfet") | Q(name="Membre de club")).all() \
if club.name == "Kfet"else Role.objects.filter(name="Membre de club").all() if club.name == "Kfet"else Role.objects.filter(name="Membre de club").all()
# Set the same roles as before # Set the same roles as before
if old_membership: if old_membership:
@ -908,7 +864,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
membership.refresh_from_db() membership.refresh_from_db()
if old_membership.exists(): if old_membership.exists():
membership.roles.set(old_membership.get().roles.all()) membership.roles.set(old_membership.get().roles.all())
membership.roles.set(Role.objects.filter(Q(name="Adhérent⋅e Kfet") | Q(name="Membre de club")).all()) membership.roles.set(Role.objects.filter(Q(name="Adhérent Kfet") | Q(name="Membre de club")).all())
membership.save() membership.save()
return ret return ret
@ -956,15 +912,10 @@ 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(**{f"user__first_name{suffix}": prefix + pattern}) Q(user__first_name__iregex='^' + pattern)
| Q(**{f"user__last_name{suffix}": prefix + pattern}) | Q(user__last_name__iregex='^' + pattern)
| Q(**{f"user__note__alias__normalized_name{suffix}": prefix + Alias.normalize(pattern)}) | Q(user__note__alias__normalized_name__iregex='^' + Alias.normalize(pattern))
) )
only_active = "only_active" not in self.request.GET or self.request.GET["only_active"] != '0' only_active = "only_active" not in self.request.GET or self.request.GET["only_active"] != '0'

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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
default_app_config = 'note.apps.NoteConfig' default_app_config = 'note.apps.NoteConfig'

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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.contrib import admin from django.contrib import admin

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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.conf import settings from django.conf import settings

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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 .views import NotePolymorphicViewSet, AliasViewSet, ConsumerViewSet, \ from .views import NotePolymorphicViewSet, AliasViewSet, ConsumerViewSet, \

View File

@ -1,16 +1,16 @@
# Copyright (C) 2018-2025 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 from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework import status, viewsets from rest_framework import viewsets
from rest_framework.response import Response from rest_framework.response import Response
from api.filters import RegexSafeSearchFilter from rest_framework import status
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, RegexSafeSearchFilter, OrderingFilter] filter_backends = [DjangoFilterBackend, SearchFilter, 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,14 +48,10 @@ 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(**{f"alias__name{suffix}": alias_prefix + alias}) Q(alias__name__iregex="^" + alias)
| Q(**{f"alias__normalized_name{suffix}": alias_prefix + Alias.normalize(alias)}) | Q(alias__normalized_name__iregex="^" + Alias.normalize(alias))
| Q(**{f"alias__normalized_name{suffix}": alias_prefix + alias.lower()}) | Q(alias__normalized_name__iregex="^" + alias.lower())
) )
return queryset.order_by("id") return queryset.order_by("id")
@ -69,7 +65,7 @@ class TrustViewSet(ReadProtectedModelViewSet):
""" """
queryset = Trust.objects queryset = Trust.objects
serializer_class = TrustSerializer serializer_class = TrustSerializer
filter_backends = [RegexSafeSearchFilter, DjangoFilterBackend, OrderingFilter] filter_backends = [SearchFilter, 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']
@ -95,11 +91,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/alias/ then render it on /api/note/aliases/
""" """
queryset = Alias.objects queryset = Alias.objects
serializer_class = AliasSerializer serializer_class = AliasSerializer
filter_backends = [RegexSafeSearchFilter, DjangoFilterBackend, OrderingFilter] filter_backends = [SearchFilter, 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', ]
@ -130,22 +126,18 @@ 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(
**{f"name{suffix}": alias_prefix + alias} name__iregex="^" + alias
).union( ).union(
queryset.filter( queryset.filter(
Q(**{f"normalized_name{suffix}": alias_prefix + Alias.normalize(alias)}) Q(normalized_name__iregex="^" + Alias.normalize(alias))
& ~Q(**{f"name{suffix}": alias_prefix + alias}) & ~Q(name__iregex="^" + alias)
), ),
all=True).union( all=True).union(
queryset.filter( queryset.filter(
Q(**{f"normalized_name{suffix}": "^" + alias.lower()}) Q(normalized_name__iregex="^" + alias.lower())
& ~Q(**{f"normalized_name{suffix}": "^" + Alias.normalize(alias)}) & ~Q(normalized_name__iregex="^" + Alias.normalize(alias))
& ~Q(**{f"name{suffix}": "^" + alias}) & ~Q(name__iregex="^" + alias)
), ),
all=True) all=True)
@ -155,7 +147,7 @@ class AliasViewSet(ReadProtectedModelViewSet):
class ConsumerViewSet(ReadOnlyProtectedModelViewSet): class ConsumerViewSet(ReadOnlyProtectedModelViewSet):
queryset = Alias.objects queryset = Alias.objects
serializer_class = ConsumerSerializer serializer_class = ConsumerSerializer
filter_backends = [RegexSafeSearchFilter, OrderingFilter, DjangoFilterBackend] filter_backends = [SearchFilter, 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', ]
@ -174,7 +166,11 @@ 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
valid_regex = is_regex(alias) try:
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')
@ -183,10 +179,19 @@ class ConsumerViewSet(ReadOnlyProtectedModelViewSet):
# We match first an alias if it is matched without normalization, # We match first an alias if it is matched without normalization,
# then if the normalized pattern matches a normalized alias. # then if the normalized pattern matches a normalized alias.
queryset = queryset.filter( queryset = queryset.filter(
Q(**{f'name{suffix}': alias_prefix + alias}) **{f'name{suffix}': alias_prefix + alias}
| Q(**{f'normalized_name{suffix}': alias_prefix + Alias.normalize(alias)}) ).union(
| Q(**{f'normalized_name{suffix}': alias_prefix + alias.lower()}) queryset.filter(
) Q(**{f'normalized_name{suffix}': alias_prefix + Alias.normalize(alias)})
& ~Q(**{f'name{suffix}': alias_prefix + alias})
),
all=True).union(
queryset.filter(
Q(**{f'normalized_name{suffix}': alias_prefix + alias.lower()})
& ~Q(**{f'normalized_name{suffix}': alias_prefix + Alias.normalize(alias)})
& ~Q(**{f'name{suffix}': alias_prefix + alias})
),
all=True)
queryset = queryset if settings.DATABASES[queryset.db]["ENGINE"] == 'django.db.backends.postgresql' \ queryset = queryset if settings.DATABASES[queryset.db]["ENGINE"] == 'django.db.backends.postgresql' \
else queryset.order_by("name") else queryset.order_by("name")
@ -202,7 +207,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, RegexSafeSearchFilter] filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['name', 'templates', 'templates__name'] filterset_fields = ['name', 'templates', 'templates__name']
search_fields = ['$name', '$templates__name', ] search_fields = ['$name', '$templates__name', ]
@ -215,7 +220,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 = [RegexSafeSearchFilter, DjangoFilterBackend, OrderingFilter] filter_backends = [SearchFilter, 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', ]
@ -229,7 +234,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 = [RegexSafeSearchFilter, DjangoFilterBackend, OrderingFilter] filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
filterset_fields = ['source', 'source_alias', 'source__alias__name', 'source__alias__normalized_name', filterset_fields = ['source', 'source_alias', 'source__alias__name', 'source__alias__normalized_name',
'destination', 'destination_alias', 'destination__alias__name', 'destination', 'destination_alias', 'destination__alias__name',
'destination__alias__normalized_name', 'quantity', 'polymorphic_ctype', 'amount', 'destination__alias__normalized_name', 'quantity', 'polymorphic_ctype', 'amount',

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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.apps import AppConfig from django.apps import AppConfig

View File

@ -1,14 +1,13 @@
# Copyright (C) 2018-2025 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 datetime import datetime from datetime import datetime
from bootstrap_datepicker_plus.widgets import DateTimePickerInput
from django import forms from django import forms
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.forms import CheckboxSelectMultiple from django.forms import CheckboxSelectMultiple
from django.utils.timezone import make_aware from django.utils.timezone import make_aware
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from note_kfet.inputs import Autocomplete, AmountInput from note_kfet.inputs import Autocomplete, AmountInput, DateTimePickerInput
from .models import TransactionTemplate, NoteClub, Alias from .models import TransactionTemplate, NoteClub, Alias

View File

@ -1,25 +0,0 @@
# Generated by Django 4.2.15 on 2024-08-28 08:00
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('note', '0006_trust'),
]
operations = [
migrations.AlterField(
model_name='note',
name='polymorphic_ctype',
field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype'),
),
migrations.AlterField(
model_name='transaction',
name='polymorphic_ctype',
field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype'),
),
]

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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 .notes import Alias, Note, NoteClub, NoteSpecial, NoteUser, Trust from .notes import Alias, Note, NoteClub, NoteSpecial, NoteUser, Trust

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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 unicodedata import unicodedata

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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.core.exceptions import ValidationError from django.core.exceptions import ValidationError

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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.utils import timezone from django.utils import timezone

View File

@ -1,4 +1,4 @@
// Copyright (C) 2018-2025 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
// When a transaction is performed, lock the interface to prevent spam clicks. // When a transaction is performed, lock the interface to prevent spam clicks.
@ -245,7 +245,7 @@ function consume (source, source_alias, dest, quantity, amount, reason, type, ca
invalidity_reason: 'Solde insuffisant', invalidity_reason: 'Solde insuffisant',
polymorphic_ctype: type, polymorphic_ctype: type,
resourcetype: 'RecurrentTransaction', resourcetype: 'RecurrentTransaction',
source: source.id, source: source,
source_alias: source_alias, source_alias: source_alias,
destination: dest, destination: dest,
template: template template: template
@ -294,10 +294,3 @@ searchbar.addEventListener("keyup", function (e) {
if (firstMatch && e.key === "Enter") if (firstMatch && e.key === "Enter")
firstMatch.click() firstMatch.click()
}); });
function createshiny() {
const list_btn = document.querySelectorAll('.btn-outline-dark')
const shiny_class = list_btn[Math.floor(Math.random() * list_btn.length)].classList
shiny_class.replace('btn-outline-dark', 'btn-outline-dark-shiny')
}
createshiny()

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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 html import html
@ -260,13 +260,11 @@ class ButtonTable(tables.Table):
text=_('edit'), text=_('edit'),
accessor='pk', accessor='pk',
verbose_name=_("Edit"), verbose_name=_("Edit"),
orderable=False,
) )
hideshow = tables.Column( hideshow = tables.Column(
verbose_name=_("Hide/Show"), verbose_name=_("Hide/Show"),
accessor="pk", accessor="pk",
orderable=False,
attrs={ attrs={
'td': { 'td': {
'class': 'col-sm-1', 'class': 'col-sm-1',
@ -278,8 +276,7 @@ class ButtonTable(tables.Table):
delete_col = tables.TemplateColumn(template_code=DELETE_TEMPLATE, delete_col = tables.TemplateColumn(template_code=DELETE_TEMPLATE,
extra_context={"delete_trans": _('delete')}, extra_context={"delete_trans": _('delete')},
attrs={'td': {'class': 'col-sm-1'}}, attrs={'td': {'class': 'col-sm-1'}},
verbose_name=_("Delete"), verbose_name=_("Delete"), )
orderable=False, )
def render_amount(self, value): def render_amount(self, value):
return pretty_money(value) return pretty_money(value)

View File

@ -9,7 +9,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
name="{{ widget.name }}" name="{{ widget.name }}"
{# Other attributes are loaded #} {# Other attributes are loaded #}
{% for name, value in widget.attrs.items %} {% for name, value in widget.attrs.items %}
{% if value is not False %}{{ name }}{% if value is not True %}="{{ value|stringformat:'s' }}"{% endif %}{% endif %} {% ifnotequal value False %}{{ name }}{% ifnotequal value True %}="{{ value|stringformat:'s' }}"{% endifnotequal %}{% endifnotequal %}
{% endfor %}> {% endfor %}>
<div class="input-group-append"> <div class="input-group-append">
<span class="input-group-text"></span> <span class="input-group-text"></span>

View File

@ -22,7 +22,7 @@
</p> </p>
<p> <p>
Par ailleurs, le BDE ne sert pas d'alcool aux adhérent⋅es dont le solde Par ailleurs, le BDE ne sert pas d'alcool aux adhérents dont le solde
est inférieur à 0 €. est inférieur à 0 €.
</p> </p>

View File

@ -22,4 +22,4 @@ virement bancaire.
-- --
Le BDE Le BDE
{% 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" %}

View File

@ -89,7 +89,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
</ul> </ul>
<div class="card-body"> <div class="card-body">
<select id="debit_type" class="form-control custom-select d-none"> <select id="debit_type" class="form-control custom-select d-none">
{% for special_type in special_types|slice:"::-1" %} {% for special_type in special_types %}
<option value="{{ special_type.id }}">{{ special_type.special_type }}</option> <option value="{{ special_type.id }}">{{ special_type.special_type }}</option>
{% endfor %} {% endfor %}
</select> </select>

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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 template from django import template

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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 template from django import template

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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.tests import TestAPI from api.tests import TestAPI

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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.urls import path from django.urls import path

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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 json import json
@ -13,7 +13,6 @@ 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
@ -90,15 +89,11 @@ 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(**{f"name{suffix}": pattern}) Q(name__iregex=pattern)
| Q(**{f"destination__club__name{suffix}": pattern}) | Q(destination__club__name__iregex=pattern)
| Q(**{f"category__name{suffix}": pattern}) | Q(category__name__iregex=pattern)
| Q(**{f"description{suffix}": pattern}) | Q(description__iregex=pattern)
) )
qs = qs.order_by('-display', 'category__name', 'destination__club__name', 'name') qs = qs.order_by('-display', 'category__name', 'destination__club__name', 'name')
@ -228,10 +223,7 @@ 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"]:
# Check if this is a valid regex. If not, we won't check regex transactions = transactions.filter(reason__iregex=data["reason"])
valid_regex = is_regex(data["reason"])
suffix = "__iregex" if valid_regex else "__istartswith"
transactions = transactions.filter(Q(**{f"reason{suffix}": data["reason"]}))
if "valid" in data and data["valid"]: if "valid" in data and data["valid"]:
transactions = transactions.filter(valid=data["valid"]) transactions = transactions.filter(valid=data["valid"])
if "amount_gte" in data and data["amount_gte"]: if "amount_gte" in data and data["amount_gte"]:

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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
default_app_config = 'permission.apps.PermissionConfig' default_app_config = 'permission.apps.PermissionConfig'

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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.contrib import admin from django.contrib import admin
@ -31,4 +31,3 @@ class RoleAdmin(admin.ModelAdmin):
Admin customisation for Role Admin customisation for Role
""" """
list_display = ('name', ) list_display = ('name', )
filter_horizontal = ('permissions',)

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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 rest_framework import serializers from rest_framework import serializers

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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 .views import PermissionViewSet, RoleViewSet from .views import PermissionViewSet, RoleViewSet

View File

@ -1,9 +1,9 @@
# Copyright (C) 2018-2025 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_filters.rest_framework import DjangoFilterBackend
from api.filters import RegexSafeSearchFilter
from api.viewsets import ReadOnlyProtectedModelViewSet from api.viewsets import ReadOnlyProtectedModelViewSet
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import SearchFilter
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, RegexSafeSearchFilter] filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['model', 'type', 'query', 'mask', 'field', 'permanent', ] filterset_fields = ['model', 'type', 'query', 'mask', 'field', 'permanent', ]
search_fields = ['$model__model', '$query', '$description', ] search_fields = ['$model__name', '$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, RegexSafeSearchFilter] filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['name', 'permissions', 'for_club', 'memberships__user', ] filterset_fields = ['name', 'permissions', 'for_club', 'memberships__user', ]
search_fields = ['$name', '$for_club__name', ] search_fields = ['$name', '$for_club__name', ]

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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.apps import AppConfig from django.apps import AppConfig

View File

@ -1,7 +1,7 @@
# Copyright (C) 2018-2025 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 datetime import date, timedelta from datetime import date
from django.contrib.auth.backends import ModelBackend from django.contrib.auth.backends import ModelBackend
from django.contrib.auth.models import User from django.contrib.auth.models import User
@ -106,7 +106,6 @@ class PermissionBackend(ModelBackend):
Q=Q, Q=Q,
now=timezone.now(), now=timezone.now(),
today=date.today(), today=date.today(),
week=timedelta(days=7),
) )
yield permission yield permission

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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 sys import sys
from functools import lru_cache from functools import lru_cache

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 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 functools import functools
@ -135,18 +135,18 @@ class Permission(models.Model):
# A json encoded Q object with the following grammar # A json encoded Q object with the following grammar
# query -> [] | {} (the empty query representing all objects) # query -> [] | {} (the empty query representing all objects)
# query -> ["AND", query, ...] AND multiple queries # query -> ["AND", query, …] AND multiple queries
# | ["OR", query, ...] OR multiple queries # | ["OR", query, …] OR multiple queries
# | ["NOT", query] Opposite of query # | ["NOT", query] Opposite of query
# query -> {key: value, ...} A list of fields and values of a Q object # query -> {key: value, …} A list of fields and values of a Q object
# key -> string A field name # key -> string A field name
# value -> int | string | bool | null Literal values # value -> int | string | bool | null Literal values
# | [parameter, ...] A parameter. See compute_param for more details. # | [parameter, …] A parameter. See compute_param for more details.
# | {"F": oper} An F object # | {"F": oper} An F object
# oper -> [string, ...] A parameter. See compute_param for more details. # oper -> [string, …] A parameter. See compute_param for more details.
# | ["ADD", oper, ...] Sum multiple F objects or literal # | ["ADD", oper, …] Sum multiple F objects or literal
# | ["SUB", oper, oper] Substract two F objects or literal # | ["SUB", oper, oper] Substract two F objects or literal
# | ["MUL", oper, ...] Multiply F objects or literals # | ["MUL", oper, …] Multiply F objects or literals
# | int | string | bool | null Literal values # | int | string | bool | null Literal values
# | ["F", string] A field # | ["F", string] A field
# #

Some files were not shown because too many files have changed in this diff Show More