mirror of
https://gitlab.crans.org/bde/nk20
synced 2025-07-07 07:54:02 +02:00
Compare commits
193 Commits
6d7076b03e
...
export_mem
Author | SHA1 | Date | |
---|---|---|---|
5ef019c5c2 | |||
8da62e62fb | |||
56a43396d4 | |||
7966d6f397 | |||
cb61c511ce | |||
25bfa575ed | |||
e21d9fcfbe | |||
b293904525 | |||
bd7e6b8ad4 | |||
a208a4fa25 | |||
4799b2c52d | |||
562dcfb908 | |||
12ef258ff0 | |||
2ae32ee3b6 | |||
ec1bd45481 | |||
370a9a069e | |||
7f0a3784e9 | |||
36f4adf2e7 | |||
ae7d5d5489 | |||
434097aba4 | |||
a0ebf8658d | |||
423454ba5d | |||
3ccb31639c | |||
5fb12a1388 | |||
fe029893b0 | |||
767e98c2a3 | |||
1bdad76fe9 | |||
0196db7fff | |||
1f53ad4407 | |||
018f6e3f13 | |||
9752a030d9 | |||
b27bdb090d | |||
55a0fbb6cb | |||
c356534309 | |||
51315a0555 | |||
e5f9fe2cf5 | |||
c630a3fbd5 | |||
79b8ebeca4 | |||
dc14ba0101 | |||
6028bfeb56 | |||
bd9773a8af | |||
cdeb76d9f8 | |||
ac4574200d | |||
b17d31e8ee | |||
30d27459dd | |||
333f7aa284 | |||
587314e03c | |||
9f888a5281 | |||
88b1a25ca0 | |||
8cb50f58f2 | |||
041a8f20a9 | |||
b1ffb28532 | |||
6225fb51f1 | |||
1dd74e8024 | |||
1af9f5f23c | |||
83d5a7ceff | |||
a7cba0a4a3 | |||
ccd9a66ab9 | |||
c7a92fa4b2 | |||
5f1b698d58 | |||
0a5368d23f | |||
26b351a51c | |||
1836677c47 | |||
e7a98c86f0 | |||
eb5044490b | |||
983d7ec052 | |||
dc56deaf85 | |||
19d1ecfc66 | |||
694f54e1c4 | |||
b0c3eee699 | |||
cd942779ca | |||
0d0fdef363 | |||
7ed544b3ac | |||
821efbf78b | |||
a209e0d366 | |||
ef485e0628 | |||
1481aa0635 | |||
867bf9fd25 | |||
47fda0ea36 | |||
623290827a | |||
a87ce625f3 | |||
3559787fa7 | |||
bd6ed27ae5 | |||
43dc676747 | |||
caaeab6b0b | |||
54ba786884 | |||
80e109114f | |||
787005e60d | |||
414e103686 | |||
942d887c2e | |||
a63c34fe37 | |||
2be6133458 | |||
7975fe47a6 | |||
476fbceeea | |||
8fbaa0bdc8 | |||
a0de63effd | |||
09fb1d227e | |||
2e27d4f05c | |||
5d16dc4e7d | |||
3c34033bf5 | |||
131f508433 | |||
c1a353963a | |||
178ce2b579 | |||
9162319734 | |||
5d2a8e9b79 | |||
33c94d0720 | |||
5040e8e8ea | |||
c5697c4cb4 | |||
e188c5a153 | |||
94e1fdc93a | |||
d1ef367bab | |||
0fbb19c5fd | |||
21cbf2b21a | |||
185a2cabf2 | |||
7552e55c8d | |||
361de9f8b4 | |||
e2426bd6a6 | |||
7fea619a9f | |||
7b5eefcc0a | |||
e4aa16986f | |||
b92e6e4e10 | |||
dd675b3676 | |||
f50849b4f8 | |||
73ff35c232 | |||
a5df98224f | |||
2cb9ac8735 | |||
35d4849a28 | |||
96539d262f | |||
946674f59b | |||
a201d8376a | |||
a21b9275ea | |||
d4e85e8215 | |||
7af2ebba40 | |||
bd94400883 | |||
5558341c8c | |||
35ef82223c | |||
9ccac36831 | |||
2e71ce05a9 | |||
f2cb10b69f | |||
24c4edf2e3 | |||
213e9a8b12 | |||
2c56178b15 | |||
48a5b04579 | |||
2ab5c4082a | |||
053225c6dc | |||
ac7b86651d | |||
21f5a5d566 | |||
ff9c78ed4e | |||
1e121297d1 | |||
549f56dc0b | |||
debeb33d46 | |||
28117c8c61 | |||
0d9891fbd8 | |||
4be4a18dd1 | |||
27b00ba4f0 | |||
3fcbb4f310 | |||
d1c9a2a7f1 | |||
a673fd6871 | |||
a324d3a892 | |||
951ba74f8f | |||
abc4f14bd1 | |||
47138bafd4 | |||
a3920fcae3 | |||
ae4213d087 | |||
cbf92651f0 | |||
12c93ff9da | |||
354c79bb82 | |||
1ea7b3dda1 | |||
35ffbfcf55 | |||
162371042c | |||
581715d804 | |||
c7c6f0350f | |||
9d1024024b | |||
d595d908c6 | |||
734f5b242d | |||
b0c7d43a50 | |||
7322d55789 | |||
1a258dfe9e | |||
bbbdcc7247 | |||
feeb99041f | |||
6c61daf1c5 | |||
96215cc1ff | |||
b7a71d911d | |||
2ee7f41dfe | |||
fb3337966e | |||
399a32bece | |||
82fea65b5e | |||
abc88d0118 | |||
b6b81a8b8f | |||
d228dbf225 | |||
516a7f4be5 | |||
2f8c9b54e7 | |||
e9f18c3ed9 |
@ -7,25 +7,10 @@ stages:
|
||||
variables:
|
||||
GIT_SUBMODULE_STRATEGY: recursive
|
||||
|
||||
# Debian Buster
|
||||
# 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:
|
||||
# Ubuntu 22.04
|
||||
py310-django42:
|
||||
stage: test
|
||||
image: ubuntu:20.04
|
||||
image: ubuntu:22.04
|
||||
before_script:
|
||||
# Fix tzdata prompt
|
||||
- ln -sf /usr/share/zoneinfo/Europe/Paris /etc/localtime && echo Europe/Paris > /etc/timezone
|
||||
@ -37,12 +22,12 @@ py38-django22:
|
||||
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 py38-django22
|
||||
script: tox -e py310-django42
|
||||
|
||||
# Debian Bullseye
|
||||
py39-django22:
|
||||
# Debian Bookworm
|
||||
py311-django42:
|
||||
stage: test
|
||||
image: debian:bullseye
|
||||
image: debian:bookworm
|
||||
before_script:
|
||||
- >
|
||||
apt-get update &&
|
||||
@ -52,11 +37,11 @@ py39-django22:
|
||||
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 py39-django22
|
||||
script: tox -e py311-django42
|
||||
|
||||
linters:
|
||||
stage: quality-assurance
|
||||
image: debian:bullseye
|
||||
image: debian:bookworm
|
||||
before_script:
|
||||
- apt-get update && apt-get install -y tox
|
||||
script: tox -e linters
|
||||
|
2
.gitmodules
vendored
2
.gitmodules
vendored
@ -1,3 +1,3 @@
|
||||
[submodule "apps/scripts"]
|
||||
path = apps/scripts
|
||||
url = https://gitlab.crans.org/bde/nk20-scripts.git
|
||||
url = https://gitlab.crans.org/bde/nk20-scripts
|
||||
|
@ -55,7 +55,7 @@ Bien que cela permette de créer une instance sur toutes les distributions,
|
||||
(env)$ ./manage.py makemigrations
|
||||
(env)$ ./manage.py migrate
|
||||
(env)$ ./manage.py loaddata initial
|
||||
(env)$ ./manage.py createsuperuser # Création d'un utilisateur initial
|
||||
(env)$ ./manage.py createsuperuser # Création d'un⋅e utilisateur⋅rice initial
|
||||
```
|
||||
|
||||
6. Enjoy :
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
default_app_config = 'activity.apps.ActivityConfig'
|
||||
|
@ -1,11 +1,11 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.contrib import admin
|
||||
from note_kfet.admin import admin_site
|
||||
|
||||
from .forms import GuestForm
|
||||
from .models import Activity, ActivityType, Entry, Guest
|
||||
from .models import Activity, ActivityType, Entry, Guest, Opener
|
||||
|
||||
|
||||
@admin.register(Activity, site=admin_site)
|
||||
@ -45,3 +45,11 @@ class EntryAdmin(admin.ModelAdmin):
|
||||
Admin customisation for Entry
|
||||
"""
|
||||
list_display = ('note', 'activity', 'time', 'guest')
|
||||
|
||||
|
||||
@admin.register(Opener, site=admin_site)
|
||||
class OpenerAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Admin customisation for Opener
|
||||
"""
|
||||
list_display = ('activity', 'opener')
|
||||
|
@ -1,9 +1,11 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
from rest_framework.validators import UniqueTogetherValidator
|
||||
|
||||
from ..models import Activity, ActivityType, Entry, Guest, GuestTransaction
|
||||
from ..models import Activity, ActivityType, Entry, Guest, GuestTransaction, Opener
|
||||
|
||||
|
||||
class ActivityTypeSerializer(serializers.ModelSerializer):
|
||||
@ -59,3 +61,17 @@ class GuestTransactionSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = GuestTransaction
|
||||
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"))]
|
||||
|
@ -1,7 +1,7 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from .views import ActivityTypeViewSet, ActivityViewSet, EntryViewSet, GuestViewSet
|
||||
from .views import ActivityTypeViewSet, ActivityViewSet, EntryViewSet, GuestViewSet, OpenerViewSet
|
||||
|
||||
|
||||
def register_activity_urls(router, path):
|
||||
@ -12,3 +12,4 @@ def register_activity_urls(router, path):
|
||||
router.register(path + '/type', ActivityTypeViewSet)
|
||||
router.register(path + '/guest', GuestViewSet)
|
||||
router.register(path + '/entry', EntryViewSet)
|
||||
router.register(path + '/opener', OpenerViewSet)
|
||||
|
@ -1,12 +1,15 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from api.filters import RegexSafeSearchFilter
|
||||
from api.viewsets import ReadProtectedModelViewSet
|
||||
from django.core.exceptions import ValidationError
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework.filters import SearchFilter
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
|
||||
from .serializers import ActivitySerializer, ActivityTypeSerializer, EntrySerializer, GuestSerializer
|
||||
from ..models import Activity, ActivityType, Entry, Guest
|
||||
from .serializers import ActivitySerializer, ActivityTypeSerializer, EntrySerializer, GuestSerializer, OpenerSerializer
|
||||
from ..models import Activity, ActivityType, Entry, Guest, Opener
|
||||
|
||||
|
||||
class ActivityTypeViewSet(ReadProtectedModelViewSet):
|
||||
@ -29,7 +32,7 @@ class ActivityViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
queryset = Activity.objects.order_by('id')
|
||||
serializer_class = ActivitySerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||
filterset_fields = ['name', 'description', 'activity_type', 'location', 'creater', 'organizer', 'attendees_club',
|
||||
'date_start', 'date_end', 'valid', 'open', ]
|
||||
search_fields = ['$name', '$description', '$location', '$creater__last_name', '$creater__first_name',
|
||||
@ -47,7 +50,7 @@ class GuestViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
queryset = Guest.objects.order_by('id')
|
||||
serializer_class = GuestSerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||
filterset_fields = ['activity', 'activity__name', 'last_name', 'first_name', 'inviter', 'inviter__alias__name',
|
||||
'inviter__alias__normalized_name', ]
|
||||
search_fields = ['$activity__name', '$last_name', '$first_name', '$inviter__user__email', '$inviter__alias__name',
|
||||
@ -62,7 +65,36 @@ class EntryViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
queryset = Entry.objects.order_by('id')
|
||||
serializer_class = EntrySerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||
filterset_fields = ['activity', 'time', 'note', 'guest', ]
|
||||
search_fields = ['$activity__name', '$note__user__email', '$note__alias__name', '$note__alias__normalized_name',
|
||||
'$guest__last_name', '$guest__first_name', ]
|
||||
|
||||
|
||||
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)
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
@ -1,16 +1,17 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from datetime import timedelta
|
||||
from random import shuffle
|
||||
|
||||
from bootstrap_datepicker_plus.widgets import DateTimePickerInput
|
||||
from django import forms
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from member.models import Club
|
||||
from note.models import Note, NoteUser
|
||||
from note_kfet.inputs import Autocomplete, DateTimePickerInput
|
||||
from note_kfet.inputs import Autocomplete
|
||||
from note_kfet.middlewares import get_current_request
|
||||
from permission.backends import PermissionBackend
|
||||
|
||||
@ -43,7 +44,7 @@ class ActivityForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Activity
|
||||
exclude = ('creater', 'valid', 'open', )
|
||||
exclude = ('creater', 'valid', 'open', 'opener', )
|
||||
widgets = {
|
||||
"organizer": Autocomplete(
|
||||
model=Club,
|
||||
|
28
apps/activity/migrations/0004_opener.py
Normal file
28
apps/activity/migrations/0004_opener.py
Normal file
@ -0,0 +1,28 @@
|
||||
# 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')},
|
||||
},
|
||||
),
|
||||
]
|
@ -0,0 +1,24 @@
|
||||
# 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'),
|
||||
),
|
||||
]
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import os
|
||||
@ -11,7 +11,7 @@ from django.db import models, transaction
|
||||
from django.db.models import Q
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from note.models import NoteUser, Transaction
|
||||
from note.models import NoteUser, Transaction, Note
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
|
||||
@ -310,3 +310,31 @@ class GuestTransaction(Transaction):
|
||||
@property
|
||||
def type(self):
|
||||
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))
|
||||
|
57
apps/activity/static/activity/js/opener.js
Normal file
57
apps/activity/static/activity/js/opener.js
Normal file
@ -0,0 +1,57 @@
|
||||
/**
|
||||
* 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)
|
||||
})
|
@ -1,15 +1,17 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.utils import timezone
|
||||
from django.utils.html import escape
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from note_kfet.middlewares import get_current_request
|
||||
import django_tables2 as tables
|
||||
from django_tables2 import A
|
||||
from permission.backends import PermissionBackend
|
||||
from note.templatetags.pretty_money import pretty_money
|
||||
|
||||
from .models import Activity, Entry, Guest
|
||||
from .models import Activity, Entry, Guest, Opener
|
||||
|
||||
|
||||
class ActivityTable(tables.Table):
|
||||
@ -113,3 +115,34 @@ class EntryTable(tables.Table):
|
||||
'data-last-name': lambda record: record.last_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"),)
|
||||
|
@ -4,11 +4,31 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
{% endcomment %}
|
||||
{% load i18n perms %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load static django_tables2 i18n %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="text-white">{{ title }}</h1>
|
||||
{% 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 %}
|
||||
<div class="card bg-white mb-3">
|
||||
<h3 class="card-header text-center">
|
||||
@ -22,6 +42,8 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
{% endblock %}
|
||||
|
||||
{% block extrajavascript %}
|
||||
<script src="{% static "activity/js/opener.js" %}"></script>
|
||||
<script src="{% static "js/autocomplete_model.js" %}"></script>
|
||||
<script>
|
||||
function remove_guest(guest_id) {
|
||||
$.ajax({
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from datetime import timedelta
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.urls import path
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from hashlib import md5
|
||||
@ -18,14 +18,15 @@ from django.views import View
|
||||
from django.views.decorators.cache import cache_page
|
||||
from django.views.generic import DetailView, TemplateView, UpdateView
|
||||
from django.views.generic.list import ListView
|
||||
from django_tables2.views import MultiTableMixin
|
||||
from django_tables2.views import MultiTableMixin, SingleTableMixin
|
||||
from api.viewsets import is_regex
|
||||
from note.models import Alias, NoteSpecial, NoteUser
|
||||
from permission.backends import PermissionBackend
|
||||
from permission.views import ProtectQuerysetMixin, ProtectedCreateView
|
||||
|
||||
from .forms import ActivityForm, GuestForm
|
||||
from .models import Activity, Entry, Guest
|
||||
from .tables import ActivityTable, EntryTable, GuestTable
|
||||
from .models import Activity, Entry, Guest, Opener
|
||||
from .tables import ActivityTable, EntryTable, GuestTable, OpenerTable
|
||||
|
||||
|
||||
class ActivityCreateView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||
@ -63,19 +64,15 @@ class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin
|
||||
Displays all Activities, and classify if they are on-going or upcoming ones.
|
||||
"""
|
||||
model = Activity
|
||||
tables = [ActivityTable, ActivityTable]
|
||||
tables = [
|
||||
lambda data: ActivityTable(data, prefix="all-"),
|
||||
lambda data: ActivityTable(data, prefix="upcoming-"),
|
||||
]
|
||||
extra_context = {"title": _("Activities")}
|
||||
|
||||
def get_queryset(self, **kwargs):
|
||||
return super().get_queryset(**kwargs).distinct()
|
||||
|
||||
def get_tables(self):
|
||||
tables = super().get_tables()
|
||||
|
||||
tables[0].prefix = "all-"
|
||||
tables[1].prefix = "upcoming-"
|
||||
return tables
|
||||
|
||||
def get_tables_data(self):
|
||||
# first table = all activities, second table = upcoming
|
||||
return [
|
||||
@ -99,7 +96,7 @@ class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin
|
||||
return context
|
||||
|
||||
|
||||
class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, DetailView):
|
||||
"""
|
||||
Shows details about one activity. Add guest to context
|
||||
"""
|
||||
@ -107,15 +104,40 @@ class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
context_object_name = "activity"
|
||||
extra_context = {"title": _("Activity detail")}
|
||||
|
||||
tables = [
|
||||
lambda data: GuestTable(data, prefix="guests-"),
|
||||
lambda data: OpenerTable(data, prefix="opener-"),
|
||||
]
|
||||
|
||||
def get_tables_data(self):
|
||||
return [
|
||||
Guest.objects.filter(activity=self.object)
|
||||
.filter(PermissionBackend.filter_queryset(self.request, Guest, "view")),
|
||||
self.object.opener.filter(activity=self.object)
|
||||
.filter(PermissionBackend.filter_queryset(self.request, Opener, "view")),
|
||||
]
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data()
|
||||
|
||||
table = GuestTable(data=Guest.objects.filter(activity=self.object)
|
||||
.filter(PermissionBackend.filter_queryset(self.request, Guest, "view")))
|
||||
context["guests"] = table
|
||||
tables = context["tables"]
|
||||
for name, table in zip(["guests", "opener"], tables):
|
||||
context[name] = table
|
||||
|
||||
context["activity_started"] = timezone.now() > timezone.localtime(self.object.date_start)
|
||||
|
||||
context["widget"] = {
|
||||
"name": "opener",
|
||||
"resetable": True,
|
||||
"attrs": {
|
||||
"class": "autocomplete form-control",
|
||||
"id": "opener",
|
||||
"api_url": "/api/note/alias/?note__polymorphic_ctype__model=noteuser",
|
||||
"name_field": "name",
|
||||
"placeholder": ""
|
||||
}
|
||||
}
|
||||
|
||||
return context
|
||||
|
||||
|
||||
@ -172,12 +194,14 @@ class ActivityInviteView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||
return reverse_lazy('activity:activity_detail', kwargs={"pk": self.kwargs["pk"]})
|
||||
|
||||
|
||||
class ActivityEntryView(LoginRequiredMixin, TemplateView):
|
||||
class ActivityEntryView(LoginRequiredMixin, SingleTableMixin, TemplateView):
|
||||
"""
|
||||
Manages entry to an activity
|
||||
"""
|
||||
template_name = "activity/activity_entry.html"
|
||||
|
||||
table_class = EntryTable
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
"""
|
||||
Don't display the entry interface if the user has no right to see it (no right to add an entry for itself),
|
||||
@ -212,13 +236,16 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
|
||||
|
||||
if "search" in self.request.GET and self.request.GET["search"]:
|
||||
pattern = self.request.GET["search"]
|
||||
if pattern[0] != "^":
|
||||
pattern = "^" + pattern
|
||||
|
||||
# Check if this is a valid regex. If not, we won't check regex
|
||||
valid_regex = is_regex(pattern)
|
||||
suffix = "__iregex" if valid_regex else "__istartswith"
|
||||
pattern = "^" + pattern if valid_regex and pattern[0] != "^" else pattern
|
||||
guest_qs = guest_qs.filter(
|
||||
Q(first_name__iregex=pattern)
|
||||
| Q(last_name__iregex=pattern)
|
||||
| Q(inviter__alias__name__iregex=pattern)
|
||||
| Q(inviter__alias__normalized_name__iregex=Alias.normalize(pattern))
|
||||
Q(**{f"first_name{suffix}": pattern})
|
||||
| Q(**{f"last_name{suffix}": pattern})
|
||||
| Q(**{f"inviter__alias__name{suffix}": pattern})
|
||||
| Q(**{f"inviter__alias__normalized_name{suffix}": Alias.normalize(pattern)})
|
||||
)
|
||||
else:
|
||||
guest_qs = guest_qs.none()
|
||||
@ -238,23 +265,26 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
|
||||
# Keep only users that have a note
|
||||
note_qs = note_qs.filter(note__noteuser__isnull=False)
|
||||
|
||||
# Keep only members
|
||||
# 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(),
|
||||
)
|
||||
note__noteuser__user__memberships__date_end__gte=timezone.now()).exclude(note__inactivity_reason='forced')
|
||||
|
||||
# Filter with permission backend
|
||||
note_qs = note_qs.filter(PermissionBackend.filter_queryset(self.request, Alias, "view"))
|
||||
|
||||
if "search" in self.request.GET and self.request.GET["search"]:
|
||||
pattern = self.request.GET["search"]
|
||||
|
||||
# Check if this is a valid regex. If not, we won't check regex
|
||||
valid_regex = is_regex(pattern)
|
||||
suffix = "__iregex" if valid_regex else "__icontains"
|
||||
note_qs = note_qs.filter(
|
||||
Q(note__noteuser__user__first_name__iregex=pattern)
|
||||
| Q(note__noteuser__user__last_name__iregex=pattern)
|
||||
| Q(name__iregex=pattern)
|
||||
| Q(normalized_name__iregex=Alias.normalize(pattern))
|
||||
Q(**{f"note__noteuser__user__first_name{suffix}": pattern})
|
||||
| Q(**{f"note__noteuser__user__last_name{suffix}": pattern})
|
||||
| Q(**{f"name{suffix}": pattern})
|
||||
| Q(**{f"normalized_name{suffix}": Alias.normalize(pattern)})
|
||||
)
|
||||
else:
|
||||
note_qs = note_qs.none()
|
||||
@ -266,15 +296,9 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
|
||||
if settings.DATABASES[note_qs.db]["ENGINE"] == 'django.db.backends.postgresql' else note_qs.distinct()[:20]
|
||||
return note_qs
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""
|
||||
Query the list of Guest and Note to the activity and add information to makes entry with JS.
|
||||
"""
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
def get_table_data(self):
|
||||
activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request, Activity, "view"))\
|
||||
.distinct().get(pk=self.kwargs["pk"])
|
||||
context["activity"] = activity
|
||||
|
||||
matched = []
|
||||
|
||||
@ -287,8 +311,17 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
|
||||
note.activity = activity
|
||||
matched.append(note)
|
||||
|
||||
table = EntryTable(data=matched)
|
||||
context["table"] = table
|
||||
return matched
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""
|
||||
Query the list of Guest and Note to the activity and add information to makes entry with JS.
|
||||
"""
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request, Activity, "view"))\
|
||||
.distinct().get(pk=self.kwargs["pk"])
|
||||
context["activity"] = activity
|
||||
|
||||
context["entries"] = Entry.objects.filter(activity=activity)
|
||||
|
||||
@ -296,7 +329,7 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
|
||||
context["noteuser_ctype"] = ContentType.objects.get_for_model(NoteUser).pk
|
||||
context["notespecial_ctype"] = ContentType.objects.get_for_model(NoteSpecial).pk
|
||||
|
||||
activities_open = Activity.objects.filter(open=True).filter(
|
||||
activities_open = Activity.objects.filter(open=True, activity_type__manage_entries=True).filter(
|
||||
PermissionBackend.filter_queryset(self.request, Activity, "view")).distinct().all()
|
||||
context["activities_open"] = [a for a in activities_open
|
||||
if PermissionBackend.check_perm(self.request,
|
||||
@ -330,8 +363,8 @@ X-WR-CALNAME:Kfet Calendar
|
||||
NAME:Kfet Calendar
|
||||
CALSCALE:GREGORIAN
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:Europe/Berlin
|
||||
X-LIC-LOCATION:Europe/Berlin
|
||||
TZID:Europe/Paris
|
||||
X-LIC-LOCATION:Europe/Paris
|
||||
BEGIN:DAYLIGHT
|
||||
TZOFFSETFROM:+0100
|
||||
TZOFFSETTO:+0200
|
||||
@ -353,10 +386,10 @@ END:VTIMEZONE
|
||||
DTSTAMP:{"{:%Y%m%dT%H%M%S}".format(activity.date_start)}Z
|
||||
UID:{md5((activity.name + "$" + str(activity.id) + str(activity.date_start)).encode("UTF-8")).hexdigest()}
|
||||
SUMMARY;CHARSET=UTF-8:{self.multilines(activity.name, 75, 22)}
|
||||
DTSTART;TZID=Europe/Berlin:{"{:%Y%m%dT%H%M%S}".format(activity.date_start)}
|
||||
DTEND;TZID=Europe/Berlin:{"{:%Y%m%dT%H%M%S}".format(activity.date_end)}
|
||||
DTSTART:{"{:%Y%m%dT%H%M%S}Z".format(activity.date_start)}
|
||||
DTEND:{"{:%Y%m%dT%H%M%S}Z".format(activity.date_end)}
|
||||
LOCATION:{self.multilines(activity.location, 75, 9) if activity.location else "Kfet"}
|
||||
DESCRIPTION;CHARSET=UTF-8:""" + self.multilines(activity.description.replace("\n", "\\n"), 75, 26) + """
|
||||
DESCRIPTION;CHARSET=UTF-8:""" + self.multilines(activity.description.replace("\n", "\\n"), 75, 26) + f"""
|
||||
-- {activity.organizer.name}
|
||||
END:VEVENT
|
||||
"""
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
default_app_config = 'api.apps.APIConfig'
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
42
apps/api/filters.py
Normal file
42
apps/api/filters.py
Normal file
@ -0,0 +1,42 @@
|
||||
import re
|
||||
from functools import lru_cache
|
||||
|
||||
from rest_framework.filters import SearchFilter
|
||||
|
||||
|
||||
class RegexSafeSearchFilter(SearchFilter):
|
||||
@lru_cache
|
||||
def validate_regex(self, search_term) -> bool:
|
||||
try:
|
||||
re.compile(search_term)
|
||||
return True
|
||||
except re.error:
|
||||
return False
|
||||
|
||||
def get_search_fields(self, view, request):
|
||||
"""
|
||||
Ensure that given regex are valid.
|
||||
If not, we consider that the user is trying to search by substring.
|
||||
"""
|
||||
search_fields = super().get_search_fields(view, request)
|
||||
search_terms = self.get_search_terms(request)
|
||||
|
||||
for search_term in search_terms:
|
||||
if not self.validate_regex(search_term):
|
||||
# Invalid regex. We assume we don't query by regex but by substring.
|
||||
search_fields = [f.replace('$', '') for f in search_fields]
|
||||
break
|
||||
|
||||
return search_fields
|
||||
|
||||
def get_search_terms(self, request):
|
||||
"""
|
||||
Ensure that search field is a valid regex query. If not, we remove extra characters.
|
||||
"""
|
||||
terms = super().get_search_terms(request)
|
||||
if not all(self.validate_regex(term) for term in terms):
|
||||
# Invalid regex. If a ^ is prefixed to the search term, we remove it.
|
||||
terms = [term[1:] if term[0] == '^' else term for term in terms]
|
||||
# Same for dollars.
|
||||
terms = [term[:-1] if term[-1] == '$' else term for term in terms]
|
||||
return terms
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import json
|
||||
@ -12,11 +12,12 @@ from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models.fields.files import ImageFieldFile
|
||||
from django.test import TestCase
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from phonenumbers import PhoneNumber
|
||||
from rest_framework.filters import OrderingFilter
|
||||
from api.filters import RegexSafeSearchFilter
|
||||
from member.models import Membership, Club
|
||||
from note.models import NoteClub, NoteUser, Alias, Note
|
||||
from permission.models import PermissionMask, Permission, Role
|
||||
from phonenumbers import PhoneNumber
|
||||
from rest_framework.filters import SearchFilter, OrderingFilter
|
||||
|
||||
from .viewsets import ContentTypeViewSet, UserViewSet
|
||||
|
||||
@ -87,7 +88,7 @@ class TestAPI(TestCase):
|
||||
resp = self.client.get(url + f"?ordering=-{field}")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
if SearchFilter in backends:
|
||||
if RegexSafeSearchFilter in backends:
|
||||
# Basic search
|
||||
for field in viewset.search_fields:
|
||||
obj = self.fix_note_object(obj, field)
|
||||
|
@ -1,8 +1,9 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.conf import settings
|
||||
from django.conf.urls import url, include
|
||||
from django.conf.urls import include
|
||||
from django.urls import re_path
|
||||
from rest_framework import routers
|
||||
|
||||
from .views import UserInformationView
|
||||
@ -46,12 +47,16 @@ if "wei" in settings.INSTALLED_APPS:
|
||||
from wei.api.urls import register_wei_urls
|
||||
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'
|
||||
|
||||
# Wire up our API using automatic URL routing.
|
||||
# Additionally, we include login URLs for the browsable API.
|
||||
urlpatterns = [
|
||||
url('^', include(router.urls)),
|
||||
url('^me/', UserInformationView.as_view()),
|
||||
url('^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
|
||||
re_path('^', include(router.urls)),
|
||||
re_path('^me/', UserInformationView.as_view()),
|
||||
re_path('^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
|
||||
]
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
|
@ -1,19 +1,29 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import re
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from django.db.models import Q
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from rest_framework.filters import SearchFilter
|
||||
from rest_framework.viewsets import ReadOnlyModelViewSet, ModelViewSet
|
||||
from permission.backends import PermissionBackend
|
||||
from note.models import Alias
|
||||
|
||||
from .filters import RegexSafeSearchFilter
|
||||
from .serializers import UserSerializer, ContentTypeSerializer
|
||||
|
||||
|
||||
def is_regex(pattern):
|
||||
try:
|
||||
re.compile(pattern)
|
||||
return True
|
||||
except (re.error, TypeError):
|
||||
return False
|
||||
|
||||
|
||||
class ReadProtectedModelViewSet(ModelViewSet):
|
||||
"""
|
||||
Protect a ModelViewSet by filtering the objects that the user cannot see.
|
||||
@ -60,34 +70,38 @@ class UserViewSet(ReadProtectedModelViewSet):
|
||||
|
||||
if "search" in self.request.GET:
|
||||
pattern = self.request.GET["search"]
|
||||
# Check if this is a valid regex. If not, we won't check regex
|
||||
valid_regex = is_regex(pattern)
|
||||
suffix = "__iregex" if valid_regex else "__istartswith"
|
||||
prefix = "^" if valid_regex else ""
|
||||
|
||||
# Filter with different rules
|
||||
# We use union-all to keep each filter rule sorted in result
|
||||
queryset = queryset.filter(
|
||||
# Match without normalization
|
||||
note__alias__name__iregex="^" + pattern
|
||||
Q(**{f"note__alias__name{suffix}": prefix + pattern})
|
||||
).union(
|
||||
queryset.filter(
|
||||
# Match with normalization
|
||||
Q(note__alias__normalized_name__iregex="^" + Alias.normalize(pattern))
|
||||
& ~Q(note__alias__name__iregex="^" + pattern)
|
||||
Q(**{f"note__alias__normalized_name{suffix}": prefix + Alias.normalize(pattern)})
|
||||
& ~Q(**{f"note__alias__name{suffix}": prefix + pattern})
|
||||
),
|
||||
all=True,
|
||||
).union(
|
||||
queryset.filter(
|
||||
# Match on lower pattern
|
||||
Q(note__alias__normalized_name__iregex="^" + pattern.lower())
|
||||
& ~Q(note__alias__normalized_name__iregex="^" + Alias.normalize(pattern))
|
||||
& ~Q(note__alias__name__iregex="^" + pattern)
|
||||
Q(**{f"note__alias__normalized_name{suffix}": prefix + pattern.lower()})
|
||||
& ~Q(**{f"note__alias__normalized_name{suffix}": prefix + Alias.normalize(pattern)})
|
||||
& ~Q(**{f"note__alias__name{suffix}": prefix + pattern})
|
||||
),
|
||||
all=True,
|
||||
).union(
|
||||
queryset.filter(
|
||||
# Match on firstname or lastname
|
||||
(Q(last_name__iregex="^" + pattern) | Q(first_name__iregex="^" + pattern))
|
||||
& ~Q(note__alias__normalized_name__iregex="^" + pattern.lower())
|
||||
& ~Q(note__alias__normalized_name__iregex="^" + Alias.normalize(pattern))
|
||||
& ~Q(note__alias__name__iregex="^" + pattern)
|
||||
(Q(**{f"last_name{suffix}": prefix + pattern}) | Q(**{f"first_name{suffix}": prefix + pattern}))
|
||||
& ~Q(**{f"note__alias__normalized_name{suffix}": prefix + pattern.lower()})
|
||||
& ~Q(**{f"note__alias__normalized_name{suffix}": prefix + Alias.normalize(pattern)})
|
||||
& ~Q(**{f"note__alias__name{suffix}": prefix + pattern})
|
||||
),
|
||||
all=True,
|
||||
)
|
||||
@ -107,6 +121,6 @@ class ContentTypeViewSet(ReadOnlyModelViewSet):
|
||||
"""
|
||||
queryset = ContentType.objects.order_by('id')
|
||||
serializer_class = ContentTypeSerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||
filterset_fields = ['id', 'app_label', 'model', ]
|
||||
search_fields = ['$app_label', '$model', ]
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.contrib import admin
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from rest_framework import serializers
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from .views import AllergenViewSet, BasicFoodViewSet, QRCodeViewSet, TransformedFoodViewSet
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from api.viewsets import ReadProtectedModelViewSet
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from random import shuffle
|
||||
@ -7,11 +7,12 @@ from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils import timezone
|
||||
from member.models import Club
|
||||
from note_kfet.inputs import Autocomplete, DateTimePickerInput
|
||||
from bootstrap_datepicker_plus.widgets import DateTimePickerInput
|
||||
from note_kfet.inputs import Autocomplete
|
||||
from note_kfet.middlewares import get_current_request
|
||||
from permission.backends import PermissionBackend
|
||||
|
||||
from .models import BasicFood, QRCode, TransformedFood, Food
|
||||
from .models import BasicFood, QRCode, TransformedFood
|
||||
|
||||
|
||||
class AddIngredientForms(forms.ModelForm):
|
||||
@ -20,11 +21,19 @@ class AddIngredientForms(forms.ModelForm):
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['ingredient'].queryset = self.fields['ingredient'].queryset.filter(is_ready=False, is_active=True, was_eaten=False)
|
||||
self.fields['ingredient'].queryset = self.fields['ingredient'].queryset.filter(
|
||||
polymorphic_ctype__model='transformedfood',
|
||||
is_ready=False,
|
||||
is_active=True,
|
||||
was_eaten=False,
|
||||
)
|
||||
# Caution, the logic is inverted here, we flip the logic on saving in AddIngredientView
|
||||
self.fields['is_active'].initial = True
|
||||
self.fields['is_active'].label = _("Fully used")
|
||||
|
||||
class Meta:
|
||||
model = TransformedFood
|
||||
fields = ('ingredient',)
|
||||
fields = ('ingredient', 'is_active')
|
||||
|
||||
|
||||
class BasicFoodForms(forms.ModelForm):
|
||||
@ -38,7 +47,7 @@ class BasicFoodForms(forms.ModelForm):
|
||||
self.fields['owner'].required = True
|
||||
|
||||
# Some example
|
||||
self.fields['name'].widget.attrs.update({"placeholder": _("pasta")})
|
||||
self.fields['name'].widget.attrs.update({"placeholder": _("Pasta METRO 5kg")})
|
||||
clubs = list(Club.objects.filter(PermissionBackend.filter_queryset(get_current_request(), Club, "change")).all())
|
||||
shuffle(clubs)
|
||||
self.fields['owner'].widget.attrs["placeholder"] = ", ".join(club.name for club in clubs[:4]) + ", ..."
|
||||
@ -61,7 +70,11 @@ class QRCodeForms(forms.ModelForm):
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['food_container'].queryset = self.fields['food_container'].queryset.filter(is_ready=False)
|
||||
self.fields['food_container'].queryset = self.fields['food_container'].queryset.filter(
|
||||
is_active=True,
|
||||
was_eaten=False,
|
||||
polymorphic_ctype__model='transformedfood',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = QRCode
|
||||
@ -84,7 +97,7 @@ class TransformedFoodForms(forms.ModelForm):
|
||||
self.fields['was_eaten'].initial = False
|
||||
|
||||
# Some example
|
||||
self.fields['name'].widget.attrs.update({"placeholder": _("lasagna")})
|
||||
self.fields['name'].widget.attrs.update({"placeholder": _("Lasagna")})
|
||||
clubs = list(Club.objects.filter(PermissionBackend.filter_queryset(get_current_request(), Club, "change")).all())
|
||||
shuffle(clubs)
|
||||
self.fields['owner'].widget.attrs["placeholder"] = ", ".join(club.name for club in clubs[:4]) + ", ..."
|
||||
@ -99,13 +112,3 @@ class TransformedFoodForms(forms.ModelForm):
|
||||
),
|
||||
'creation_date': DateTimePickerInput(),
|
||||
}
|
||||
|
||||
class FoodForms(forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['was_eaten'].initial = True
|
||||
|
||||
class Meta:
|
||||
model = Food
|
||||
fields = ('was_eaten',)
|
||||
|
||||
|
20
apps/food/migrations/0005_alter_food_polymorphic_ctype.py
Normal file
20
apps/food/migrations/0005_alter_food_polymorphic_ctype.py
Normal file
@ -0,0 +1,20 @@
|
||||
# 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'),
|
||||
),
|
||||
]
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from datetime import timedelta
|
||||
@ -94,7 +94,6 @@ class Food(PolymorphicModel):
|
||||
verbose_name=_('is active'),
|
||||
)
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@ -176,6 +175,11 @@ class TransformedFood(Food):
|
||||
default=timedelta(days=3),
|
||||
)
|
||||
|
||||
@transaction.atomic
|
||||
def archive(self):
|
||||
# When a meal are archived, if it was eaten, update ingredient fully used for this meal
|
||||
raise NotImplementedError
|
||||
|
||||
@transaction.atomic
|
||||
def update_allergens(self):
|
||||
# When allergens are changed, simply update the parents' allergens
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import django_tables2 as tables
|
||||
|
@ -7,21 +7,31 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
{% block content %}
|
||||
<div class="card bg-white mb-3">
|
||||
<h3 class="card-header text-center">
|
||||
HTML not finished <br>
|
||||
{{ title }}
|
||||
{{ title }} {{ food.name }}
|
||||
</h3>
|
||||
<div class="card-body">
|
||||
<p>{% trans 'Name' %} : {{ food.name }}</p>
|
||||
<p>{% trans 'Owner' %} : {{ food.owner }}</p>
|
||||
<p>{% trans 'Arrival date' %} : {{ food.arrival_date }}</p>
|
||||
<p>{% trans 'Expiry date' %} : {{ food.expiry_date }}</p>
|
||||
<p>{% trans 'Allergens' %} :</p>
|
||||
<ul>
|
||||
<li><p>{% trans 'Owner' %} : {{ food.owner }}</p></li>
|
||||
<li><p>{% trans 'Arrival date' %} : {{ food.arrival_date }}</p></li>
|
||||
<li><p>{% trans 'Expiry date' %} : {{ food.expiry_date }} ({{ food.date_type }})</p></li>
|
||||
<li>{% trans 'Allergens' %} :</li>
|
||||
<ul>
|
||||
{% for allergen in food.allergens.iterator %}
|
||||
<li>{{ allergen.name }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<a href="{% url "food:basic_update" pk=food.pk %}">{% trans 'Update' %}</a>
|
||||
<p>
|
||||
<li><p>{% trans 'Active' %} : {{ food.is_active }}<p></li>
|
||||
<li><p>{% trans 'Eaten' %} : {{ food.was_eaten }}<p></li>
|
||||
</ul>
|
||||
{% if can_update %}
|
||||
<a class="btn btn-sm btn-warning" href="{% url "food:basic_update" pk=food.pk %}">{% trans 'Update' %}</a>
|
||||
{% endif %}
|
||||
{% if can_add_ingredient %}
|
||||
<a class="btn btn-sm btn-success" href="{% url "food:add_ingredient" pk=food.pk %}">
|
||||
{% trans 'Add to a meal' %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -1,21 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% comment %}
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
{% endcomment %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card bg-white mb-3">
|
||||
<h3 class="card-header text-center">
|
||||
HTML not finished <br>
|
||||
{{ title }}
|
||||
</h3>
|
||||
<div class="row">
|
||||
<div class="col-xl-12">
|
||||
<div class="btn-group btn-block">
|
||||
<a href="{% url "food:basic_create" %}" class="btn btn-sm btn-outline-primary">Basic</a>
|
||||
<a href="{% url "food:transformed_create" %}" class="btn btn-sm btn-outline-primary">Transformed</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -2,23 +2,54 @@
|
||||
{% comment %}
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
{% endcomment %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load i18n crispy_forms_tags %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card bg-white mb-3">
|
||||
<h3 class="card-header text-center">
|
||||
HTML not finished <br>
|
||||
{{ title }}
|
||||
</h3>
|
||||
<div class="card-body" id="form">
|
||||
<a class="btn btn-sm btn-success" href="{% url "food:qrcode_basic_create" slug=slug %}" data-turbolinks="false">
|
||||
New basic food
|
||||
<a class="btn btn-sm btn-success" href="{% url "food:qrcode_basic_create" slug=slug %}">
|
||||
{% trans 'New basic food' %}
|
||||
</a>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
<button class="btn btn-primary" type="submit">{% trans "Submit" %}</button>
|
||||
</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>
|
||||
{% endblock %}
|
||||
|
@ -7,14 +7,14 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
{% block content %}
|
||||
<div class="card bg-white mb-3">
|
||||
<h3 class="card-header text-center">
|
||||
HTML finished <br>
|
||||
{{ title }}
|
||||
{{ title }} {% trans 'number' %} {{ qrcode.qr_code_number }}
|
||||
</h3>
|
||||
<div class="card-body">
|
||||
<p>{% trans 'QR-code number' %} : {{ qrcode.qr_code_number }}</p>
|
||||
<p>{% trans 'Name' %} : {{ qrcode.food_container.name }}</p>
|
||||
<p>{% trans 'Owner' %} : {{ qrcode.food_container.owner }}</p>
|
||||
<p>{% trans 'Expiry date' %} : {{ qrcode.food_container.expiry_date }}</p>
|
||||
<ul>
|
||||
<li><p>{% trans 'Name' %} : {{ qrcode.food_container.name }}</p></li>
|
||||
<li><p>{% trans 'Owner' %} : {{ qrcode.food_container.owner }}</p></li>
|
||||
<li><p>{% trans 'Expiry date' %} : {{ qrcode.food_container.expiry_date }}</p></li>
|
||||
</ul>
|
||||
{% if qrcode.food_container.polymorphic_ctype.model == 'basicfood' and can_update_basic %}
|
||||
<a class="btn btn-sm btn-warning" href="{% url "food:basic_update" pk=qrcode.food_container.pk %}" data-turbolinks="false">
|
||||
{% trans 'Update' %}
|
||||
@ -24,9 +24,14 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
{% trans 'Update' %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if can_view_detail %}
|
||||
<a class="btn btn-sm btn-primary" href="{% url "food:food_view" pk=qrcode.food_container.pk %}">
|
||||
{% trans 'View details' %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if can_add_ingredient %}
|
||||
<a class="btn btn-sm btn-success" href="{% url "food:add_ingredient" pk=qrcode.food_container.pk %}">
|
||||
{% trans 'Add the ingredient' %}
|
||||
{% trans 'Add to a meal' %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
@ -7,27 +7,45 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
{% block content %}
|
||||
<div class="card bg-white mb-3">
|
||||
<h3 class="card-header text-center">
|
||||
HTML not finished <br>
|
||||
{{ title }}
|
||||
{{ title }} {{ food.name }}
|
||||
</h3>
|
||||
<div class="card-body">
|
||||
<p>{% trans 'Name' %} : {{ food.name }}</p>
|
||||
<p>{% trans 'Owner' %} : {{ food.owner }}</p>
|
||||
<p>{% trans 'Creation date' %} : {{ food.creation_date }}</p>
|
||||
<p>{% trans 'Expiry date' %} : {{ food.expiry_date }}</p>
|
||||
<p>{% trans 'Allergens' %} :</p>
|
||||
<ul>
|
||||
<li><p>{% trans 'Owner' %} : {{ food.owner }}</p></li>
|
||||
{% if can_see_ready %}
|
||||
<li><p>{% trans 'Ready' %} : {{ food.is_ready }}</p></li>
|
||||
{% endif %}
|
||||
<li><p>{% trans 'Creation date' %} : {{ food.creation_date }}</p></li>
|
||||
<li><p>{% trans 'Expiry date' %} : {{ food.expiry_date }}</p></li>
|
||||
<li>{% trans 'Allergens' %} :</li>
|
||||
<ul>
|
||||
{% for allergen in food.allergens.iterator %}
|
||||
<li>{{ allergen.name }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<p>{% trans 'Ingredients' %} :</p>
|
||||
<p>
|
||||
<li>{% trans 'Ingredients' %} :</li>
|
||||
<ul>
|
||||
{% for ingredient in food.ingredient.iterator %}
|
||||
<li><a href="{% url "food:food_view" pk=ingredient.pk %}">{{ ingredient.name }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<a href="{% url "food:transformed_update" pk=food.pk %}">{% trans 'Update' %}</a>
|
||||
<p>
|
||||
<li><p>{% trans 'Shelf life' %} : {{ food.shelf_life }}</p></li>
|
||||
<li><p>{% trans 'Ready' %} : {{ food.is_ready }}</p></li>
|
||||
<li><p>{% trans 'Active' %} : {{ food.is_active }}</p></li>
|
||||
<li><p>{% trans 'Eaten' %} : {{ food.was_eaten }}</p></li>
|
||||
</ul>
|
||||
{% if can_update %}
|
||||
<a class="btn btn-sm btn-warning" href="{% url "food:transformed_update" pk=food.pk %}">
|
||||
{% trans 'Update' %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if can_add_ingredient %}
|
||||
<a class="btn btn-sm btn-success" href="{% url "food:add_ingredient" pk=food.pk %}">
|
||||
{% trans 'Add to a meal' %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -7,7 +7,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
{% block content %}
|
||||
<div class="card bg-white mb-3">
|
||||
<h3 class="card-header text-center">
|
||||
HTML not finished <br>
|
||||
{{ title }}
|
||||
</h3>
|
||||
<div class="card-body" id="form">
|
@ -35,7 +35,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
{% if open.data %}
|
||||
{% render_table open %}
|
||||
{% else %}
|
||||
<div> class="card-body">
|
||||
<div class="card-body">
|
||||
<div class="alert alert-warning">
|
||||
{% trans "There is no free meal." %}
|
||||
</div>
|
||||
@ -50,7 +50,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
{% if table.data %}
|
||||
{% render_table table %}
|
||||
{% else %}
|
||||
<div> class="card-body">
|
||||
<div class="card-body">
|
||||
<div class="alert alert-warning">
|
||||
{% trans "There is no meal." %}
|
||||
</div>
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.urls import path
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.db import transaction
|
||||
@ -8,15 +8,13 @@ from django_tables2.views import MultiTableMixin
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils import timezone
|
||||
from django.views.generic import DetailView, UpdateView, TemplateView
|
||||
from django.views.generic import DetailView, UpdateView
|
||||
from django.views.generic.list import ListView
|
||||
from django.forms import HiddenInput
|
||||
from permission.backends import PermissionBackend
|
||||
from permission.views import ProtectQuerysetMixin, ProtectedCreateView
|
||||
from member.models import Club
|
||||
from note_kfet.middlewares import get_current_request
|
||||
|
||||
from .forms import AddIngredientForms, BasicFoodForms, QRCodeForms, TransformedFoodForms, FoodForms
|
||||
from .forms import AddIngredientForms, BasicFoodForms, QRCodeForms, TransformedFoodForms
|
||||
from .models import BasicFood, Food, QRCode, TransformedFood
|
||||
from .tables import TransformedFoodTable
|
||||
|
||||
@ -25,7 +23,6 @@ class AddIngredientView(ProtectQuerysetMixin, UpdateView):
|
||||
"""
|
||||
A view to add an ingredient
|
||||
"""
|
||||
# TO DO : ajouter un champ fully_used dans le form et changer was_eaten en conséquence + mieux filtrer les plat dispo avec des perms
|
||||
model = Food
|
||||
template_name = 'food/add_ingredient_form.html'
|
||||
extra_context = {"title": _("Add the ingredient")}
|
||||
@ -41,19 +38,22 @@ class AddIngredientView(ProtectQuerysetMixin, UpdateView):
|
||||
form.instance.creater = self.request.user
|
||||
food = Food.objects.get(pk=self.kwargs['pk'])
|
||||
add_ingredient_form = AddIngredientForms(data=self.request.POST)
|
||||
food_form = FoodForms(data=self.request.POST)
|
||||
if food.is_ready:
|
||||
form.add_error(None, _("The product is already prepared"))
|
||||
return self.form_invalid(form)
|
||||
if not add_ingredient_form.is_valid():
|
||||
return self.form_invalid(form)
|
||||
|
||||
# We flip logic ""fully used = not is_active""
|
||||
food.is_active = not food.is_active
|
||||
# Save the aliment and the allergens associed
|
||||
for transformed_pk in self.request.POST.getlist('ingredient'):
|
||||
transformed = TransformedFood.objects.get(pk=transformed_pk)
|
||||
if not transformed.is_ready:
|
||||
transformed.ingredient.add(food)
|
||||
transformed.update()
|
||||
food.save()
|
||||
|
||||
return HttpResponseRedirect(self.get_success_url())
|
||||
|
||||
def get_success_url(self, **kwargs):
|
||||
@ -67,7 +67,7 @@ class BasicFoodUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||
model = BasicFood
|
||||
form_class = BasicFoodForms
|
||||
template_name = 'food/basicfood_form.html'
|
||||
extra_context = {"title": _("Add a new aliment")}
|
||||
extra_context = {"title": _("Update an aliment")}
|
||||
|
||||
@transaction.atomic
|
||||
def form_valid(self, form):
|
||||
@ -86,36 +86,29 @@ class BasicFoodUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
form = context['form']
|
||||
# TO DO : Add perms here
|
||||
if 1==0:
|
||||
form.fields['is_active'].widget = HiddenInput()
|
||||
if 1==0:
|
||||
form.fields['was_eaten'].widget = HiddenInput()
|
||||
form.fields['is_active'].help_text = _("Uncheck if the food doesn't exist anymore")
|
||||
form.fields['was_eaten'].help_text = _("Check if the food has been entirely eaten")
|
||||
|
||||
return context
|
||||
|
||||
class FoodCreateView(ProtectQuerysetMixin, LoginRequiredMixin, TemplateView):
|
||||
"""
|
||||
A view to add a new aliment
|
||||
"""
|
||||
template_name = 'food/create_food_form.html'
|
||||
extra_context = {"title": _("Add a new aliment")}
|
||||
|
||||
|
||||
class FoodView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
"""
|
||||
A view to see a food
|
||||
"""
|
||||
model = Food
|
||||
extra_context = {"title": _("Details")}
|
||||
extra_context = {"title": _("Details of:")}
|
||||
context_object_name = "food"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
context["can_update"] = PermissionBackend.check_perm(self.request, "food.change_food")
|
||||
context["can_add_ingredient"] = PermissionBackend.check_perm(self.request, "food.change_transformedfood")
|
||||
return context
|
||||
|
||||
|
||||
class QRCodeBasicFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||
#####################################################################
|
||||
# TO DO
|
||||
# - this feature is very pratical for meat or fish, nevertheless we can implement this later
|
||||
# - fix picture save
|
||||
# - implement solution crop and convert image (reuse or recode ImageForm from members apps)
|
||||
#####################################################################
|
||||
@ -157,9 +150,19 @@ class QRCodeBasicFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||
return reverse('food:qrcode_view', kwargs={"slug": self.kwargs['slug']})
|
||||
|
||||
def get_sample_object(self):
|
||||
|
||||
# We choose a club which may work or BDE else
|
||||
owner_id = 1
|
||||
for membership in self.request.user.memberships.all():
|
||||
club_id = membership.club.id
|
||||
food = BasicFood(name="", expiry_date=timezone.now(), owner_id=club_id)
|
||||
if PermissionBackend.check_perm(self.request, "food.add_basicfood", food):
|
||||
owner_id = club_id
|
||||
|
||||
return BasicFood(
|
||||
name="",
|
||||
expiry_date=timezone.now(),
|
||||
owner_id=owner_id,
|
||||
)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
@ -170,6 +173,14 @@ class QRCodeBasicFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||
form.fields['is_active'].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
|
||||
|
||||
|
||||
@ -186,14 +197,15 @@ class QRCodeCreateView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||
qrcode = kwargs["slug"]
|
||||
if self.model.objects.filter(qr_code_number=qrcode).count() > 0:
|
||||
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:
|
||||
return super().get(*args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["slug"] = self.kwargs["slug"]
|
||||
|
||||
context["last_basic"] = BasicFood.objects.order_by('-pk').all()[:10]
|
||||
|
||||
return context
|
||||
|
||||
@transaction.atomic
|
||||
@ -210,7 +222,6 @@ class QRCodeCreateView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||
qrcode.save()
|
||||
qrcode.refresh_from_db()
|
||||
|
||||
qrcode.food_container.is_ready = True
|
||||
qrcode.food_container.save()
|
||||
|
||||
return super().form_valid(form)
|
||||
@ -222,6 +233,7 @@ class QRCodeCreateView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||
def get_sample_object(self):
|
||||
return QRCode(
|
||||
qr_code_number=self.kwargs["slug"],
|
||||
food_container_id=1
|
||||
)
|
||||
|
||||
|
||||
@ -243,11 +255,19 @@ class QRCodeView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
# TO DO : Add perms here
|
||||
context["can_update_basic"]=True
|
||||
context["can_update_transformed"]=True
|
||||
context["can_add_ingredient"] = True
|
||||
|
||||
qr_code_number = self.kwargs['slug']
|
||||
qrcode = self.model.objects.get(qr_code_number=qr_code_number)
|
||||
|
||||
model = qrcode.food_container.polymorphic_ctype.model
|
||||
|
||||
if model == "basicfood":
|
||||
context["can_update_basic"] = PermissionBackend.check_perm(self.request, "food.change_basicfood")
|
||||
context["can_view_detail"] = PermissionBackend.check_perm(self.request, "food.view_basicfood")
|
||||
if model == "transformedfood":
|
||||
context["can_update_transformed"] = PermissionBackend.check_perm(self.request, "food.change_transformedfood")
|
||||
context["can_view_detail"] = PermissionBackend.check_perm(self.request, "food.view_transformedfood")
|
||||
context["can_add_ingredient"] = PermissionBackend.check_perm(self.request, "food.change_transformedfood")
|
||||
return context
|
||||
|
||||
|
||||
@ -255,9 +275,8 @@ class TransformedFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||
"""
|
||||
A view to add a tranformed food
|
||||
"""
|
||||
# TO DO : fix the "NotImplementedError" (╯°□°)╯︵ ┻━┻ ...
|
||||
model = TransformedFood
|
||||
template_name = 'food/transformed_food_form.html'
|
||||
template_name = 'food/transformedfood_form.html'
|
||||
form_class = TransformedFoodForms
|
||||
extra_context = {"title": _("Add a new meal")}
|
||||
|
||||
@ -285,6 +304,26 @@ class TransformedFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||
self.object.refresh_from_db()
|
||||
return reverse('food:food_view', kwargs={"pk": self.object.pk})
|
||||
|
||||
def get_sample_object(self):
|
||||
# We choose a club which may work or BDE else
|
||||
owner_id = 1
|
||||
for membership in self.request.user.memberships.all():
|
||||
club_id = membership.club.id
|
||||
food = TransformedFood(name="",
|
||||
creation_date=timezone.now(),
|
||||
expiry_date=timezone.now(),
|
||||
owner_id=club_id)
|
||||
if PermissionBackend.check_perm(self.request, "food.add_transformedfood", food):
|
||||
owner_id = club_id
|
||||
break
|
||||
|
||||
return TransformedFood(
|
||||
name="",
|
||||
owner_id=owner_id,
|
||||
creation_date=timezone.now(),
|
||||
expiry_date=timezone.now(),
|
||||
)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
@ -293,10 +332,6 @@ class TransformedFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||
form.fields['is_active'].widget = HiddenInput()
|
||||
form.fields['is_ready'].widget = HiddenInput()
|
||||
form.fields['was_eaten'].widget = HiddenInput()
|
||||
|
||||
# Field shelf life is only display for authorized user
|
||||
# TO DO : Add permission here
|
||||
if not True:
|
||||
form.fields['shelf_life'].widget = HiddenInput()
|
||||
|
||||
return context
|
||||
@ -307,9 +342,9 @@ class TransformedFoodUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, Update
|
||||
A view to update transformed product
|
||||
"""
|
||||
model = TransformedFood
|
||||
template_name = 'food/transformed_food_form.html'
|
||||
template_name = 'food/transformedfood_form.html'
|
||||
form_class = TransformedFoodForms
|
||||
extra_context = {'title' : _('Update meal')}
|
||||
extra_context = {'title': _('Update a meal')}
|
||||
|
||||
@transaction.atomic
|
||||
def form_valid(self, form):
|
||||
@ -328,16 +363,9 @@ class TransformedFoodUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, Update
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
form = context['form']
|
||||
|
||||
fields = ['is_active','is_ready','was_eaten','shelf_life']
|
||||
# TO DO : Add permissions here
|
||||
permissions = [True]*len(fields)
|
||||
for i in range(len(fields)):
|
||||
if not permissions[i] : form[fields[i]].widget = HiddenInput()
|
||||
return context
|
||||
|
||||
|
||||
class TransformedListView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, ListView):
|
||||
"""
|
||||
Displays ready TransformedFood
|
||||
@ -374,8 +402,18 @@ class TransformedListView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMi
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
# context["can_create_meal"] = PermissionBackend.check_perm(self.request, "food.add_transformedfood", TransformedFood(creation_date = timezone.now(), name = "", expiry_date = timezone.now(), owner = )) <- défi prendre un club qui fonctionne (s'il existe) pour l'utilisateur
|
||||
context["can_create_meal"] = True
|
||||
# We choose a club which should work
|
||||
for membership in self.request.user.memberships.all():
|
||||
club_id = membership.club.id
|
||||
food = TransformedFood(
|
||||
name="",
|
||||
owner_id=club_id,
|
||||
creation_date=timezone.now(),
|
||||
expiry_date=timezone.now(),
|
||||
)
|
||||
if PermissionBackend.check_perm(self.request, "food.add_transformedfood", food):
|
||||
context['can_create_meal'] = True
|
||||
break
|
||||
|
||||
tables = context["tables"]
|
||||
for name, table in zip(["table", "open", "served"], tables):
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
default_app_config = 'logs.apps.LogsConfig'
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from rest_framework import serializers
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from .views import ChangelogViewSet
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.conf import settings
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
@ -56,13 +56,13 @@ def save_object(sender, instance, **kwargs):
|
||||
# noinspection PyProtectedMember
|
||||
previous = instance._previous
|
||||
|
||||
# Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP
|
||||
# Si un⋅e utilisateur⋅rice est connecté⋅e, on récupère l'utilisateur⋅rice courant⋅e ainsi que son adresse IP
|
||||
request = get_current_request()
|
||||
|
||||
if request is None:
|
||||
# Si la modification n'a pas été faite via le client Web, on suppose que c'est du à `manage.py`
|
||||
# On récupère alors l'utilisateur·trice connecté·e à la VM, et on récupère la note associée
|
||||
# IMPORTANT : l'utilisateur dans la VM doit être un des alias note du respo info
|
||||
# IMPORTANT : l'utilisateur⋅rice dans la VM doit être un des alias note du respo info
|
||||
ip = "127.0.0.1"
|
||||
username = Alias.normalize(getpass.getuser())
|
||||
note = NoteUser.objects.filter(alias__normalized_name=username)
|
||||
@ -134,13 +134,13 @@ def delete_object(sender, instance, **kwargs):
|
||||
if instance._meta.label_lower in EXCLUDED or hasattr(instance, "_no_signal"):
|
||||
return
|
||||
|
||||
# Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP
|
||||
# Si un⋅e utilisateur⋅rice est connecté⋅e, on récupère l'utilisateur⋅rice courant⋅e ainsi que son adresse IP
|
||||
request = get_current_request()
|
||||
|
||||
if request is None:
|
||||
# Si la modification n'a pas été faite via le client Web, on suppose que c'est du à `manage.py`
|
||||
# On récupère alors l'utilisateur·trice connecté·e à la VM, et on récupère la note associée
|
||||
# IMPORTANT : l'utilisateur dans la VM doit être un des alias note du respo info
|
||||
# IMPORTANT : l'utilisateur⋅rice dans la VM doit être un des alias note du respo info
|
||||
ip = "127.0.0.1"
|
||||
username = Alias.normalize(getpass.getuser())
|
||||
note = NoteUser.objects.filter(alias__normalized_name=username)
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
default_app_config = 'member.apps.MemberConfig'
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.contrib import admin
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from rest_framework import serializers
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from .views import ProfileViewSet, ClubViewSet, MembershipViewSet
|
||||
|
@ -1,8 +1,9 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework.filters import OrderingFilter, SearchFilter
|
||||
from rest_framework.filters import OrderingFilter
|
||||
from api.filters import RegexSafeSearchFilter
|
||||
from api.viewsets import ReadProtectedModelViewSet
|
||||
|
||||
from .serializers import ProfileSerializer, ClubSerializer, MembershipSerializer
|
||||
@ -17,7 +18,7 @@ class ProfileViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
queryset = Profile.objects.order_by('id')
|
||||
serializer_class = ProfileSerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||
filterset_fields = ['user', 'user__first_name', 'user__last_name', 'user__username', 'user__email',
|
||||
'user__note__alias__name', 'user__note__alias__normalized_name', 'phone_number', "section",
|
||||
'department', 'promotion', 'address', 'paid', 'ml_events_registration', 'ml_sport_registration',
|
||||
@ -34,7 +35,7 @@ class ClubViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
queryset = Club.objects.order_by('id')
|
||||
serializer_class = ClubSerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||
filterset_fields = ['name', 'email', 'note__alias__name', 'note__alias__normalized_name', 'parent_club',
|
||||
'parent_club__name', 'require_memberships', 'membership_fee_paid', 'membership_fee_unpaid',
|
||||
'membership_duration', 'membership_start', 'membership_end', ]
|
||||
@ -49,7 +50,7 @@ class MembershipViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
queryset = Membership.objects.order_by('id')
|
||||
serializer_class = MembershipSerializer
|
||||
filter_backends = [DjangoFilterBackend, OrderingFilter, SearchFilter]
|
||||
filter_backends = [DjangoFilterBackend, OrderingFilter, RegexSafeSearchFilter]
|
||||
filterset_fields = ['club__name', 'club__email', 'club__note__alias__name', 'club__note__alias__normalized_name',
|
||||
'user__username', 'user__last_name', 'user__first_name', 'user__email',
|
||||
'user__note__alias__name', 'user__note__alias__normalized_name',
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from cas_server.auth import DjangoAuthUser # pragma: no cover
|
||||
|
@ -1,9 +1,9 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import io
|
||||
|
||||
from PIL import Image, ImageSequence
|
||||
from bootstrap_datepicker_plus.widgets import DatePickerInput
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.forms import AuthenticationForm
|
||||
@ -13,8 +13,9 @@ from django.forms import CheckboxSelectMultiple
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from note.models import NoteSpecial, Alias
|
||||
from note_kfet.inputs import Autocomplete, AmountInput, DatePickerInput
|
||||
from note_kfet.inputs import Autocomplete, AmountInput
|
||||
from permission.models import PermissionMask, Role
|
||||
from PIL import Image, ImageSequence
|
||||
|
||||
from .models import Profile, Club, Membership
|
||||
|
||||
@ -32,7 +33,7 @@ class UserForm(forms.ModelForm):
|
||||
# Django usernames can only contain letters, numbers, @, ., +, - and _.
|
||||
# We want to allow users to have uncommon and unpractical usernames:
|
||||
# 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:
|
||||
model = User
|
||||
@ -43,6 +44,7 @@ class ProfileForm(forms.ModelForm):
|
||||
"""
|
||||
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"))
|
||||
|
||||
last_report = forms.DateTimeField(required=False, disabled=True, label=_("Last report date"))
|
||||
@ -75,7 +77,8 @@ class ProfileForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Profile
|
||||
fields = '__all__'
|
||||
exclude = ('user', 'email_confirmed', 'registration_valid', )
|
||||
# Remove ml_[asso]_registration from exclude if the concerned association uses nk20 to manage its mailing list.
|
||||
exclude = ('user', 'email_confirmed', 'registration_valid', 'ml_sport_registration', )
|
||||
|
||||
|
||||
class ImageForm(forms.Form):
|
||||
@ -210,9 +213,9 @@ class MembershipForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Membership
|
||||
fields = ('user', 'date_start')
|
||||
# Le champ d'utilisateur est remplacé par un champ d'auto-complétion.
|
||||
# Le champ d'utilisateur⋅rice est remplacé par un champ d'auto-complétion.
|
||||
# Quand des lettres sont tapées, une requête est envoyée sur l'API d'auto-complétion
|
||||
# et récupère les noms d'utilisateur valides
|
||||
# et récupère les noms d'utilisateur⋅rices valides
|
||||
widgets = {
|
||||
'user':
|
||||
Autocomplete(
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import hashlib
|
||||
|
18
apps/member/migrations/0013_auto_20240801_1436.py
Normal file
18
apps/member/migrations/0013_auto_20240801_1436.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 2.2.28 on 2024-08-01 12:36
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('member', '0012_club_add_registration_form'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='profile',
|
||||
name='promotion',
|
||||
field=models.PositiveSmallIntegerField(default=2024, help_text='Year of entry to the school (None if not ENS student)', null=True, verbose_name='promotion'),
|
||||
),
|
||||
]
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import datetime
|
||||
@ -295,7 +295,14 @@ class Club(models.Model):
|
||||
|
||||
today = datetime.date.today()
|
||||
|
||||
while (today - self.membership_start).days >= 365:
|
||||
# Avoid any problems on February 29
|
||||
if self.membership_start.month == 2 and self.membership_start.day == 29:
|
||||
self.membership_start -= datetime.timedelta(days=1)
|
||||
if self.membership_end.month == 2 and self.membership_end.day == 29:
|
||||
self.membership_end += datetime.timedelta(days=1)
|
||||
|
||||
while today >= datetime.date(self.membership_start.year + 1,
|
||||
self.membership_start.month, self.membership_start.day):
|
||||
if self.membership_start:
|
||||
self.membership_start = datetime.date(self.membership_start.year + 1,
|
||||
self.membership_start.month, self.membership_start.day)
|
||||
@ -473,10 +480,10 @@ class Membership(models.Model):
|
||||
|
||||
if self.club.parent_club.name == "BDE":
|
||||
parent_membership.roles.set(
|
||||
Role.objects.filter(Q(name="Adhérent BDE") | Q(name="Membre de club")).all())
|
||||
Role.objects.filter(Q(name="Adhérent⋅e BDE") | Q(name="Membre de club")).all())
|
||||
elif self.club.parent_club.name == "Kfet":
|
||||
parent_membership.roles.set(
|
||||
Role.objects.filter(Q(name="Adhérent Kfet") | Q(name="Membre de club")).all())
|
||||
Role.objects.filter(Q(name="Adhérent⋅e Kfet") | Q(name="Membre de club")).all())
|
||||
else:
|
||||
parent_membership.roles.set(Role.objects.filter(name="Membre de club").all())
|
||||
parent_membership.save()
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from datetime import date
|
||||
@ -42,12 +42,12 @@ class UserTable(tables.Table):
|
||||
"""
|
||||
alias = tables.Column()
|
||||
|
||||
section = tables.Column(accessor='profile__section')
|
||||
section = tables.Column(accessor='profile__section', orderable=False)
|
||||
|
||||
# Override the column to let replace the URL
|
||||
email = tables.EmailColumn(linkify=lambda record: "mailto:{}".format(record.email))
|
||||
|
||||
balance = tables.Column(accessor='note__balance', verbose_name=_("Balance"))
|
||||
balance = tables.Column(accessor='note__balance', verbose_name=_("Balance"), orderable=False)
|
||||
|
||||
def render_email(self, record, value):
|
||||
# Replace the email by a dash if the user can't see the profile detail
|
||||
|
@ -11,7 +11,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
{{ title }}
|
||||
</h3>
|
||||
<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">
|
||||
<label class="form-check-label" for="only_active">
|
||||
<input type="checkbox" class="checkboxinput form-check-input" id="only_active"
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from datetime import date
|
||||
|
@ -291,7 +291,7 @@ class TestMemberships(TestCase):
|
||||
|
||||
response = self.client.post(reverse("member:club_manage_roles", args=(self.membership.pk,)), data=dict(
|
||||
roles=[role.id for role in Role.objects.filter(
|
||||
Q(name="Membre de club") | Q(name="Trésorier·ère de club") | Q(name="Bureau de club")).all()],
|
||||
Q(name="Membre de club") | Q(name="Trésorièr⋅e de club") | Q(name="Bureau de club")).all()],
|
||||
))
|
||||
self.assertRedirects(response, self.user.profile.get_absolute_url(), 302, 200)
|
||||
self.membership.refresh_from_db()
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.urls import path
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from datetime import timedelta, date
|
||||
@ -16,8 +16,9 @@ from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import DetailView, UpdateView, TemplateView
|
||||
from django.views.generic.edit import FormMixin
|
||||
from django_tables2.views import SingleTableView
|
||||
from django_tables2.views import MultiTableMixin, SingleTableMixin, SingleTableView
|
||||
from rest_framework.authtoken.models import Token
|
||||
from api.viewsets import is_regex
|
||||
from note.models import Alias, NoteClub, NoteUser, Trust
|
||||
from note.models.transactions import Transaction, SpecialTransaction
|
||||
from note.tables import HistoryTable, AliasTable, TrustTable, TrustedTable
|
||||
@ -25,6 +26,7 @@ from note_kfet.middlewares import _set_current_request
|
||||
from permission.backends import PermissionBackend
|
||||
from permission.models import Role
|
||||
from permission.views import ProtectQuerysetMixin, ProtectedCreateView
|
||||
from django import forms
|
||||
|
||||
from .forms import UserForm, ProfileForm, ImageForm, ClubForm, MembershipForm, \
|
||||
CustomAuthenticationForm, MembershipRolesForm
|
||||
@ -71,11 +73,24 @@ class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||
form.fields['email'].required = True
|
||||
form.fields['email'].help_text = _("This address must be valid.")
|
||||
|
||||
if PermissionBackend.check_perm(self.request, "member.change_profile", context['user_object'].profile):
|
||||
context['profile_form'] = self.profile_form(instance=context['user_object'].profile,
|
||||
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:
|
||||
del context['profile_form'].fields["last_report"]
|
||||
del 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
|
||||
|
||||
@ -219,16 +234,20 @@ class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
||||
if "search" in self.request.GET and self.request.GET["search"]:
|
||||
pattern = self.request.GET["search"]
|
||||
|
||||
# Check if this is a valid regex. If not, we won't check regex
|
||||
valid_regex = is_regex(pattern)
|
||||
suffix = "__iregex" if valid_regex else "__istartswith"
|
||||
prefix = "^" if valid_regex else ""
|
||||
qs = qs.filter(
|
||||
username__iregex="^" + pattern
|
||||
Q(**{f"username{suffix}": prefix + pattern})
|
||||
).union(
|
||||
qs.filter(
|
||||
(Q(alias__iregex="^" + pattern)
|
||||
| Q(normalized_alias__iregex="^" + Alias.normalize(pattern))
|
||||
| Q(last_name__iregex="^" + pattern)
|
||||
| Q(first_name__iregex="^" + pattern)
|
||||
(Q(**{f"alias{suffix}": prefix + pattern})
|
||||
| Q(**{f"normalized_alias{suffix}": prefix + Alias.normalize(pattern)})
|
||||
| Q(**{f"last_name{suffix}": prefix + pattern})
|
||||
| Q(**{f"first_name{suffix}": prefix + pattern})
|
||||
| Q(email__istartswith=pattern))
|
||||
& ~Q(username__iregex="^" + pattern)
|
||||
& ~Q(**{f"username{suffix}": prefix + pattern})
|
||||
), all=True)
|
||||
else:
|
||||
qs = qs.none()
|
||||
@ -243,7 +262,7 @@ class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
||||
return context
|
||||
|
||||
|
||||
class ProfileTrustView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
class ProfileTrustView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, DetailView):
|
||||
"""
|
||||
View and manage user trust relationships
|
||||
"""
|
||||
@ -252,13 +271,25 @@ class ProfileTrustView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
context_object_name = 'user_object'
|
||||
extra_context = {"title": _("Note friendships")}
|
||||
|
||||
tables = [
|
||||
lambda data: TrustTable(data, prefix="trust-"),
|
||||
lambda data: TrustedTable(data, prefix="trusted-"),
|
||||
]
|
||||
|
||||
def get_tables_data(self):
|
||||
note = self.object.note
|
||||
return [
|
||||
note.trusting.filter(PermissionBackend.filter_queryset(self.request, Trust, "view")).distinct(),
|
||||
note.trusted.filter(PermissionBackend.filter_queryset(self.request, Trust, "view")).distinct(),
|
||||
]
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
note = context['object'].note
|
||||
context["trusting"] = TrustTable(
|
||||
note.trusting.filter(PermissionBackend.filter_queryset(self.request, Trust, "view")).distinct().all())
|
||||
context["trusted_by"] = TrustedTable(
|
||||
note.trusted.filter(PermissionBackend.filter_queryset(self.request, Trust, "view")).distinct().all())
|
||||
|
||||
tables = context["tables"]
|
||||
for name, table in zip(["trusting", "trusted_by"], tables):
|
||||
context[name] = table
|
||||
|
||||
context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_trust", Trust(
|
||||
trusting=context["object"].note,
|
||||
trusted=context["object"].note
|
||||
@ -277,7 +308,7 @@ class ProfileTrustView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
return context
|
||||
|
||||
|
||||
class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableMixin, DetailView):
|
||||
"""
|
||||
View and manage user aliases.
|
||||
"""
|
||||
@ -286,12 +317,15 @@ class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
context_object_name = 'user_object'
|
||||
extra_context = {"title": _("Note aliases")}
|
||||
|
||||
table_class = AliasTable
|
||||
context_table_name = "aliases"
|
||||
|
||||
def get_table_data(self):
|
||||
return self.object.note.alias.filter(PermissionBackend.filter_queryset(self.request, Alias, "view")).distinct() \
|
||||
.order_by('normalized_name')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
note = context['object'].note
|
||||
context["aliases"] = AliasTable(
|
||||
note.alias.filter(PermissionBackend.filter_queryset(self.request, Alias, "view")).distinct()
|
||||
.order_by('normalized_name').all())
|
||||
context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_alias", Alias(
|
||||
note=context["object"].note,
|
||||
name="",
|
||||
@ -410,10 +444,15 @@ class ClubListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
||||
if "search" in self.request.GET:
|
||||
pattern = self.request.GET["search"]
|
||||
|
||||
# Check if this is a valid regex. If not, we won't check regex
|
||||
valid_regex = is_regex(pattern)
|
||||
suffix = "__iregex" if valid_regex else "__istartswith"
|
||||
prefix = "^" if valid_regex else ""
|
||||
|
||||
qs = qs.filter(
|
||||
Q(name__iregex=pattern)
|
||||
| Q(note__alias__name__iregex=pattern)
|
||||
| Q(note__alias__normalized_name__iregex=Alias.normalize(pattern))
|
||||
Q(**{f"name{suffix}": prefix + pattern})
|
||||
| Q(**{f"note__alias__name{suffix}": prefix + pattern})
|
||||
| Q(**{f"note__alias__normalized_name{suffix}": prefix + Alias.normalize(pattern)})
|
||||
)
|
||||
|
||||
return qs
|
||||
@ -510,7 +549,7 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
return context
|
||||
|
||||
|
||||
class ClubAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
class ClubAliasView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableMixin, DetailView):
|
||||
"""
|
||||
Manage aliases of a club.
|
||||
"""
|
||||
@ -519,11 +558,16 @@ class ClubAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
context_object_name = 'club'
|
||||
extra_context = {"title": _("Note aliases")}
|
||||
|
||||
table_class = AliasTable
|
||||
context_table_name = "aliases"
|
||||
|
||||
def get_table_data(self):
|
||||
return self.object.note.alias.filter(
|
||||
PermissionBackend.filter_queryset(self.request, Alias, "view")).distinct()
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
note = context['object'].note
|
||||
context["aliases"] = AliasTable(note.alias.filter(
|
||||
PermissionBackend.filter_queryset(self.request, Alias, "view")).distinct().all())
|
||||
|
||||
context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_alias", Alias(
|
||||
note=context["object"].note,
|
||||
name="",
|
||||
@ -827,8 +871,8 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||
|
||||
ret = super().form_valid(form)
|
||||
|
||||
member_role = Role.objects.filter(Q(name="Adhérent BDE") | Q(name="Membre de club")).all() \
|
||||
if club.name == "BDE" else Role.objects.filter(Q(name="Adhérent Kfet") | Q(name="Membre de club")).all() \
|
||||
member_role = Role.objects.filter(Q(name="Adhérent⋅e BDE") | Q(name="Membre de club")).all() \
|
||||
if club.name == "BDE" else Role.objects.filter(Q(name="Adhérent⋅e Kfet") | Q(name="Membre de club")).all() \
|
||||
if club.name == "Kfet"else Role.objects.filter(name="Membre de club").all()
|
||||
# Set the same roles as before
|
||||
if old_membership:
|
||||
@ -864,7 +908,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||
membership.refresh_from_db()
|
||||
if old_membership.exists():
|
||||
membership.roles.set(old_membership.get().roles.all())
|
||||
membership.roles.set(Role.objects.filter(Q(name="Adhérent Kfet") | Q(name="Membre de club")).all())
|
||||
membership.roles.set(Role.objects.filter(Q(name="Adhérent⋅e Kfet") | Q(name="Membre de club")).all())
|
||||
membership.save()
|
||||
|
||||
return ret
|
||||
@ -912,10 +956,15 @@ class ClubMembersListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableV
|
||||
|
||||
if 'search' in self.request.GET:
|
||||
pattern = self.request.GET['search']
|
||||
|
||||
# Check if this is a valid regex. If not, we won't check regex
|
||||
valid_regex = is_regex(pattern)
|
||||
suffix = "__iregex" if valid_regex else "__istartswith"
|
||||
prefix = "^" if valid_regex else ""
|
||||
qs = qs.filter(
|
||||
Q(user__first_name__iregex='^' + pattern)
|
||||
| Q(user__last_name__iregex='^' + pattern)
|
||||
| Q(user__note__alias__normalized_name__iregex='^' + Alias.normalize(pattern))
|
||||
Q(**{f"user__first_name{suffix}": prefix + pattern})
|
||||
| Q(**{f"user__last_name{suffix}": prefix + pattern})
|
||||
| Q(**{f"user__note__alias__normalized_name{suffix}": prefix + Alias.normalize(pattern)})
|
||||
)
|
||||
|
||||
only_active = "only_active" not in self.request.GET or self.request.GET["only_active"] != '0'
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
default_app_config = 'note.apps.NoteConfig'
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.contrib import admin
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.conf import settings
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from .views import NotePolymorphicViewSet, AliasViewSet, ConsumerViewSet, \
|
||||
|
@ -1,16 +1,16 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
import re
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.models import Q
|
||||
from django.core.exceptions import ValidationError
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework.filters import OrderingFilter, SearchFilter
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.filters import OrderingFilter
|
||||
from rest_framework import status, viewsets
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from api.viewsets import ReadProtectedModelViewSet, ReadOnlyProtectedModelViewSet
|
||||
from api.filters import RegexSafeSearchFilter
|
||||
from api.viewsets import ReadProtectedModelViewSet, ReadOnlyProtectedModelViewSet, \
|
||||
is_regex
|
||||
from permission.backends import PermissionBackend
|
||||
|
||||
from .serializers import NotePolymorphicSerializer, AliasSerializer, ConsumerSerializer, \
|
||||
@ -29,7 +29,7 @@ class NotePolymorphicViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
queryset = Note.objects.order_by('id')
|
||||
serializer_class = NotePolymorphicSerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
||||
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter, OrderingFilter]
|
||||
filterset_fields = ['alias__name', 'polymorphic_ctype', 'is_active', 'balance', 'last_negative', 'created_at', ]
|
||||
search_fields = ['$alias__normalized_name', '$alias__name', '$polymorphic_ctype__model',
|
||||
'$noteuser__user__last_name', '$noteuser__user__first_name', '$noteuser__user__email',
|
||||
@ -48,10 +48,14 @@ class NotePolymorphicViewSet(ReadProtectedModelViewSet):
|
||||
.distinct()
|
||||
|
||||
alias = self.request.query_params.get("alias", ".*")
|
||||
# Check if this is a valid regex. If not, we won't check regex
|
||||
valid_regex = is_regex(alias)
|
||||
suffix = '__iregex' if valid_regex else '__istartswith'
|
||||
alias_prefix = '^' if valid_regex else ''
|
||||
queryset = queryset.filter(
|
||||
Q(alias__name__iregex="^" + alias)
|
||||
| Q(alias__normalized_name__iregex="^" + Alias.normalize(alias))
|
||||
| Q(alias__normalized_name__iregex="^" + alias.lower())
|
||||
Q(**{f"alias__name{suffix}": alias_prefix + alias})
|
||||
| Q(**{f"alias__normalized_name{suffix}": alias_prefix + Alias.normalize(alias)})
|
||||
| Q(**{f"alias__normalized_name{suffix}": alias_prefix + alias.lower()})
|
||||
)
|
||||
|
||||
return queryset.order_by("id")
|
||||
@ -65,7 +69,7 @@ class TrustViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
queryset = Trust.objects
|
||||
serializer_class = TrustSerializer
|
||||
filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
|
||||
filter_backends = [RegexSafeSearchFilter, DjangoFilterBackend, OrderingFilter]
|
||||
search_fields = ['$trusting__alias__name', '$trusting__alias__normalized_name',
|
||||
'$trusted__alias__name', '$trusted__alias__normalized_name']
|
||||
filterset_fields = ['trusting', 'trusting__noteuser__user', 'trusted', 'trusted__noteuser__user']
|
||||
@ -91,11 +95,11 @@ class AliasViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `Alias` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/note/aliases/
|
||||
then render it on /api/note/alias/
|
||||
"""
|
||||
queryset = Alias.objects
|
||||
serializer_class = AliasSerializer
|
||||
filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
|
||||
filter_backends = [RegexSafeSearchFilter, DjangoFilterBackend, OrderingFilter]
|
||||
search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ]
|
||||
filterset_fields = ['name', 'normalized_name', 'note', 'note__noteuser__user',
|
||||
'note__noteclub__club', 'note__polymorphic_ctype__model', ]
|
||||
@ -126,18 +130,22 @@ class AliasViewSet(ReadProtectedModelViewSet):
|
||||
|
||||
alias = self.request.query_params.get("alias", None)
|
||||
if alias:
|
||||
# Check if this is a valid regex. If not, we won't check regex
|
||||
valid_regex = is_regex(alias)
|
||||
suffix = '__iregex' if valid_regex else '__istartswith'
|
||||
alias_prefix = '^' if valid_regex else ''
|
||||
queryset = queryset.filter(
|
||||
name__iregex="^" + alias
|
||||
**{f"name{suffix}": alias_prefix + alias}
|
||||
).union(
|
||||
queryset.filter(
|
||||
Q(normalized_name__iregex="^" + Alias.normalize(alias))
|
||||
& ~Q(name__iregex="^" + alias)
|
||||
Q(**{f"normalized_name{suffix}": alias_prefix + Alias.normalize(alias)})
|
||||
& ~Q(**{f"name{suffix}": alias_prefix + alias})
|
||||
),
|
||||
all=True).union(
|
||||
queryset.filter(
|
||||
Q(normalized_name__iregex="^" + alias.lower())
|
||||
& ~Q(normalized_name__iregex="^" + Alias.normalize(alias))
|
||||
& ~Q(name__iregex="^" + alias)
|
||||
Q(**{f"normalized_name{suffix}": "^" + alias.lower()})
|
||||
& ~Q(**{f"normalized_name{suffix}": "^" + Alias.normalize(alias)})
|
||||
& ~Q(**{f"name{suffix}": "^" + alias})
|
||||
),
|
||||
all=True)
|
||||
|
||||
@ -147,7 +155,7 @@ class AliasViewSet(ReadProtectedModelViewSet):
|
||||
class ConsumerViewSet(ReadOnlyProtectedModelViewSet):
|
||||
queryset = Alias.objects
|
||||
serializer_class = ConsumerSerializer
|
||||
filter_backends = [SearchFilter, OrderingFilter, DjangoFilterBackend]
|
||||
filter_backends = [RegexSafeSearchFilter, OrderingFilter, DjangoFilterBackend]
|
||||
search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ]
|
||||
filterset_fields = ['name', 'normalized_name', 'note', 'note__noteuser__user',
|
||||
'note__noteclub__club', 'note__polymorphic_ctype__model', ]
|
||||
@ -166,11 +174,7 @@ class ConsumerViewSet(ReadOnlyProtectedModelViewSet):
|
||||
|
||||
alias = self.request.query_params.get("alias", None)
|
||||
# Check if this is a valid regex. If not, we won't check regex
|
||||
try:
|
||||
re.compile(alias)
|
||||
valid_regex = True
|
||||
except (re.error, TypeError):
|
||||
valid_regex = False
|
||||
valid_regex = is_regex(alias)
|
||||
suffix = '__iregex' if valid_regex else '__istartswith'
|
||||
alias_prefix = '^' if valid_regex else ''
|
||||
queryset = queryset.prefetch_related('note')
|
||||
@ -179,19 +183,10 @@ class ConsumerViewSet(ReadOnlyProtectedModelViewSet):
|
||||
# We match first an alias if it is matched without normalization,
|
||||
# then if the normalized pattern matches a normalized alias.
|
||||
queryset = queryset.filter(
|
||||
**{f'name{suffix}': alias_prefix + alias}
|
||||
).union(
|
||||
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)
|
||||
Q(**{f'name{suffix}': alias_prefix + alias})
|
||||
| Q(**{f'normalized_name{suffix}': alias_prefix + Alias.normalize(alias)})
|
||||
| Q(**{f'normalized_name{suffix}': alias_prefix + alias.lower()})
|
||||
)
|
||||
|
||||
queryset = queryset if settings.DATABASES[queryset.db]["ENGINE"] == 'django.db.backends.postgresql' \
|
||||
else queryset.order_by("name")
|
||||
@ -207,7 +202,7 @@ class TemplateCategoryViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
queryset = TemplateCategory.objects.order_by('name')
|
||||
serializer_class = TemplateCategorySerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||
filterset_fields = ['name', 'templates', 'templates__name']
|
||||
search_fields = ['$name', '$templates__name', ]
|
||||
|
||||
@ -220,7 +215,7 @@ class TransactionTemplateViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
queryset = TransactionTemplate.objects.order_by('name')
|
||||
serializer_class = TransactionTemplateSerializer
|
||||
filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
|
||||
filter_backends = [RegexSafeSearchFilter, DjangoFilterBackend, OrderingFilter]
|
||||
filterset_fields = ['name', 'amount', 'display', 'category', 'category__name', ]
|
||||
search_fields = ['$name', '$category__name', ]
|
||||
ordering_fields = ['amount', ]
|
||||
@ -234,7 +229,7 @@ class TransactionViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
queryset = Transaction.objects.order_by('-created_at')
|
||||
serializer_class = TransactionPolymorphicSerializer
|
||||
filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
|
||||
filter_backends = [RegexSafeSearchFilter, DjangoFilterBackend, OrderingFilter]
|
||||
filterset_fields = ['source', 'source_alias', 'source__alias__name', 'source__alias__normalized_name',
|
||||
'destination', 'destination_alias', 'destination__alias__name',
|
||||
'destination__alias__normalized_name', 'quantity', 'polymorphic_ctype', 'amount',
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
@ -1,13 +1,14 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
from datetime import datetime
|
||||
|
||||
from bootstrap_datepicker_plus.widgets import DateTimePickerInput
|
||||
from django import forms
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.forms import CheckboxSelectMultiple
|
||||
from django.utils.timezone import make_aware
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from note_kfet.inputs import Autocomplete, AmountInput, DateTimePickerInput
|
||||
from note_kfet.inputs import Autocomplete, AmountInput
|
||||
|
||||
from .models import TransactionTemplate, NoteClub, Alias
|
||||
|
||||
|
@ -0,0 +1,25 @@
|
||||
# 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'),
|
||||
),
|
||||
]
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from .notes import Alias, Note, NoteClub, NoteSpecial, NoteUser, Trust
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import unicodedata
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.utils import timezone
|
||||
|
@ -1,4 +1,4 @@
|
||||
// Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
// Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// When a transaction is performed, lock the interface to prevent spam clicks.
|
||||
@ -245,7 +245,7 @@ function consume (source, source_alias, dest, quantity, amount, reason, type, ca
|
||||
invalidity_reason: 'Solde insuffisant',
|
||||
polymorphic_ctype: type,
|
||||
resourcetype: 'RecurrentTransaction',
|
||||
source: source,
|
||||
source: source.id,
|
||||
source_alias: source_alias,
|
||||
destination: dest,
|
||||
template: template
|
||||
@ -294,3 +294,10 @@ searchbar.addEventListener("keyup", function (e) {
|
||||
if (firstMatch && e.key === "Enter")
|
||||
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()
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import html
|
||||
@ -260,11 +260,13 @@ class ButtonTable(tables.Table):
|
||||
text=_('edit'),
|
||||
accessor='pk',
|
||||
verbose_name=_("Edit"),
|
||||
orderable=False,
|
||||
)
|
||||
|
||||
hideshow = tables.Column(
|
||||
verbose_name=_("Hide/Show"),
|
||||
accessor="pk",
|
||||
orderable=False,
|
||||
attrs={
|
||||
'td': {
|
||||
'class': 'col-sm-1',
|
||||
@ -276,7 +278,8 @@ class ButtonTable(tables.Table):
|
||||
delete_col = tables.TemplateColumn(template_code=DELETE_TEMPLATE,
|
||||
extra_context={"delete_trans": _('delete')},
|
||||
attrs={'td': {'class': 'col-sm-1'}},
|
||||
verbose_name=_("Delete"), )
|
||||
verbose_name=_("Delete"),
|
||||
orderable=False, )
|
||||
|
||||
def render_amount(self, value):
|
||||
return pretty_money(value)
|
||||
|
@ -9,7 +9,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
name="{{ widget.name }}"
|
||||
{# Other attributes are loaded #}
|
||||
{% for name, value in widget.attrs.items %}
|
||||
{% ifnotequal value False %}{{ name }}{% ifnotequal value True %}="{{ value|stringformat:'s' }}"{% endifnotequal %}{% endifnotequal %}
|
||||
{% if value is not False %}{{ name }}{% if value is not True %}="{{ value|stringformat:'s' }}"{% endif %}{% endif %}
|
||||
{% endfor %}>
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text">€</span>
|
||||
|
@ -22,7 +22,7 @@
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Par ailleurs, le BDE ne sert pas d'alcool aux adhérents dont le solde
|
||||
Par ailleurs, le BDE ne sert pas d'alcool aux adhérent⋅es dont le solde
|
||||
est inférieur à 0 €.
|
||||
</p>
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django import template
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django import template
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from api.tests import TestAPI
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.urls import path
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import json
|
||||
@ -13,6 +13,7 @@ from django.views.generic import CreateView, UpdateView, DetailView
|
||||
from django.urls import reverse_lazy
|
||||
from django_tables2 import SingleTableView
|
||||
from activity.models import Entry
|
||||
from api.viewsets import is_regex
|
||||
from permission.backends import PermissionBackend
|
||||
from permission.views import ProtectQuerysetMixin
|
||||
from note_kfet.inputs import AmountInput
|
||||
@ -89,11 +90,15 @@ class TransactionTemplateListView(ProtectQuerysetMixin, LoginRequiredMixin, Sing
|
||||
qs = super().get_queryset().distinct()
|
||||
if "search" in self.request.GET:
|
||||
pattern = self.request.GET["search"]
|
||||
|
||||
# Check if this is a valid regex. If not, we won't check regex
|
||||
valid_regex = is_regex(pattern)
|
||||
suffix = "__iregex" if valid_regex else "__icontains"
|
||||
qs = qs.filter(
|
||||
Q(name__iregex=pattern)
|
||||
| Q(destination__club__name__iregex=pattern)
|
||||
| Q(category__name__iregex=pattern)
|
||||
| Q(description__iregex=pattern)
|
||||
Q(**{f"name{suffix}": pattern})
|
||||
| Q(**{f"destination__club__name{suffix}": pattern})
|
||||
| Q(**{f"category__name{suffix}": pattern})
|
||||
| Q(**{f"description{suffix}": pattern})
|
||||
)
|
||||
|
||||
qs = qs.order_by('-display', 'category__name', 'destination__club__name', 'name')
|
||||
@ -223,7 +228,10 @@ class TransactionSearchView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView
|
||||
if "type" in data and data["type"]:
|
||||
transactions = transactions.filter(polymorphic_ctype__in=data["type"])
|
||||
if "reason" in data and data["reason"]:
|
||||
transactions = transactions.filter(reason__iregex=data["reason"])
|
||||
# Check if this is a valid regex. If not, we won't check regex
|
||||
valid_regex = is_regex(data["reason"])
|
||||
suffix = "__iregex" if valid_regex else "__istartswith"
|
||||
transactions = transactions.filter(Q(**{f"reason{suffix}": data["reason"]}))
|
||||
if "valid" in data and data["valid"]:
|
||||
transactions = transactions.filter(valid=data["valid"])
|
||||
if "amount_gte" in data and data["amount_gte"]:
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
default_app_config = 'permission.apps.PermissionConfig'
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-lateré
|
||||
|
||||
from django.contrib import admin
|
||||
@ -31,3 +31,4 @@ class RoleAdmin(admin.ModelAdmin):
|
||||
Admin customisation for Role
|
||||
"""
|
||||
list_display = ('name', )
|
||||
filter_horizontal = ('permissions',)
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from rest_framework import serializers
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from .views import PermissionViewSet, RoleViewSet
|
||||
|
@ -1,9 +1,9 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from api.viewsets import ReadOnlyProtectedModelViewSet
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework.filters import SearchFilter
|
||||
from api.filters import RegexSafeSearchFilter
|
||||
from api.viewsets import ReadOnlyProtectedModelViewSet
|
||||
|
||||
from .serializers import PermissionSerializer, RoleSerializer
|
||||
from ..models import Permission, Role
|
||||
@ -17,9 +17,9 @@ class PermissionViewSet(ReadOnlyProtectedModelViewSet):
|
||||
"""
|
||||
queryset = Permission.objects.order_by('id')
|
||||
serializer_class = PermissionSerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||
filterset_fields = ['model', 'type', 'query', 'mask', 'field', 'permanent', ]
|
||||
search_fields = ['$model__name', '$query', '$description', ]
|
||||
search_fields = ['$model__model', '$query', '$description', ]
|
||||
|
||||
|
||||
class RoleViewSet(ReadOnlyProtectedModelViewSet):
|
||||
@ -30,6 +30,6 @@ class RoleViewSet(ReadOnlyProtectedModelViewSet):
|
||||
"""
|
||||
queryset = Role.objects.order_by('id')
|
||||
serializer_class = RoleSerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||
filterset_fields = ['name', 'permissions', 'for_club', 'memberships__user', ]
|
||||
search_fields = ['$name', '$for_club__name', ]
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
@ -1,7 +1,7 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from datetime import date
|
||||
from datetime import date, timedelta
|
||||
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
from django.contrib.auth.models import User
|
||||
@ -106,6 +106,7 @@ class PermissionBackend(ModelBackend):
|
||||
Q=Q,
|
||||
now=timezone.now(),
|
||||
today=date.today(),
|
||||
week=timedelta(days=7),
|
||||
)
|
||||
yield permission
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
import sys
|
||||
from functools import lru_cache
|
||||
|
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user