mirror of
https://gitlab.crans.org/bde/nk20
synced 2025-06-21 09:58:23 +02:00
Compare commits
265 Commits
svg_icons
...
8ec42bcf2b
Author | SHA1 | Date | |
---|---|---|---|
8ec42bcf2b | |||
28117c8c61 | |||
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 | |||
b8f81048a5 | |||
af819f45a1 | |||
076d065ffa | |||
2da77d9c17 | |||
01584d6330 | |||
4c0a5922c4 | |||
f90b28fc7c | |||
925e0f26f5 | |||
c912383f86 | |||
32830e43fd | |||
11c6a6fa7a | |||
201d6b114a | |||
19e77df299 | |||
5fd6ec5668 | |||
10a01c5bc2 | |||
989905ea64 | |||
0218d43a17 | |||
5d30b0e819 | |||
ec759dd3c0 | |||
2eb965291d | |||
7f182ee2ee | |||
3132aa4c38 | |||
c7eb774859 | |||
32f8d285b3 | |||
050256ea13 | |||
7afd15b1cc | |||
258361f116 | |||
a307530579 | |||
5de930bf40 | |||
f7ebe0e99b | |||
73de6e2176 | |||
201611b105 | |||
40c239e9da | |||
2aaab2b454 | |||
fc088dec86 | |||
2d60f1fd7b | |||
7b48b09329 | |||
ffac940511 | |||
50f98fd5ad | |||
402e19d1ce | |||
0b0394b61f | |||
98422d8259 | |||
29509b5b26 | |||
0d64ad31e0 | |||
5781cbd6a5 | |||
5295e61a00 | |||
e79ed6226a | |||
68152e6354 | |||
6c61daf1c5 | |||
b8cc297baf | |||
cd8224f2e0 | |||
3c882a7854 | |||
357e1bbaa2 | |||
f5c4c58525 | |||
dafb602b08 | |||
5b377e6a75 | |||
28bd62531e | |||
b3a31c27a5 | |||
c7a8e6a1a5 | |||
546a3a72b1 | |||
2e5664f79d | |||
e367666fe9 | |||
04a9b3daf0 | |||
d1df8f3eac | |||
a5221f66ef | |||
7d59cd6cd2 | |||
0db0474217 | |||
2b3eb15f59 | |||
a6b479db19 | |||
048d251f75 | |||
7b11cb0797 | |||
ff3c30517e | |||
f481ea6acb | |||
802fd8c2d7 | |||
5209a586a9 | |||
24f54ac876 | |||
988b4c9e88 | |||
e32c267995 | |||
5e39209ab1 | |||
08b2fabe07 | |||
405479e5ad | |||
0cc130092f | |||
ff6e207512 | |||
0f1e4d2e60 | |||
6255bcbbb1 | |||
d82a1001c4 | |||
31a54482f0 | |||
4ee02345d4 | |||
422c087d17 | |||
30d6e2c95e | |||
f3a3f07e38 | |||
a5e802f370 | |||
540f3bc354 | |||
2d19457506 | |||
72786d0d2b | |||
f099cbc879 | |||
977eb7c0d4 | |||
d81b1f2710 | |||
6a69590a82 | |||
7afc583282 | |||
4fb0b7d736 | |||
18a5b65a1c | |||
f545af4977 | |||
103e2d0635 | |||
aedf0e87ba | |||
dab45b5fd4 | |||
b3353b563c | |||
6bc52be707 | |||
834d68fe35 | |||
c6a2849d35 | |||
4ab22c92b3 | |||
c328c1457c | |||
96da7d01ae | |||
d27f942339 | |||
738d6c932d | |||
1760196578 | |||
13b9b6edea | |||
e06e3b2972 | |||
9596aa7b8c | |||
ba0d64f0d4 | |||
8d17801e28 | |||
609362c4f8 | |||
03d2d5f03e | |||
d2057a9f45 | |||
b6e68eeebe | |||
6410542027 | |||
6b1cd3ba7a | |||
9f114b8ca2 | |||
e0132b6dc8 | |||
f1cc82fab3 | |||
644cf14c4b | |||
f19a489313 | |||
dedd6c69cc | |||
b42f5afeab | |||
31e67ae3f6 | |||
b08da7a727 | |||
451aa64f33 | |||
3c99b0f3e9 | |||
201a179947 | |||
96784aee3b | |||
981c4d0300 | |||
11223430fd | |||
7aeb977e72 | |||
52fef1df42 | |||
16f8a60a3f | |||
2839d3de1e | |||
30afa6da0a | |||
84fc77696f | |||
19fc620d1f | |||
d5819ac562 | |||
a79df8f1f6 | |||
364b18e188 | |||
10a883b2e5 | |||
1410ab6c4f | |||
623dd61be6 | |||
48a0a87e7c | |||
563f525b11 | |||
63c1d74f1a | |||
c42fb380a6 | |||
c636d52a73 | |||
6a9021ec14 | |||
9c9149b53a | |||
cb74311e7b | |||
9d7dd566c9 | |||
6bceb394c5 | |||
62cf8f9d84 | |||
9944ebcaad | |||
8537f043f7 | |||
2dd1c3fb89 | |||
c8665c5798 | |||
e9f1b6f52d | |||
1d95ae4810 | |||
c89a95f8d2 | |||
73640b1dfa | |||
84b16ab603 | |||
6a1b51dbbf | |||
c441a43a8b | |||
87f3b51b04 | |||
0a853fd3e6 | |||
c429734810 | |||
5d759111b6 | |||
70baf7566c | |||
eb355f547c | |||
7068170f18 | |||
45ee9a8941 | |||
454ea19603 | |||
5a77a66391 | |||
761fc170eb
|
|||
ac23d7eb54
|
|||
40e7415062
|
|||
319405d2b1
|
|||
633ab88b04
|
|||
e29b42eecc
|
|||
dc69faaf1d
|
|||
442a5c5e36
|
|||
7ab0fec3bc
|
|||
bd4fb23351 | |||
ee22e9b3b6 | |||
19ae616fb4 | |||
b7657ec362 | |||
4d03d9460d | |||
3633f66a87 | |||
d43fbe7ac6 | |||
df5f9b5f1e | |||
4161248bff
|
|||
58136f3c48
|
|||
d9b4e0a9a9
|
|||
8563a8d235
|
|||
5f69232560 | |||
d3273e9ee2
|
|||
4e30f805a7 | |||
546e422e64
|
|||
9048a416df
|
|||
8578bd743c
|
|||
45a10dad00
|
|||
18a1282773
|
|||
132afc3d15
|
|||
6bf16a181a
|
|||
e20df82346
|
|||
1eb72044c2 | |||
f88eae924c
|
|||
4b6e3ba546
|
|||
bf0fe3479f | |||
45ba4f9537
|
|||
b204805ce2
|
|||
2f28e34cec
|
|||
9c8ea2cd41
|
|||
41289857b2 | |||
28a8792c9f
|
|||
58cafad032
|
|||
7848cd9cc2
|
|||
d18ccfac23
|
|||
e479e1e3a4 | |||
82b0c83b1f | |||
38ca414ef6
|
|||
fd811053c7
|
|||
9d386d1ecf
|
|||
ca2b9f061c |
1
.gitignore
vendored
1
.gitignore
vendored
@ -42,6 +42,7 @@ map.json
|
|||||||
backups/
|
backups/
|
||||||
/static/
|
/static/
|
||||||
/media/
|
/media/
|
||||||
|
/tmp/
|
||||||
|
|
||||||
# Virtualenv
|
# Virtualenv
|
||||||
env/
|
env/
|
||||||
|
@ -8,19 +8,19 @@ variables:
|
|||||||
GIT_SUBMODULE_STRATEGY: recursive
|
GIT_SUBMODULE_STRATEGY: recursive
|
||||||
|
|
||||||
# Debian Buster
|
# Debian Buster
|
||||||
py37-django22:
|
# py37-django22:
|
||||||
stage: test
|
# stage: test
|
||||||
image: debian:buster-backports
|
# image: debian:buster-backports
|
||||||
before_script:
|
# before_script:
|
||||||
- >
|
# - >
|
||||||
apt-get update &&
|
# apt-get update &&
|
||||||
apt-get install --no-install-recommends -t buster-backports -y
|
# apt-get install --no-install-recommends -t buster-backports -y
|
||||||
python3-django python3-django-crispy-forms
|
# python3-django python3-django-crispy-forms
|
||||||
python3-django-extensions python3-django-filters python3-django-polymorphic
|
# python3-django-extensions python3-django-filters python3-django-polymorphic
|
||||||
python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil
|
# python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil
|
||||||
python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache
|
# python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache
|
||||||
python3-bs4 python3-setuptools tox texlive-xetex
|
# python3-bs4 python3-setuptools tox texlive-xetex
|
||||||
script: tox -e py37-django22
|
# script: tox -e py37-django22
|
||||||
|
|
||||||
# Ubuntu 20.04
|
# Ubuntu 20.04
|
||||||
py38-django22:
|
py38-django22:
|
||||||
@ -56,7 +56,7 @@ py39-django22:
|
|||||||
|
|
||||||
linters:
|
linters:
|
||||||
stage: quality-assurance
|
stage: quality-assurance
|
||||||
image: debian:buster-backports
|
image: debian:bullseye
|
||||||
before_script:
|
before_script:
|
||||||
- apt-get update && apt-get install -y tox
|
- apt-get update && apt-get install -y tox
|
||||||
script: tox -e linters
|
script: tox -e linters
|
||||||
|
2
.gitmodules
vendored
2
.gitmodules
vendored
@ -1,3 +1,3 @@
|
|||||||
[submodule "apps/scripts"]
|
[submodule "apps/scripts"]
|
||||||
path = apps/scripts
|
path = apps/scripts
|
||||||
url = https://gitlab.crans.org/bde/nk20-scripts.git
|
url = https://gitlab.crans.org/bde/nk20-scripts
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
# NoteKfet 2020
|
# NoteKfet 2020
|
||||||
|
|
||||||
[](https://www.gnu.org/licenses/gpl-3.0.txt)
|
[](https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
[](https://gitlab.crans.org/bde/nk20/commits/master)
|
[](https://gitlab.crans.org/bde/nk20/commits/main)
|
||||||
[](https://gitlab.crans.org/bde/nk20/commits/master)
|
[](https://gitlab.crans.org/bde/nk20/commits/main)
|
||||||
|
|
||||||
## Table des matières
|
## Table des matières
|
||||||
|
|
||||||
@ -55,7 +55,7 @@ Bien que cela permette de créer une instance sur toutes les distributions,
|
|||||||
(env)$ ./manage.py makemigrations
|
(env)$ ./manage.py makemigrations
|
||||||
(env)$ ./manage.py migrate
|
(env)$ ./manage.py migrate
|
||||||
(env)$ ./manage.py loaddata initial
|
(env)$ ./manage.py loaddata initial
|
||||||
(env)$ ./manage.py createsuperuser # Création d'un utilisateur initial
|
(env)$ ./manage.py createsuperuser # Création d'un⋅e utilisateur⋅rice initial
|
||||||
```
|
```
|
||||||
|
|
||||||
6. Enjoy :
|
6. Enjoy :
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
prompt: "Password of the database (leave it blank to skip database init)"
|
prompt: "Password of the database (leave it blank to skip database init)"
|
||||||
private: yes
|
private: yes
|
||||||
vars:
|
vars:
|
||||||
mirror: mirror.crans.org
|
mirror: eclats.crans.org
|
||||||
roles:
|
roles:
|
||||||
- 1-apt-basic
|
- 1-apt-basic
|
||||||
- 2-nk20
|
- 2-nk20
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
note:
|
note:
|
||||||
server_name: note.crans.org
|
server_name: note.crans.org
|
||||||
git_branch: master
|
git_branch: main
|
||||||
serve_static: true
|
serve_static: true
|
||||||
cron_enabled: true
|
cron_enabled: true
|
||||||
email: notekfet2020@lists.crans.org
|
email: notekfet2020@lists.crans.org
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
---
|
---
|
||||||
- name: Add buster-backports to apt sources
|
- name: Add buster-backports to apt sources if needed
|
||||||
apt_repository:
|
apt_repository:
|
||||||
repo: deb http://{{ mirror }}/debian buster-backports main
|
repo: deb http://{{ mirror }}/debian buster-backports main
|
||||||
state: present
|
state: present
|
||||||
when: ansible_facts['distribution'] == "Debian"
|
when:
|
||||||
|
- ansible_distribution == "Debian"
|
||||||
|
- ansible_distribution_major_version | int == 10
|
||||||
|
|
||||||
- name: Install note_kfet APT dependencies
|
- name: Install note_kfet APT dependencies
|
||||||
apt:
|
apt:
|
||||||
update_cache: true
|
update_cache: true
|
||||||
default_release: "{{ 'buster-backports' if ansible_facts['distribution'] == 'Debian' }}"
|
|
||||||
install_recommends: false
|
install_recommends: false
|
||||||
name:
|
name:
|
||||||
# Common tools
|
# Common tools
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
default_app_config = 'activity.apps.ActivityConfig'
|
default_app_config = 'activity.apps.ActivityConfig'
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
from rest_framework.validators import UniqueTogetherValidator
|
||||||
|
|
||||||
from ..models import Activity, ActivityType, Entry, Guest, GuestTransaction
|
from ..models import Activity, ActivityType, Entry, Guest, GuestTransaction, Opener
|
||||||
|
|
||||||
|
|
||||||
class ActivityTypeSerializer(serializers.ModelSerializer):
|
class ActivityTypeSerializer(serializers.ModelSerializer):
|
||||||
@ -59,3 +61,17 @@ class GuestTransactionSerializer(serializers.ModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = GuestTransaction
|
model = GuestTransaction
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
|
|
||||||
|
|
||||||
|
class OpenerSerializer(serializers.ModelSerializer):
|
||||||
|
"""
|
||||||
|
REST API Serializer for Openers.
|
||||||
|
The djangorestframework plugin will analyse the model `Opener` and parse all fields in the API.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Opener
|
||||||
|
fields = '__all__'
|
||||||
|
validators = [UniqueTogetherValidator(
|
||||||
|
queryset=Opener.objects.all(), fields=("opener", "activity"),
|
||||||
|
message=_("This opener already exists"))]
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from .views import ActivityTypeViewSet, ActivityViewSet, EntryViewSet, GuestViewSet
|
from .views import ActivityTypeViewSet, ActivityViewSet, EntryViewSet, GuestViewSet, OpenerViewSet
|
||||||
|
|
||||||
|
|
||||||
def register_activity_urls(router, path):
|
def register_activity_urls(router, path):
|
||||||
@ -12,3 +12,4 @@ def register_activity_urls(router, path):
|
|||||||
router.register(path + '/type', ActivityTypeViewSet)
|
router.register(path + '/type', ActivityTypeViewSet)
|
||||||
router.register(path + '/guest', GuestViewSet)
|
router.register(path + '/guest', GuestViewSet)
|
||||||
router.register(path + '/entry', EntryViewSet)
|
router.register(path + '/entry', EntryViewSet)
|
||||||
|
router.register(path + '/opener', OpenerViewSet)
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from api.filters import RegexSafeSearchFilter
|
||||||
from api.viewsets import ReadProtectedModelViewSet
|
from api.viewsets import ReadProtectedModelViewSet
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
from rest_framework.filters import SearchFilter
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status
|
||||||
|
|
||||||
from .serializers import ActivitySerializer, ActivityTypeSerializer, EntrySerializer, GuestSerializer
|
from .serializers import ActivitySerializer, ActivityTypeSerializer, EntrySerializer, GuestSerializer, OpenerSerializer
|
||||||
from ..models import Activity, ActivityType, Entry, Guest
|
from ..models import Activity, ActivityType, Entry, Guest, Opener
|
||||||
|
|
||||||
|
|
||||||
class ActivityTypeViewSet(ReadProtectedModelViewSet):
|
class ActivityTypeViewSet(ReadProtectedModelViewSet):
|
||||||
@ -29,7 +32,7 @@ class ActivityViewSet(ReadProtectedModelViewSet):
|
|||||||
"""
|
"""
|
||||||
queryset = Activity.objects.order_by('id')
|
queryset = Activity.objects.order_by('id')
|
||||||
serializer_class = ActivitySerializer
|
serializer_class = ActivitySerializer
|
||||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||||
filterset_fields = ['name', 'description', 'activity_type', 'location', 'creater', 'organizer', 'attendees_club',
|
filterset_fields = ['name', 'description', 'activity_type', 'location', 'creater', 'organizer', 'attendees_club',
|
||||||
'date_start', 'date_end', 'valid', 'open', ]
|
'date_start', 'date_end', 'valid', 'open', ]
|
||||||
search_fields = ['$name', '$description', '$location', '$creater__last_name', '$creater__first_name',
|
search_fields = ['$name', '$description', '$location', '$creater__last_name', '$creater__first_name',
|
||||||
@ -47,7 +50,7 @@ class GuestViewSet(ReadProtectedModelViewSet):
|
|||||||
"""
|
"""
|
||||||
queryset = Guest.objects.order_by('id')
|
queryset = Guest.objects.order_by('id')
|
||||||
serializer_class = GuestSerializer
|
serializer_class = GuestSerializer
|
||||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||||
filterset_fields = ['activity', 'activity__name', 'last_name', 'first_name', 'inviter', 'inviter__alias__name',
|
filterset_fields = ['activity', 'activity__name', 'last_name', 'first_name', 'inviter', 'inviter__alias__name',
|
||||||
'inviter__alias__normalized_name', ]
|
'inviter__alias__normalized_name', ]
|
||||||
search_fields = ['$activity__name', '$last_name', '$first_name', '$inviter__user__email', '$inviter__alias__name',
|
search_fields = ['$activity__name', '$last_name', '$first_name', '$inviter__user__email', '$inviter__alias__name',
|
||||||
@ -62,7 +65,36 @@ class EntryViewSet(ReadProtectedModelViewSet):
|
|||||||
"""
|
"""
|
||||||
queryset = Entry.objects.order_by('id')
|
queryset = Entry.objects.order_by('id')
|
||||||
serializer_class = EntrySerializer
|
serializer_class = EntrySerializer
|
||||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||||
filterset_fields = ['activity', 'time', 'note', 'guest', ]
|
filterset_fields = ['activity', 'time', 'note', 'guest', ]
|
||||||
search_fields = ['$activity__name', '$note__user__email', '$note__alias__name', '$note__alias__normalized_name',
|
search_fields = ['$activity__name', '$note__user__email', '$note__alias__name', '$note__alias__normalized_name',
|
||||||
'$guest__last_name', '$guest__first_name', ]
|
'$guest__last_name', '$guest__first_name', ]
|
||||||
|
|
||||||
|
|
||||||
|
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-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
"name": "Pot",
|
"name": "Pot",
|
||||||
"manage_entries": true,
|
"manage_entries": true,
|
||||||
"can_invite": true,
|
"can_invite": true,
|
||||||
"guest_entry_fee": 500
|
"guest_entry_fee": 1000
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -28,5 +28,25 @@
|
|||||||
"can_invite": false,
|
"can_invite": false,
|
||||||
"guest_entry_fee": 0
|
"guest_entry_fee": 0
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "activity.activitytype",
|
||||||
|
"pk": 5,
|
||||||
|
"fields": {
|
||||||
|
"name": "Soir\u00e9e avec entrées",
|
||||||
|
"manage_entries": true,
|
||||||
|
"can_invite": false,
|
||||||
|
"guest_entry_fee": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "activity.activitytype",
|
||||||
|
"pk": 7,
|
||||||
|
"fields": {
|
||||||
|
"name": "Soir\u00e9e avec invitations",
|
||||||
|
"manage_entries": true,
|
||||||
|
"can_invite": true,
|
||||||
|
"guest_entry_fee": 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
@ -43,7 +43,7 @@ class ActivityForm(forms.ModelForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Activity
|
model = Activity
|
||||||
exclude = ('creater', 'valid', 'open', )
|
exclude = ('creater', 'valid', 'open', 'opener', )
|
||||||
widgets = {
|
widgets = {
|
||||||
"organizer": Autocomplete(
|
"organizer": Autocomplete(
|
||||||
model=Club,
|
model=Club,
|
||||||
|
18
apps/activity/migrations/0003_auto_20240323_1422.py
Normal file
18
apps/activity/migrations/0003_auto_20240323_1422.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 2.2.28 on 2024-03-23 13:22
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('activity', '0002_auto_20200904_2341'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='activity',
|
||||||
|
name='description',
|
||||||
|
field=models.TextField(blank=True, default='', verbose_name='description'),
|
||||||
|
),
|
||||||
|
]
|
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')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
import os
|
import os
|
||||||
@ -11,7 +11,7 @@ from django.db import models, transaction
|
|||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from note.models import NoteUser, Transaction
|
from note.models import NoteUser, Transaction, Note
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
@ -66,6 +66,8 @@ class Activity(models.Model):
|
|||||||
|
|
||||||
description = models.TextField(
|
description = models.TextField(
|
||||||
verbose_name=_('description'),
|
verbose_name=_('description'),
|
||||||
|
blank=True,
|
||||||
|
default="",
|
||||||
)
|
)
|
||||||
|
|
||||||
location = models.CharField(
|
location = models.CharField(
|
||||||
@ -123,6 +125,14 @@ class Activity(models.Model):
|
|||||||
verbose_name=_('open'),
|
verbose_name=_('open'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("activity")
|
||||||
|
verbose_name_plural = _("activities")
|
||||||
|
unique_together = ("name", "date_start", "date_end",)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
@ -144,14 +154,6 @@ class Activity(models.Model):
|
|||||||
if settings.DATABASES["default"]["ENGINE"] == 'django.db.backends.postgresql' else refresh_activities()
|
if settings.DATABASES["default"]["ENGINE"] == 'django.db.backends.postgresql' else refresh_activities()
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _("activity")
|
|
||||||
verbose_name_plural = _("activities")
|
|
||||||
unique_together = ("name", "date_start", "date_end",)
|
|
||||||
|
|
||||||
|
|
||||||
class Entry(models.Model):
|
class Entry(models.Model):
|
||||||
"""
|
"""
|
||||||
@ -252,14 +254,13 @@ class Guest(models.Model):
|
|||||||
verbose_name=_("inviter"),
|
verbose_name=_("inviter"),
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
class Meta:
|
||||||
def has_entry(self):
|
verbose_name = _("guest")
|
||||||
try:
|
verbose_name_plural = _("guests")
|
||||||
if self.entry:
|
unique_together = ("activity", "last_name", "first_name", )
|
||||||
return True
|
|
||||||
return False
|
def __str__(self):
|
||||||
except AttributeError:
|
return self.first_name + " " + self.last_name
|
||||||
return False
|
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
|
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
|
||||||
@ -290,13 +291,14 @@ class Guest(models.Model):
|
|||||||
|
|
||||||
return super().save(force_insert, force_update, using, update_fields)
|
return super().save(force_insert, force_update, using, update_fields)
|
||||||
|
|
||||||
def __str__(self):
|
@property
|
||||||
return self.first_name + " " + self.last_name
|
def has_entry(self):
|
||||||
|
try:
|
||||||
class Meta:
|
if self.entry:
|
||||||
verbose_name = _("guest")
|
return True
|
||||||
verbose_name_plural = _("guests")
|
return False
|
||||||
unique_together = ("activity", "last_name", "first_name", )
|
except AttributeError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class GuestTransaction(Transaction):
|
class GuestTransaction(Transaction):
|
||||||
@ -308,3 +310,31 @@ class GuestTransaction(Transaction):
|
|||||||
@property
|
@property
|
||||||
def type(self):
|
def type(self):
|
||||||
return _('Invitation')
|
return _('Invitation')
|
||||||
|
|
||||||
|
|
||||||
|
class Opener(models.Model):
|
||||||
|
"""
|
||||||
|
Allow the user to make activity entries without more rights
|
||||||
|
"""
|
||||||
|
activity = models.ForeignKey(
|
||||||
|
Activity,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='opener',
|
||||||
|
verbose_name=_('activity')
|
||||||
|
)
|
||||||
|
|
||||||
|
opener = models.ForeignKey(
|
||||||
|
Note,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='activity_responsible',
|
||||||
|
verbose_name=_('Opener')
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Opener")
|
||||||
|
verbose_name_plural = _("Openers")
|
||||||
|
unique_together = ("opener", "activity")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return _("{opener} is opener of activity {acivity}").format(
|
||||||
|
opener=str(self.opener), acivity=str(self.activity))
|
||||||
|
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-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.html import escape
|
from django.utils.html import escape
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from note_kfet.middlewares import get_current_request
|
||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
from django_tables2 import A
|
from django_tables2 import A
|
||||||
|
from permission.backends import PermissionBackend
|
||||||
from note.templatetags.pretty_money import pretty_money
|
from note.templatetags.pretty_money import pretty_money
|
||||||
|
|
||||||
from .models import Activity, Entry, Guest
|
from .models import Activity, Entry, Guest, Opener
|
||||||
|
|
||||||
|
|
||||||
class ActivityTable(tables.Table):
|
class ActivityTable(tables.Table):
|
||||||
@ -113,3 +115,34 @@ class EntryTable(tables.Table):
|
|||||||
'data-last-name': lambda record: record.last_name,
|
'data-last-name': lambda record: record.last_name,
|
||||||
'data-first-name': lambda record: record.first_name,
|
'data-first-name': lambda record: record.first_name,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# function delete_button(id) provided in template file
|
||||||
|
DELETE_TEMPLATE = """
|
||||||
|
<button id="{{ record.pk }}" class="btn btn-danger btn-sm" onclick="delete_button(this.id)"> {{ delete_trans }}</button>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class OpenerTable(tables.Table):
|
||||||
|
class Meta:
|
||||||
|
attrs = {
|
||||||
|
'class': 'table table condensed table-striped',
|
||||||
|
'id': "opener_table"
|
||||||
|
}
|
||||||
|
model = Opener
|
||||||
|
fields = ("opener",)
|
||||||
|
template_name = 'django_tables2/bootstrap4.html'
|
||||||
|
|
||||||
|
show_header = False
|
||||||
|
opener = tables.Column(attrs={'td': {'class': 'text-center'}})
|
||||||
|
|
||||||
|
delete_col = tables.TemplateColumn(
|
||||||
|
template_code=DELETE_TEMPLATE,
|
||||||
|
extra_context={"delete_trans": _('Delete')},
|
||||||
|
attrs={
|
||||||
|
'td': {
|
||||||
|
'class': lambda record: 'col-sm-1'
|
||||||
|
+ (' d-none' if not PermissionBackend.check_perm(
|
||||||
|
get_current_request(), "activity.delete_opener", record)
|
||||||
|
else '')}},
|
||||||
|
verbose_name=_("Delete"),)
|
||||||
|
@ -4,11 +4,31 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
{% load i18n perms %}
|
{% load i18n perms %}
|
||||||
{% load render_table from django_tables2 %}
|
{% load render_table from django_tables2 %}
|
||||||
|
{% load static django_tables2 i18n %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1 class="text-white">{{ title }}</h1>
|
<h1 class="text-white">{{ title }}</h1>
|
||||||
{% include "activity/includes/activity_info.html" %}
|
{% include "activity/includes/activity_info.html" %}
|
||||||
|
|
||||||
|
{% if activity.activity_type.manage_entries and ".change__opener"|has_perm:activity %}
|
||||||
|
<div class="card bg-white mb-3">
|
||||||
|
<h3 class="card-header text-center">
|
||||||
|
{% trans "Openers" %}
|
||||||
|
</h3>
|
||||||
|
<div class="card-body">
|
||||||
|
<form class="input-group" method="POST" id="form_opener">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="activity" value="{{ object.pk }}">
|
||||||
|
{%include "autocomplete_model.html" %}
|
||||||
|
<div class="input-group-append">
|
||||||
|
<input type="submit" class="btn btn-success" value="{% trans "Add" %}">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% render_table opener %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if guests.data %}
|
{% if guests.data %}
|
||||||
<div class="card bg-white mb-3">
|
<div class="card bg-white mb-3">
|
||||||
<h3 class="card-header text-center">
|
<h3 class="card-header text-center">
|
||||||
@ -22,6 +42,8 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extrajavascript %}
|
{% block extrajavascript %}
|
||||||
|
<script src="{% static "activity/js/opener.js" %}"></script>
|
||||||
|
<script src="{% static "js/autocomplete_model.js" %}"></script>
|
||||||
<script>
|
<script>
|
||||||
function remove_guest(guest_id) {
|
function remove_guest(guest_id) {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
|
@ -17,4 +17,27 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extrajavascript %}
|
||||||
|
<script>
|
||||||
|
var date_end = document.getElementById("id_date_end");
|
||||||
|
var date_start = document.getElementById("id_date_start");
|
||||||
|
|
||||||
|
function update_date_end (){
|
||||||
|
if(date_end.value=="" || date_end.value<date_start.value){
|
||||||
|
date_end.value = date_start.value;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function update_date_start (){
|
||||||
|
if(date_start.value=="" || date_end.value<date_start.value){
|
||||||
|
date_start.value = date_end.value;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
date_start.addEventListener('focusout', update_date_end);
|
||||||
|
date_end.addEventListener('focusout', update_date_start);
|
||||||
|
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
@ -46,4 +46,4 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
</h3>
|
</h3>
|
||||||
{% render_table table %}
|
{% render_table table %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from hashlib import md5
|
from hashlib import md5
|
||||||
@ -17,14 +17,16 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from django.views import View
|
from django.views import View
|
||||||
from django.views.decorators.cache import cache_page
|
from django.views.decorators.cache import cache_page
|
||||||
from django.views.generic import DetailView, TemplateView, UpdateView
|
from django.views.generic import DetailView, TemplateView, UpdateView
|
||||||
from django_tables2.views import SingleTableView
|
from django.views.generic.list import ListView
|
||||||
|
from django_tables2.views import MultiTableMixin, SingleTableMixin
|
||||||
|
from api.viewsets import is_regex
|
||||||
from note.models import Alias, NoteSpecial, NoteUser
|
from note.models import Alias, NoteSpecial, NoteUser
|
||||||
from permission.backends import PermissionBackend
|
from permission.backends import PermissionBackend
|
||||||
from permission.views import ProtectQuerysetMixin, ProtectedCreateView
|
from permission.views import ProtectQuerysetMixin, ProtectedCreateView
|
||||||
|
|
||||||
from .forms import ActivityForm, GuestForm
|
from .forms import ActivityForm, GuestForm
|
||||||
from .models import Activity, Entry, Guest
|
from .models import Activity, Entry, Guest, Opener
|
||||||
from .tables import ActivityTable, EntryTable, GuestTable
|
from .tables import ActivityTable, EntryTable, GuestTable, OpenerTable
|
||||||
|
|
||||||
|
|
||||||
class ActivityCreateView(ProtectQuerysetMixin, ProtectedCreateView):
|
class ActivityCreateView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||||
@ -57,26 +59,36 @@ class ActivityCreateView(ProtectQuerysetMixin, ProtectedCreateView):
|
|||||||
return reverse_lazy('activity:activity_detail', kwargs={"pk": self.object.pk})
|
return reverse_lazy('activity:activity_detail', kwargs={"pk": self.object.pk})
|
||||||
|
|
||||||
|
|
||||||
class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, ListView):
|
||||||
"""
|
"""
|
||||||
Displays all Activities, and classify if they are on-going or upcoming ones.
|
Displays all Activities, and classify if they are on-going or upcoming ones.
|
||||||
"""
|
"""
|
||||||
model = Activity
|
model = Activity
|
||||||
table_class = ActivityTable
|
tables = [
|
||||||
ordering = ('-date_start',)
|
lambda data: ActivityTable(data, prefix="all-"),
|
||||||
|
lambda data: ActivityTable(data, prefix="upcoming-"),
|
||||||
|
]
|
||||||
extra_context = {"title": _("Activities")}
|
extra_context = {"title": _("Activities")}
|
||||||
|
|
||||||
def get_queryset(self, **kwargs):
|
def get_queryset(self, **kwargs):
|
||||||
return super().get_queryset(**kwargs).distinct()
|
return super().get_queryset(**kwargs).distinct()
|
||||||
|
|
||||||
|
def get_tables_data(self):
|
||||||
|
# first table = all activities, second table = upcoming
|
||||||
|
return [
|
||||||
|
self.get_queryset().order_by("-date_start"),
|
||||||
|
Activity.objects.filter(date_end__gt=timezone.now())
|
||||||
|
.filter(PermissionBackend.filter_queryset(self.request, Activity, "view"))
|
||||||
|
.distinct()
|
||||||
|
.order_by("date_start")
|
||||||
|
]
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
upcoming_activities = Activity.objects.filter(date_end__gt=timezone.now())
|
tables = context["tables"]
|
||||||
context['upcoming'] = ActivityTable(
|
for name, table in zip(["table", "upcoming"], tables):
|
||||||
data=upcoming_activities.filter(PermissionBackend.filter_queryset(self.request, Activity, "view")),
|
context[name] = table
|
||||||
prefix='upcoming-',
|
|
||||||
)
|
|
||||||
|
|
||||||
started_activities = self.get_queryset().filter(open=True, valid=True).distinct().all()
|
started_activities = self.get_queryset().filter(open=True, valid=True).distinct().all()
|
||||||
context["started_activities"] = started_activities
|
context["started_activities"] = started_activities
|
||||||
@ -84,7 +96,7 @@ class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, DetailView):
|
||||||
"""
|
"""
|
||||||
Shows details about one activity. Add guest to context
|
Shows details about one activity. Add guest to context
|
||||||
"""
|
"""
|
||||||
@ -92,15 +104,40 @@ class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
|||||||
context_object_name = "activity"
|
context_object_name = "activity"
|
||||||
extra_context = {"title": _("Activity detail")}
|
extra_context = {"title": _("Activity detail")}
|
||||||
|
|
||||||
|
tables = [
|
||||||
|
lambda data: GuestTable(data, prefix="guests-"),
|
||||||
|
lambda data: OpenerTable(data, prefix="opener-"),
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_tables_data(self):
|
||||||
|
return [
|
||||||
|
Guest.objects.filter(activity=self.object)
|
||||||
|
.filter(PermissionBackend.filter_queryset(self.request, Guest, "view")),
|
||||||
|
self.object.opener.filter(activity=self.object)
|
||||||
|
.filter(PermissionBackend.filter_queryset(self.request, Opener, "view")),
|
||||||
|
]
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data()
|
context = super().get_context_data()
|
||||||
|
|
||||||
table = GuestTable(data=Guest.objects.filter(activity=self.object)
|
tables = context["tables"]
|
||||||
.filter(PermissionBackend.filter_queryset(self.request, Guest, "view")))
|
for name, table in zip(["guests", "opener"], tables):
|
||||||
context["guests"] = table
|
context[name] = table
|
||||||
|
|
||||||
context["activity_started"] = timezone.now() > timezone.localtime(self.object.date_start)
|
context["activity_started"] = timezone.now() > timezone.localtime(self.object.date_start)
|
||||||
|
|
||||||
|
context["widget"] = {
|
||||||
|
"name": "opener",
|
||||||
|
"resetable": True,
|
||||||
|
"attrs": {
|
||||||
|
"class": "autocomplete form-control",
|
||||||
|
"id": "opener",
|
||||||
|
"api_url": "/api/note/alias/?note__polymorphic_ctype__model=noteuser",
|
||||||
|
"name_field": "name",
|
||||||
|
"placeholder": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
@ -157,17 +194,22 @@ class ActivityInviteView(ProtectQuerysetMixin, ProtectedCreateView):
|
|||||||
return reverse_lazy('activity:activity_detail', kwargs={"pk": self.kwargs["pk"]})
|
return reverse_lazy('activity:activity_detail', kwargs={"pk": self.kwargs["pk"]})
|
||||||
|
|
||||||
|
|
||||||
class ActivityEntryView(LoginRequiredMixin, TemplateView):
|
class ActivityEntryView(LoginRequiredMixin, SingleTableMixin, TemplateView):
|
||||||
"""
|
"""
|
||||||
Manages entry to an activity
|
Manages entry to an activity
|
||||||
"""
|
"""
|
||||||
template_name = "activity/activity_entry.html"
|
template_name = "activity/activity_entry.html"
|
||||||
|
|
||||||
|
table_class = EntryTable
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Don't display the entry interface if the user has no right to see it (no right to add an entry for itself),
|
Don't display the entry interface if the user has no right to see it (no right to add an entry for itself),
|
||||||
it is closed or doesn't manage entries.
|
it is closed or doesn't manage entries.
|
||||||
"""
|
"""
|
||||||
|
if not self.request.user.is_authenticated:
|
||||||
|
return self.handle_no_permission()
|
||||||
|
|
||||||
activity = Activity.objects.get(pk=self.kwargs["pk"])
|
activity = Activity.objects.get(pk=self.kwargs["pk"])
|
||||||
|
|
||||||
sample_entry = Entry(activity=activity, note=self.request.user.note)
|
sample_entry = Entry(activity=activity, note=self.request.user.note)
|
||||||
@ -194,13 +236,16 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
|
|||||||
|
|
||||||
if "search" in self.request.GET and self.request.GET["search"]:
|
if "search" in self.request.GET and self.request.GET["search"]:
|
||||||
pattern = self.request.GET["search"]
|
pattern = self.request.GET["search"]
|
||||||
if pattern[0] != "^":
|
|
||||||
pattern = "^" + pattern
|
# Check if this is a valid regex. If not, we won't check regex
|
||||||
|
valid_regex = is_regex(pattern)
|
||||||
|
suffix = "__iregex" if valid_regex else "__istartswith"
|
||||||
|
pattern = "^" + pattern if valid_regex and pattern[0] != "^" else pattern
|
||||||
guest_qs = guest_qs.filter(
|
guest_qs = guest_qs.filter(
|
||||||
Q(first_name__iregex=pattern)
|
Q(**{f"first_name{suffix}": pattern})
|
||||||
| Q(last_name__iregex=pattern)
|
| Q(**{f"last_name{suffix}": pattern})
|
||||||
| Q(inviter__alias__name__iregex=pattern)
|
| Q(**{f"inviter__alias__name{suffix}": pattern})
|
||||||
| Q(inviter__alias__normalized_name__iregex=Alias.normalize(pattern))
|
| Q(**{f"inviter__alias__normalized_name{suffix}": Alias.normalize(pattern)})
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
guest_qs = guest_qs.none()
|
guest_qs = guest_qs.none()
|
||||||
@ -232,11 +277,15 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
|
|||||||
|
|
||||||
if "search" in self.request.GET and self.request.GET["search"]:
|
if "search" in self.request.GET and self.request.GET["search"]:
|
||||||
pattern = self.request.GET["search"]
|
pattern = self.request.GET["search"]
|
||||||
|
|
||||||
|
# Check if this is a valid regex. If not, we won't check regex
|
||||||
|
valid_regex = is_regex(pattern)
|
||||||
|
suffix = "__iregex" if valid_regex else "__icontains"
|
||||||
note_qs = note_qs.filter(
|
note_qs = note_qs.filter(
|
||||||
Q(note__noteuser__user__first_name__iregex=pattern)
|
Q(**{f"note__noteuser__user__first_name{suffix}": pattern})
|
||||||
| Q(note__noteuser__user__last_name__iregex=pattern)
|
| Q(**{f"note__noteuser__user__last_name{suffix}": pattern})
|
||||||
| Q(name__iregex=pattern)
|
| Q(**{f"name{suffix}": pattern})
|
||||||
| Q(normalized_name__iregex=Alias.normalize(pattern))
|
| Q(**{f"normalized_name{suffix}": Alias.normalize(pattern)})
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
note_qs = note_qs.none()
|
note_qs = note_qs.none()
|
||||||
@ -248,15 +297,9 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
|
|||||||
if settings.DATABASES[note_qs.db]["ENGINE"] == 'django.db.backends.postgresql' else note_qs.distinct()[:20]
|
if settings.DATABASES[note_qs.db]["ENGINE"] == 'django.db.backends.postgresql' else note_qs.distinct()[:20]
|
||||||
return note_qs
|
return note_qs
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_table_data(self):
|
||||||
"""
|
|
||||||
Query the list of Guest and Note to the activity and add information to makes entry with JS.
|
|
||||||
"""
|
|
||||||
context = super().get_context_data(**kwargs)
|
|
||||||
|
|
||||||
activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request, Activity, "view"))\
|
activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request, Activity, "view"))\
|
||||||
.distinct().get(pk=self.kwargs["pk"])
|
.distinct().get(pk=self.kwargs["pk"])
|
||||||
context["activity"] = activity
|
|
||||||
|
|
||||||
matched = []
|
matched = []
|
||||||
|
|
||||||
@ -269,8 +312,17 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
|
|||||||
note.activity = activity
|
note.activity = activity
|
||||||
matched.append(note)
|
matched.append(note)
|
||||||
|
|
||||||
table = EntryTable(data=matched)
|
return matched
|
||||||
context["table"] = table
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
"""
|
||||||
|
Query the list of Guest and Note to the activity and add information to makes entry with JS.
|
||||||
|
"""
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request, Activity, "view"))\
|
||||||
|
.distinct().get(pk=self.kwargs["pk"])
|
||||||
|
context["activity"] = activity
|
||||||
|
|
||||||
context["entries"] = Entry.objects.filter(activity=activity)
|
context["entries"] = Entry.objects.filter(activity=activity)
|
||||||
|
|
||||||
@ -312,8 +364,8 @@ X-WR-CALNAME:Kfet Calendar
|
|||||||
NAME:Kfet Calendar
|
NAME:Kfet Calendar
|
||||||
CALSCALE:GREGORIAN
|
CALSCALE:GREGORIAN
|
||||||
BEGIN:VTIMEZONE
|
BEGIN:VTIMEZONE
|
||||||
TZID:Europe/Berlin
|
TZID:Europe/Paris
|
||||||
X-LIC-LOCATION:Europe/Berlin
|
X-LIC-LOCATION:Europe/Paris
|
||||||
BEGIN:DAYLIGHT
|
BEGIN:DAYLIGHT
|
||||||
TZOFFSETFROM:+0100
|
TZOFFSETFROM:+0100
|
||||||
TZOFFSETTO:+0200
|
TZOFFSETTO:+0200
|
||||||
@ -335,10 +387,10 @@ END:VTIMEZONE
|
|||||||
DTSTAMP:{"{:%Y%m%dT%H%M%S}".format(activity.date_start)}Z
|
DTSTAMP:{"{:%Y%m%dT%H%M%S}".format(activity.date_start)}Z
|
||||||
UID:{md5((activity.name + "$" + str(activity.id) + str(activity.date_start)).encode("UTF-8")).hexdigest()}
|
UID:{md5((activity.name + "$" + str(activity.id) + str(activity.date_start)).encode("UTF-8")).hexdigest()}
|
||||||
SUMMARY;CHARSET=UTF-8:{self.multilines(activity.name, 75, 22)}
|
SUMMARY;CHARSET=UTF-8:{self.multilines(activity.name, 75, 22)}
|
||||||
DTSTART;TZID=Europe/Berlin:{"{:%Y%m%dT%H%M%S}".format(activity.date_start)}
|
DTSTART:{"{:%Y%m%dT%H%M%S}Z".format(activity.date_start)}
|
||||||
DTEND;TZID=Europe/Berlin:{"{:%Y%m%dT%H%M%S}".format(activity.date_end)}
|
DTEND:{"{:%Y%m%dT%H%M%S}Z".format(activity.date_end)}
|
||||||
LOCATION:{self.multilines(activity.location, 75, 9) if activity.location else "Kfet"}
|
LOCATION:{self.multilines(activity.location, 75, 9) if activity.location else "Kfet"}
|
||||||
DESCRIPTION;CHARSET=UTF-8:""" + self.multilines(activity.description.replace("\n", "\\n"), 75, 26) + """
|
DESCRIPTION;CHARSET=UTF-8:""" + self.multilines(activity.description.replace("\n", "\\n"), 75, 26) + f"""
|
||||||
-- {activity.organizer.name}
|
-- {activity.organizer.name}
|
||||||
END:VEVENT
|
END:VEVENT
|
||||||
"""
|
"""
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
default_app_config = 'api.apps.APIConfig'
|
default_app_config = 'api.apps.APIConfig'
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
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
|
5
apps/api/pagination.py
Normal file
5
apps/api/pagination.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from rest_framework.pagination import PageNumberPagination
|
||||||
|
|
||||||
|
|
||||||
|
class CustomPagination(PageNumberPagination):
|
||||||
|
page_size_query_param = 'page_size'
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
|
||||||
@ -7,8 +7,11 @@ from django.contrib.auth.models import User
|
|||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from member.api.serializers import ProfileSerializer, MembershipSerializer
|
from member.api.serializers import ProfileSerializer, MembershipSerializer
|
||||||
|
from member.models import Membership
|
||||||
from note.api.serializers import NoteSerializer
|
from note.api.serializers import NoteSerializer
|
||||||
from note.models import Alias
|
from note.models import Alias
|
||||||
|
from note_kfet.middlewares import get_current_request
|
||||||
|
from permission.backends import PermissionBackend
|
||||||
|
|
||||||
|
|
||||||
class UserSerializer(serializers.ModelSerializer):
|
class UserSerializer(serializers.ModelSerializer):
|
||||||
@ -45,18 +48,30 @@ class OAuthSerializer(serializers.ModelSerializer):
|
|||||||
"""
|
"""
|
||||||
normalized_name = serializers.SerializerMethodField()
|
normalized_name = serializers.SerializerMethodField()
|
||||||
|
|
||||||
profile = ProfileSerializer()
|
profile = serializers.SerializerMethodField()
|
||||||
|
|
||||||
note = NoteSerializer()
|
note = serializers.SerializerMethodField()
|
||||||
|
|
||||||
memberships = serializers.SerializerMethodField()
|
memberships = serializers.SerializerMethodField()
|
||||||
|
|
||||||
def get_normalized_name(self, obj):
|
def get_normalized_name(self, obj):
|
||||||
return Alias.normalize(obj.username)
|
return Alias.normalize(obj.username)
|
||||||
|
|
||||||
|
def get_profile(self, obj):
|
||||||
|
# Display the profile of the user only if we have rights to see it.
|
||||||
|
return ProfileSerializer().to_representation(obj.profile) \
|
||||||
|
if PermissionBackend.check_perm(get_current_request(), 'member.view_profile', obj.profile) else None
|
||||||
|
|
||||||
|
def get_note(self, obj):
|
||||||
|
# Display the note of the user only if we have rights to see it.
|
||||||
|
return NoteSerializer().to_representation(obj.note) \
|
||||||
|
if PermissionBackend.check_perm(get_current_request(), 'note.view_note', obj.note) else None
|
||||||
|
|
||||||
def get_memberships(self, obj):
|
def get_memberships(self, obj):
|
||||||
|
# Display only memberships that we are allowed to see.
|
||||||
return serializers.ListSerializer(child=MembershipSerializer()).to_representation(
|
return serializers.ListSerializer(child=MembershipSerializer()).to_representation(
|
||||||
obj.memberships.filter(date_start__lte=timezone.now(), date_end__gte=timezone.now()))
|
obj.memberships.filter(date_start__lte=timezone.now(), date_end__gte=timezone.now())
|
||||||
|
.filter(PermissionBackend.filter_queryset(get_current_request(), Membership, 'view')))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = User
|
model = User
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
import json
|
import json
|
||||||
@ -12,11 +12,12 @@ from django.contrib.contenttypes.models import ContentType
|
|||||||
from django.db.models.fields.files import ImageFieldFile
|
from django.db.models.fields.files import ImageFieldFile
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
|
from phonenumbers import PhoneNumber
|
||||||
|
from rest_framework.filters import OrderingFilter
|
||||||
|
from api.filters import RegexSafeSearchFilter
|
||||||
from member.models import Membership, Club
|
from member.models import Membership, Club
|
||||||
from note.models import NoteClub, NoteUser, Alias, Note
|
from note.models import NoteClub, NoteUser, Alias, Note
|
||||||
from permission.models import PermissionMask, Permission, Role
|
from permission.models import PermissionMask, Permission, Role
|
||||||
from phonenumbers import PhoneNumber
|
|
||||||
from rest_framework.filters import SearchFilter, OrderingFilter
|
|
||||||
|
|
||||||
from .viewsets import ContentTypeViewSet, UserViewSet
|
from .viewsets import ContentTypeViewSet, UserViewSet
|
||||||
|
|
||||||
@ -87,7 +88,7 @@ class TestAPI(TestCase):
|
|||||||
resp = self.client.get(url + f"?ordering=-{field}")
|
resp = self.client.get(url + f"?ordering=-{field}")
|
||||||
self.assertEqual(resp.status_code, 200)
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
if SearchFilter in backends:
|
if RegexSafeSearchFilter in backends:
|
||||||
# Basic search
|
# Basic search
|
||||||
for field in viewset.search_fields:
|
for field in viewset.search_fields:
|
||||||
obj = self.fix_note_object(obj, field)
|
obj = self.fix_note_object(obj, field)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
@ -1,19 +1,29 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from rest_framework.filters import SearchFilter
|
|
||||||
from rest_framework.viewsets import ReadOnlyModelViewSet, ModelViewSet
|
from rest_framework.viewsets import ReadOnlyModelViewSet, ModelViewSet
|
||||||
from permission.backends import PermissionBackend
|
from permission.backends import PermissionBackend
|
||||||
from note.models import Alias
|
from note.models import Alias
|
||||||
|
|
||||||
|
from .filters import RegexSafeSearchFilter
|
||||||
from .serializers import UserSerializer, ContentTypeSerializer
|
from .serializers import UserSerializer, ContentTypeSerializer
|
||||||
|
|
||||||
|
|
||||||
|
def is_regex(pattern):
|
||||||
|
try:
|
||||||
|
re.compile(pattern)
|
||||||
|
return True
|
||||||
|
except (re.error, TypeError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class ReadProtectedModelViewSet(ModelViewSet):
|
class ReadProtectedModelViewSet(ModelViewSet):
|
||||||
"""
|
"""
|
||||||
Protect a ModelViewSet by filtering the objects that the user cannot see.
|
Protect a ModelViewSet by filtering the objects that the user cannot see.
|
||||||
@ -60,34 +70,38 @@ class UserViewSet(ReadProtectedModelViewSet):
|
|||||||
|
|
||||||
if "search" in self.request.GET:
|
if "search" in self.request.GET:
|
||||||
pattern = self.request.GET["search"]
|
pattern = self.request.GET["search"]
|
||||||
|
# Check if this is a valid regex. If not, we won't check regex
|
||||||
|
valid_regex = is_regex(pattern)
|
||||||
|
suffix = "__iregex" if valid_regex else "__istartswith"
|
||||||
|
prefix = "^" if valid_regex else ""
|
||||||
|
|
||||||
# Filter with different rules
|
# Filter with different rules
|
||||||
# We use union-all to keep each filter rule sorted in result
|
# We use union-all to keep each filter rule sorted in result
|
||||||
queryset = queryset.filter(
|
queryset = queryset.filter(
|
||||||
# Match without normalization
|
# Match without normalization
|
||||||
note__alias__name__iregex="^" + pattern
|
Q(**{f"note__alias__name{suffix}": prefix + pattern})
|
||||||
).union(
|
).union(
|
||||||
queryset.filter(
|
queryset.filter(
|
||||||
# Match with normalization
|
# Match with normalization
|
||||||
Q(note__alias__normalized_name__iregex="^" + Alias.normalize(pattern))
|
Q(**{f"note__alias__normalized_name{suffix}": prefix + Alias.normalize(pattern)})
|
||||||
& ~Q(note__alias__name__iregex="^" + pattern)
|
& ~Q(**{f"note__alias__name{suffix}": prefix + pattern})
|
||||||
),
|
),
|
||||||
all=True,
|
all=True,
|
||||||
).union(
|
).union(
|
||||||
queryset.filter(
|
queryset.filter(
|
||||||
# Match on lower pattern
|
# Match on lower pattern
|
||||||
Q(note__alias__normalized_name__iregex="^" + pattern.lower())
|
Q(**{f"note__alias__normalized_name{suffix}": prefix + pattern.lower()})
|
||||||
& ~Q(note__alias__normalized_name__iregex="^" + Alias.normalize(pattern))
|
& ~Q(**{f"note__alias__normalized_name{suffix}": prefix + Alias.normalize(pattern)})
|
||||||
& ~Q(note__alias__name__iregex="^" + pattern)
|
& ~Q(**{f"note__alias__name{suffix}": prefix + pattern})
|
||||||
),
|
),
|
||||||
all=True,
|
all=True,
|
||||||
).union(
|
).union(
|
||||||
queryset.filter(
|
queryset.filter(
|
||||||
# Match on firstname or lastname
|
# Match on firstname or lastname
|
||||||
(Q(last_name__iregex="^" + pattern) | Q(first_name__iregex="^" + pattern))
|
(Q(**{f"last_name{suffix}": prefix + pattern}) | Q(**{f"first_name{suffix}": prefix + pattern}))
|
||||||
& ~Q(note__alias__normalized_name__iregex="^" + pattern.lower())
|
& ~Q(**{f"note__alias__normalized_name{suffix}": prefix + pattern.lower()})
|
||||||
& ~Q(note__alias__normalized_name__iregex="^" + Alias.normalize(pattern))
|
& ~Q(**{f"note__alias__normalized_name{suffix}": prefix + Alias.normalize(pattern)})
|
||||||
& ~Q(note__alias__name__iregex="^" + pattern)
|
& ~Q(**{f"note__alias__name{suffix}": prefix + pattern})
|
||||||
),
|
),
|
||||||
all=True,
|
all=True,
|
||||||
)
|
)
|
||||||
@ -107,6 +121,6 @@ class ContentTypeViewSet(ReadOnlyModelViewSet):
|
|||||||
"""
|
"""
|
||||||
queryset = ContentType.objects.order_by('id')
|
queryset = ContentType.objects.order_by('id')
|
||||||
serializer_class = ContentTypeSerializer
|
serializer_class = ContentTypeSerializer
|
||||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||||
filterset_fields = ['id', 'app_label', 'model', ]
|
filterset_fields = ['id', 'app_label', 'model', ]
|
||||||
search_fields = ['$app_label', '$model', ]
|
search_fields = ['$app_label', '$model', ]
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
default_app_config = 'logs.apps.LogsConfig'
|
default_app_config = 'logs.apps.LogsConfig'
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from .views import ChangelogViewSet
|
from .views import ChangelogViewSet
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -76,9 +76,6 @@ class Changelog(models.Model):
|
|||||||
verbose_name=_('timestamp'),
|
verbose_name=_('timestamp'),
|
||||||
)
|
)
|
||||||
|
|
||||||
def delete(self, using=None, keep_parents=False):
|
|
||||||
raise ValidationError(_("Logs cannot be destroyed."))
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("changelog")
|
verbose_name = _("changelog")
|
||||||
verbose_name_plural = _("changelogs")
|
verbose_name_plural = _("changelogs")
|
||||||
@ -86,3 +83,6 @@ class Changelog(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return _("Changelog of type \"{action}\" for model {model} at {timestamp}").format(
|
return _("Changelog of type \"{action}\" for model {model} at {timestamp}").format(
|
||||||
action=self.get_action_display(), model=str(self.model), timestamp=str(self.timestamp))
|
action=self.get_action_display(), model=str(self.model), timestamp=str(self.timestamp))
|
||||||
|
|
||||||
|
def delete(self, using=None, keep_parents=False):
|
||||||
|
raise ValidationError(_("Logs cannot be destroyed."))
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
@ -56,13 +56,13 @@ def save_object(sender, instance, **kwargs):
|
|||||||
# noinspection PyProtectedMember
|
# noinspection PyProtectedMember
|
||||||
previous = instance._previous
|
previous = instance._previous
|
||||||
|
|
||||||
# Si un 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()
|
request = get_current_request()
|
||||||
|
|
||||||
if request is None:
|
if request is None:
|
||||||
# Si la modification n'a pas été faite via le client Web, on suppose que c'est du à `manage.py`
|
# Si la modification n'a pas été faite via le client Web, on suppose que c'est du à `manage.py`
|
||||||
# On récupère alors l'utilisateur·trice connecté·e à la VM, et on récupère la note associée
|
# On récupère alors l'utilisateur·trice connecté·e à la VM, et on récupère la note associée
|
||||||
# IMPORTANT : l'utilisateur 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"
|
ip = "127.0.0.1"
|
||||||
username = Alias.normalize(getpass.getuser())
|
username = Alias.normalize(getpass.getuser())
|
||||||
note = NoteUser.objects.filter(alias__normalized_name=username)
|
note = NoteUser.objects.filter(alias__normalized_name=username)
|
||||||
@ -134,13 +134,13 @@ def delete_object(sender, instance, **kwargs):
|
|||||||
if instance._meta.label_lower in EXCLUDED or hasattr(instance, "_no_signal"):
|
if instance._meta.label_lower in EXCLUDED or hasattr(instance, "_no_signal"):
|
||||||
return
|
return
|
||||||
|
|
||||||
# Si un 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()
|
request = get_current_request()
|
||||||
|
|
||||||
if request is None:
|
if request is None:
|
||||||
# Si la modification n'a pas été faite via le client Web, on suppose que c'est du à `manage.py`
|
# Si la modification n'a pas été faite via le client Web, on suppose que c'est du à `manage.py`
|
||||||
# On récupère alors l'utilisateur·trice connecté·e à la VM, et on récupère la note associée
|
# On récupère alors l'utilisateur·trice connecté·e à la VM, et on récupère la note associée
|
||||||
# IMPORTANT : l'utilisateur 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"
|
ip = "127.0.0.1"
|
||||||
username = Alias.normalize(getpass.getuser())
|
username = Alias.normalize(getpass.getuser())
|
||||||
note = NoteUser.objects.filter(alias__normalized_name=username)
|
note = NoteUser.objects.filter(alias__normalized_name=username)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
default_app_config = 'member.apps.MemberConfig'
|
default_app_config = 'member.apps.MemberConfig'
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from .views import ProfileViewSet, ClubViewSet, MembershipViewSet
|
from .views import ProfileViewSet, ClubViewSet, MembershipViewSet
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
from rest_framework.filters import OrderingFilter, SearchFilter
|
from rest_framework.filters import OrderingFilter
|
||||||
|
from api.filters import RegexSafeSearchFilter
|
||||||
from api.viewsets import ReadProtectedModelViewSet
|
from api.viewsets import ReadProtectedModelViewSet
|
||||||
|
|
||||||
from .serializers import ProfileSerializer, ClubSerializer, MembershipSerializer
|
from .serializers import ProfileSerializer, ClubSerializer, MembershipSerializer
|
||||||
@ -17,7 +18,7 @@ class ProfileViewSet(ReadProtectedModelViewSet):
|
|||||||
"""
|
"""
|
||||||
queryset = Profile.objects.order_by('id')
|
queryset = Profile.objects.order_by('id')
|
||||||
serializer_class = ProfileSerializer
|
serializer_class = ProfileSerializer
|
||||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||||
filterset_fields = ['user', 'user__first_name', 'user__last_name', 'user__username', 'user__email',
|
filterset_fields = ['user', 'user__first_name', 'user__last_name', 'user__username', 'user__email',
|
||||||
'user__note__alias__name', 'user__note__alias__normalized_name', 'phone_number', "section",
|
'user__note__alias__name', 'user__note__alias__normalized_name', 'phone_number', "section",
|
||||||
'department', 'promotion', 'address', 'paid', 'ml_events_registration', 'ml_sport_registration',
|
'department', 'promotion', 'address', 'paid', 'ml_events_registration', 'ml_sport_registration',
|
||||||
@ -34,7 +35,7 @@ class ClubViewSet(ReadProtectedModelViewSet):
|
|||||||
"""
|
"""
|
||||||
queryset = Club.objects.order_by('id')
|
queryset = Club.objects.order_by('id')
|
||||||
serializer_class = ClubSerializer
|
serializer_class = ClubSerializer
|
||||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||||
filterset_fields = ['name', 'email', 'note__alias__name', 'note__alias__normalized_name', 'parent_club',
|
filterset_fields = ['name', 'email', 'note__alias__name', 'note__alias__normalized_name', 'parent_club',
|
||||||
'parent_club__name', 'require_memberships', 'membership_fee_paid', 'membership_fee_unpaid',
|
'parent_club__name', 'require_memberships', 'membership_fee_paid', 'membership_fee_unpaid',
|
||||||
'membership_duration', 'membership_start', 'membership_end', ]
|
'membership_duration', 'membership_start', 'membership_end', ]
|
||||||
@ -49,7 +50,7 @@ class MembershipViewSet(ReadProtectedModelViewSet):
|
|||||||
"""
|
"""
|
||||||
queryset = Membership.objects.order_by('id')
|
queryset = Membership.objects.order_by('id')
|
||||||
serializer_class = MembershipSerializer
|
serializer_class = MembershipSerializer
|
||||||
filter_backends = [DjangoFilterBackend, OrderingFilter, SearchFilter]
|
filter_backends = [DjangoFilterBackend, OrderingFilter, RegexSafeSearchFilter]
|
||||||
filterset_fields = ['club__name', 'club__email', 'club__note__alias__name', 'club__note__alias__normalized_name',
|
filterset_fields = ['club__name', 'club__email', 'club__note__alias__name', 'club__note__alias__normalized_name',
|
||||||
'user__username', 'user__last_name', 'user__first_name', 'user__email',
|
'user__username', 'user__last_name', 'user__first_name', 'user__email',
|
||||||
'user__note__alias__name', 'user__note__alias__normalized_name',
|
'user__note__alias__name', 'user__note__alias__normalized_name',
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from cas_server.auth import DjangoAuthUser # pragma: no cover
|
from cas_server.auth import DjangoAuthUser # pragma: no cover
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
import io
|
import io
|
||||||
@ -47,6 +47,13 @@ class ProfileForm(forms.ModelForm):
|
|||||||
|
|
||||||
last_report = forms.DateTimeField(required=False, disabled=True, label=_("Last report date"))
|
last_report = forms.DateTimeField(required=False, disabled=True, label=_("Last report date"))
|
||||||
|
|
||||||
|
VSS_charter_read = forms.BooleanField(
|
||||||
|
required=True,
|
||||||
|
label=_("Anti-VSS (<em>Violences Sexistes et Sexuelles</em>) charter read and approved"),
|
||||||
|
help_text=_("Tick after having read and accepted the anti-VSS charter \
|
||||||
|
<a href=https://perso.crans.org/club-bde/Charte-anti-VSS.pdf target=_blank> available here in pdf</a>")
|
||||||
|
)
|
||||||
|
|
||||||
def clean_promotion(self):
|
def clean_promotion(self):
|
||||||
promotion = self.cleaned_data["promotion"]
|
promotion = self.cleaned_data["promotion"]
|
||||||
if promotion > timezone.now().year:
|
if promotion > timezone.now().year:
|
||||||
@ -114,7 +121,7 @@ class ImageForm(forms.Form):
|
|||||||
frame = frame.crop((x, y, x + w, y + h))
|
frame = frame.crop((x, y, x + w, y + h))
|
||||||
frame = frame.resize(
|
frame = frame.resize(
|
||||||
(settings.PIC_WIDTH, settings.PIC_RATIO * settings.PIC_WIDTH),
|
(settings.PIC_WIDTH, settings.PIC_RATIO * settings.PIC_WIDTH),
|
||||||
Image.ANTIALIAS,
|
Image.LANCZOS,
|
||||||
)
|
)
|
||||||
frames.append(frame)
|
frames.append(frame)
|
||||||
|
|
||||||
@ -131,6 +138,9 @@ class ImageForm(forms.Form):
|
|||||||
|
|
||||||
return cleaned_data
|
return cleaned_data
|
||||||
|
|
||||||
|
def is_valid(self):
|
||||||
|
return super().is_valid() or super().clean().get('image') is None
|
||||||
|
|
||||||
|
|
||||||
class ClubForm(forms.ModelForm):
|
class ClubForm(forms.ModelForm):
|
||||||
def clean(self):
|
def clean(self):
|
||||||
@ -144,7 +154,7 @@ class ClubForm(forms.ModelForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Club
|
model = Club
|
||||||
fields = '__all__'
|
exclude = ("add_registration_form",)
|
||||||
widgets = {
|
widgets = {
|
||||||
"membership_fee_paid": AmountInput(),
|
"membership_fee_paid": AmountInput(),
|
||||||
"membership_fee_unpaid": AmountInput(),
|
"membership_fee_unpaid": AmountInput(),
|
||||||
@ -200,9 +210,9 @@ class MembershipForm(forms.ModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Membership
|
model = Membership
|
||||||
fields = ('user', 'date_start')
|
fields = ('user', 'date_start')
|
||||||
# Le champ d'utilisateur 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
|
# 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 = {
|
widgets = {
|
||||||
'user':
|
'user':
|
||||||
Autocomplete(
|
Autocomplete(
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
18
apps/member/migrations/0008_auto_20211005_1544.py
Normal file
18
apps/member/migrations/0008_auto_20211005_1544.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 2.2.24 on 2021-10-05 13:44
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('member', '0007_auto_20210313_1235'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='profile',
|
||||||
|
name='department',
|
||||||
|
field=models.CharField(choices=[('A0', 'Informatics (A0)'), ('A1', 'Mathematics (A1)'), ('A2', 'Physics (A2)'), ("A'2", "Applied physics (A'2)"), ("A''2", "Chemistry (A''2)"), ('A3', 'Biology (A3)'), ('B1234', 'SAPHIRE (B1234)'), ('B1', 'Mechanics (B1)'), ('B2', 'Civil engineering (B2)'), ('B3', 'Mechanical engineering (B3)'), ('B4', 'EEA (B4)'), ('C', 'Design (C)'), ('D2', 'Economy-management (D2)'), ('D3', 'Social sciences (D3)'), ('E', 'English (E)'), ('EXT', 'External (EXT)')], max_length=8, verbose_name='department'),
|
||||||
|
),
|
||||||
|
]
|
18
apps/member/migrations/0009_auto_20220904_2325.py
Normal file
18
apps/member/migrations/0009_auto_20220904_2325.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 2.2.26 on 2022-09-04 21:25
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('member', '0008_auto_20211005_1544'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='profile',
|
||||||
|
name='promotion',
|
||||||
|
field=models.PositiveSmallIntegerField(default=2022, help_text='Year of entry to the school (None if not ENS student)', null=True, verbose_name='promotion'),
|
||||||
|
),
|
||||||
|
]
|
18
apps/member/migrations/0010_new_default_year.py
Normal file
18
apps/member/migrations/0010_new_default_year.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 2.2.28 on 2023-08-23 21:29
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('member', '0009_auto_20220904_2325'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='profile',
|
||||||
|
name='promotion',
|
||||||
|
field=models.PositiveSmallIntegerField(default=2023, help_text='Year of entry to the school (None if not ENS student)', null=True, verbose_name='promotion'),
|
||||||
|
),
|
||||||
|
]
|
18
apps/member/migrations/0011_profile_vss_charter_read.py
Normal file
18
apps/member/migrations/0011_profile_vss_charter_read.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 2.2.28 on 2023-08-31 09:50
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('member', '0010_new_default_year'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='profile',
|
||||||
|
name='VSS_charter_read',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='VSS charter read'),
|
||||||
|
),
|
||||||
|
]
|
18
apps/member/migrations/0012_club_add_registration_form.py
Normal file
18
apps/member/migrations/0012_club_add_registration_form.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 2.2.28 on 2024-07-15 09:24
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('member', '0011_profile_vss_charter_read'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='club',
|
||||||
|
name='add_registration_form',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='add to registration form'),
|
||||||
|
),
|
||||||
|
]
|
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-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
@ -28,7 +28,6 @@ class Profile(models.Model):
|
|||||||
We do not want to patch the Django Contrib :model:`auth.User`model;
|
We do not want to patch the Django Contrib :model:`auth.User`model;
|
||||||
so this model add an user profile with additional information.
|
so this model add an user profile with additional information.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
user = models.OneToOneField(
|
user = models.OneToOneField(
|
||||||
settings.AUTH_USER_MODEL,
|
settings.AUTH_USER_MODEL,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
@ -134,6 +133,22 @@ class Profile(models.Model):
|
|||||||
default=False,
|
default=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
VSS_charter_read = models.BooleanField(
|
||||||
|
verbose_name=_("VSS charter read"),
|
||||||
|
default=False
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('user profile')
|
||||||
|
verbose_name_plural = _('user profile')
|
||||||
|
indexes = [models.Index(fields=['user'])]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str(self.user)
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('member:user_detail', args=(self.user_id,))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ens_year(self):
|
def ens_year(self):
|
||||||
"""
|
"""
|
||||||
@ -158,17 +173,6 @@ class Profile(models.Model):
|
|||||||
return SogeCredit.objects.filter(user=self.user, credit_transaction__isnull=False).exists()
|
return SogeCredit.objects.filter(user=self.user, credit_transaction__isnull=False).exists()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _('user profile')
|
|
||||||
verbose_name_plural = _('user profile')
|
|
||||||
indexes = [models.Index(fields=['user'])]
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse('member:user_detail', args=(self.user_id,))
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return str(self.user)
|
|
||||||
|
|
||||||
def send_email_validation_link(self):
|
def send_email_validation_link(self):
|
||||||
subject = "[Note Kfet] " + str(_("Activate your Note Kfet account"))
|
subject = "[Note Kfet] " + str(_("Activate your Note Kfet account"))
|
||||||
token = email_validation_token.make_token(self.user)
|
token = email_validation_token.make_token(self.user)
|
||||||
@ -200,9 +204,11 @@ class Club(models.Model):
|
|||||||
max_length=255,
|
max_length=255,
|
||||||
unique=True,
|
unique=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
email = models.EmailField(
|
email = models.EmailField(
|
||||||
verbose_name=_('email'),
|
verbose_name=_('email'),
|
||||||
)
|
)
|
||||||
|
|
||||||
parent_club = models.ForeignKey(
|
parent_club = models.ForeignKey(
|
||||||
'self',
|
'self',
|
||||||
null=True,
|
null=True,
|
||||||
@ -253,23 +259,17 @@ class Club(models.Model):
|
|||||||
help_text=_('Maximal date of a membership, after which members must renew it.'),
|
help_text=_('Maximal date of a membership, after which members must renew it.'),
|
||||||
)
|
)
|
||||||
|
|
||||||
def update_membership_dates(self):
|
add_registration_form = models.BooleanField(
|
||||||
"""
|
verbose_name=_("add to registration form"),
|
||||||
This function is called each time the club detail view is displayed.
|
default=False,
|
||||||
Update the year of the membership dates.
|
)
|
||||||
"""
|
|
||||||
if not self.membership_start:
|
|
||||||
return
|
|
||||||
|
|
||||||
today = datetime.date.today()
|
class Meta:
|
||||||
|
verbose_name = _("club")
|
||||||
|
verbose_name_plural = _("clubs")
|
||||||
|
|
||||||
if (today - self.membership_start).days >= 365:
|
def __str__(self):
|
||||||
self.membership_start = datetime.date(self.membership_start.year + 1,
|
return self.name
|
||||||
self.membership_start.month, self.membership_start.day)
|
|
||||||
self.membership_end = datetime.date(self.membership_end.year + 1,
|
|
||||||
self.membership_end.month, self.membership_end.day)
|
|
||||||
self._force_save = True
|
|
||||||
self.save(force_update=True)
|
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def save(self, force_insert=False, force_update=False, using=None,
|
def save(self, force_insert=False, force_update=False, using=None,
|
||||||
@ -282,16 +282,36 @@ class Club(models.Model):
|
|||||||
self.membership_end = None
|
self.membership_end = None
|
||||||
super().save(force_insert, force_update, update_fields)
|
super().save(force_insert, force_update, update_fields)
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _("club")
|
|
||||||
verbose_name_plural = _("clubs")
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse_lazy('member:club_detail', args=(self.pk,))
|
return reverse_lazy('member:club_detail', args=(self.pk,))
|
||||||
|
|
||||||
|
def update_membership_dates(self):
|
||||||
|
"""
|
||||||
|
This function is called each time the club detail view is displayed.
|
||||||
|
Update the year of the membership dates.
|
||||||
|
"""
|
||||||
|
if not self.membership_start or not self.membership_end:
|
||||||
|
return
|
||||||
|
|
||||||
|
today = datetime.date.today()
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
if self.membership_end:
|
||||||
|
self.membership_end = datetime.date(self.membership_end.year + 1,
|
||||||
|
self.membership_end.month, self.membership_end.day)
|
||||||
|
self._force_save = True
|
||||||
|
self.save(force_update=True)
|
||||||
|
|
||||||
|
|
||||||
class Membership(models.Model):
|
class Membership(models.Model):
|
||||||
"""
|
"""
|
||||||
@ -331,6 +351,66 @@ class Membership(models.Model):
|
|||||||
verbose_name=_('fee'),
|
verbose_name=_('fee'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('membership')
|
||||||
|
verbose_name_plural = _('memberships')
|
||||||
|
indexes = [models.Index(fields=['user'])]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return _("Membership of {user} for the club {club}").format(user=self.user.username, club=self.club.name, )
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Calculate fee and end date before saving the membership and creating the transaction if needed.
|
||||||
|
"""
|
||||||
|
# Ensure that club membership dates are valid
|
||||||
|
old_membership_start = self.club.membership_start
|
||||||
|
self.club.update_membership_dates()
|
||||||
|
if self.club.membership_start != old_membership_start:
|
||||||
|
self.club.save()
|
||||||
|
|
||||||
|
created = not self.pk
|
||||||
|
if not created:
|
||||||
|
for role in self.roles.all():
|
||||||
|
club = role.for_club
|
||||||
|
if club is not None:
|
||||||
|
if club.pk != self.club_id:
|
||||||
|
raise ValidationError(_('The role {role} does not apply to the club {club}.')
|
||||||
|
.format(role=role.name, club=club.name))
|
||||||
|
else:
|
||||||
|
if Membership.objects.filter(
|
||||||
|
user=self.user,
|
||||||
|
club=self.club,
|
||||||
|
date_start__lte=self.date_start,
|
||||||
|
date_end__gte=self.date_start,
|
||||||
|
).exists():
|
||||||
|
raise ValidationError(_('User is already a member of the club'))
|
||||||
|
|
||||||
|
if self.club.parent_club is not None:
|
||||||
|
# Check that the user is already a member of the parent club if the membership is created
|
||||||
|
if not Membership.objects.filter(
|
||||||
|
user=self.user,
|
||||||
|
club=self.club.parent_club,
|
||||||
|
date_start__gte=self.club.parent_club.membership_start,
|
||||||
|
).exists():
|
||||||
|
if hasattr(self, '_force_renew_parent') and self._force_renew_parent:
|
||||||
|
self.renew_parent()
|
||||||
|
else:
|
||||||
|
raise ValidationError(_('User is not a member of the parent club')
|
||||||
|
+ ' ' + self.club.parent_club.name)
|
||||||
|
|
||||||
|
self.fee = self.club.membership_fee_paid if self.user.profile.paid else self.club.membership_fee_unpaid
|
||||||
|
|
||||||
|
self.date_end = self.date_start + datetime.timedelta(days=self.club.membership_duration) \
|
||||||
|
if self.club.membership_duration is not None else self.date_start + datetime.timedelta(days=424242)
|
||||||
|
if self.club.membership_end is not None and self.date_end > self.club.membership_end:
|
||||||
|
self.date_end = self.club.membership_end
|
||||||
|
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
self.make_transaction()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def valid(self):
|
def valid(self):
|
||||||
"""
|
"""
|
||||||
@ -400,66 +480,14 @@ class Membership(models.Model):
|
|||||||
|
|
||||||
if self.club.parent_club.name == "BDE":
|
if self.club.parent_club.name == "BDE":
|
||||||
parent_membership.roles.set(
|
parent_membership.roles.set(
|
||||||
Role.objects.filter(Q(name="Adhérent 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":
|
elif self.club.parent_club.name == "Kfet":
|
||||||
parent_membership.roles.set(
|
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:
|
else:
|
||||||
parent_membership.roles.set(Role.objects.filter(name="Membre de club").all())
|
parent_membership.roles.set(Role.objects.filter(name="Membre de club").all())
|
||||||
parent_membership.save()
|
parent_membership.save()
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Calculate fee and end date before saving the membership and creating the transaction if needed.
|
|
||||||
"""
|
|
||||||
# Ensure that club membership dates are valid
|
|
||||||
old_membership_start = self.club.membership_start
|
|
||||||
self.club.update_membership_dates()
|
|
||||||
if self.club.membership_start != old_membership_start:
|
|
||||||
self.club.save()
|
|
||||||
|
|
||||||
created = not self.pk
|
|
||||||
if not created:
|
|
||||||
for role in self.roles.all():
|
|
||||||
club = role.for_club
|
|
||||||
if club is not None:
|
|
||||||
if club.pk != self.club_id:
|
|
||||||
raise ValidationError(_('The role {role} does not apply to the club {club}.')
|
|
||||||
.format(role=role.name, club=club.name))
|
|
||||||
else:
|
|
||||||
if Membership.objects.filter(
|
|
||||||
user=self.user,
|
|
||||||
club=self.club,
|
|
||||||
date_start__lte=self.date_start,
|
|
||||||
date_end__gte=self.date_start,
|
|
||||||
).exists():
|
|
||||||
raise ValidationError(_('User is already a member of the club'))
|
|
||||||
|
|
||||||
if self.club.parent_club is not None:
|
|
||||||
# Check that the user is already a member of the parent club if the membership is created
|
|
||||||
if not Membership.objects.filter(
|
|
||||||
user=self.user,
|
|
||||||
club=self.club.parent_club,
|
|
||||||
date_start__gte=self.club.parent_club.membership_start,
|
|
||||||
).exists():
|
|
||||||
if hasattr(self, '_force_renew_parent') and self._force_renew_parent:
|
|
||||||
self.renew_parent()
|
|
||||||
else:
|
|
||||||
raise ValidationError(_('User is not a member of the parent club')
|
|
||||||
+ ' ' + self.club.parent_club.name)
|
|
||||||
|
|
||||||
self.fee = self.club.membership_fee_paid if self.user.profile.paid else self.club.membership_fee_unpaid
|
|
||||||
|
|
||||||
self.date_end = self.date_start + datetime.timedelta(days=self.club.membership_duration) \
|
|
||||||
if self.club.membership_duration is not None else self.date_start + datetime.timedelta(days=424242)
|
|
||||||
if self.club.membership_end is not None and self.date_end > self.club.membership_end:
|
|
||||||
self.date_end = self.club.membership_end
|
|
||||||
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
|
|
||||||
self.make_transaction()
|
|
||||||
|
|
||||||
def make_transaction(self):
|
def make_transaction(self):
|
||||||
"""
|
"""
|
||||||
Create Membership transaction associated to this membership.
|
Create Membership transaction associated to this membership.
|
||||||
@ -497,11 +525,3 @@ class Membership(models.Model):
|
|||||||
soge_credit.save()
|
soge_credit.save()
|
||||||
else:
|
else:
|
||||||
transaction.save(force_insert=True)
|
transaction.save(force_insert=True)
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return _("Membership of {user} for the club {club}").format(user=self.user.username, club=self.club.name, )
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _('membership')
|
|
||||||
verbose_name_plural = _('memberships')
|
|
||||||
indexes = [models.Index(fields=['user'])]
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
|
||||||
|
64
apps/member/static/member/js/trust.js
Normal file
64
apps/member/static/member/js/trust.js
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
/**
|
||||||
|
* On form submit, create a new friendship
|
||||||
|
*/
|
||||||
|
function form_create_trust (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('trusted') + '/',
|
||||||
|
function (trusted_alias) {
|
||||||
|
if ((trusted_alias.note == formData.get('trusting')))
|
||||||
|
{
|
||||||
|
addMsg(gettext("You can't add yourself as a friend"), "danger")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
create_trust(formData.get('trusting'), trusted_alias.note)
|
||||||
|
}).fail(function (xhr, _textStatus, _error) {
|
||||||
|
errMsg(xhr.responseJSON)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a trust between users
|
||||||
|
* @param trusting:Integer trusting note id
|
||||||
|
* @param trusted:Integer trusted note id
|
||||||
|
*/
|
||||||
|
function create_trust(trusting, trusted) {
|
||||||
|
$.post('/api/note/trust/', {
|
||||||
|
trusting: trusting,
|
||||||
|
trusted: trusted,
|
||||||
|
csrfmiddlewaretoken: CSRF_TOKEN
|
||||||
|
}).done(function () {
|
||||||
|
// Reload tables
|
||||||
|
$('#trust_table').load(location.pathname + ' #trust_table')
|
||||||
|
$('#trusted_table').load(location.pathname + ' #trusted_table')
|
||||||
|
addMsg(gettext('Friendship successfully added'), 'success')
|
||||||
|
}).fail(function (xhr, _textStatus, _error) {
|
||||||
|
errMsg(xhr.responseJSON)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On click of "delete", delete the trust
|
||||||
|
* @param button_id:Integer Trust id to remove
|
||||||
|
*/
|
||||||
|
function delete_button (button_id) {
|
||||||
|
$.ajax({
|
||||||
|
url: '/api/note/trust/' + button_id + '/',
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: { 'X-CSRFTOKEN': CSRF_TOKEN }
|
||||||
|
}).done(function () {
|
||||||
|
addMsg(gettext('Friendship successfully deleted'), 'success')
|
||||||
|
$('#trust_table').load(location.pathname + ' #trust_table')
|
||||||
|
$('#trusted_table').load(location.pathname + ' #trusted_table')
|
||||||
|
}).fail(function (xhr, _textStatus, _error) {
|
||||||
|
errMsg(xhr.responseJSON)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).ready(function () {
|
||||||
|
// Attach event
|
||||||
|
document.getElementById('form_trust').addEventListener('submit', form_create_trust)
|
||||||
|
})
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from datetime import date
|
from datetime import date
|
||||||
@ -120,7 +120,7 @@ class MembershipTable(tables.Table):
|
|||||||
club=record.club,
|
club=record.club,
|
||||||
user=record.user,
|
user=record.user,
|
||||||
date_start__gte=record.club.membership_start,
|
date_start__gte=record.club.membership_start,
|
||||||
date_end__lte=record.club.membership_end,
|
date_end__lte=record.club.membership_end or date(9999, 12, 31),
|
||||||
).exists(): # If the renew is not yet performed
|
).exists(): # If the renew is not yet performed
|
||||||
empty_membership = Membership(
|
empty_membership = Membership(
|
||||||
club=record.club,
|
club=record.club,
|
||||||
|
@ -25,6 +25,14 @@
|
|||||||
</a>
|
</a>
|
||||||
</dd>
|
</dd>
|
||||||
|
|
||||||
|
<dt class="col-xl-6">{% trans 'friendships'|capfirst %}</dt>
|
||||||
|
<dd class="col-xl-6">
|
||||||
|
<a class="badge badge-secondary" href="{% url 'member:user_trust' user_object.pk %}">
|
||||||
|
<i class="fa fa-edit"></i>
|
||||||
|
{% trans 'Manage friendships' %} ({{ user_object.note.trusting.all|length }})
|
||||||
|
</a>
|
||||||
|
</dd>
|
||||||
|
|
||||||
{% if "member.view_profile"|has_perm:user_object.profile %}
|
{% if "member.view_profile"|has_perm:user_object.profile %}
|
||||||
<dt class="col-xl-6">{% trans 'section'|capfirst %}</dt>
|
<dt class="col-xl-6">{% trans 'section'|capfirst %}</dt>
|
||||||
<dd class="col-xl-6">{{ user_object.profile.section }}</dd>
|
<dd class="col-xl-6">{{ user_object.profile.section }}</dd>
|
||||||
|
@ -14,6 +14,9 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
<form method="post" enctype="multipart/form-data" id="formUpload">
|
<form method="post" enctype="multipart/form-data" id="formUpload">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ form |crispy }}
|
{{ form |crispy }}
|
||||||
|
{% if user.note.display_image != "pic/default.png" %}
|
||||||
|
<input type="submit" class="btn btn-primary" value="{% trans "Remove" %}">
|
||||||
|
{% endif %}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<!-- MODAL TO CROP THE IMAGE -->
|
<!-- MODAL TO CROP THE IMAGE -->
|
||||||
|
48
apps/member/templates/member/profile_trust.html
Normal file
48
apps/member/templates/member/profile_trust.html
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
{% extends "member/base.html" %}
|
||||||
|
{% comment %}
|
||||||
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
{% endcomment %}
|
||||||
|
{% load static django_tables2 i18n %}
|
||||||
|
|
||||||
|
{% block profile_content %}
|
||||||
|
<div class="card bg-light mb-3">
|
||||||
|
<h3 class="card-header text-center">
|
||||||
|
{% trans "Add friends" %}
|
||||||
|
</h3>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if can_create %}
|
||||||
|
<form class="input-group" method="POST" id="form_trust">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="trusting" value="{{ object.note.pk }}">
|
||||||
|
{%include "autocomplete_model.html" %}
|
||||||
|
<div class="input-group-append">
|
||||||
|
<input type="submit" class="btn btn-success" value="{% trans "Add" %}">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% render_table trusting %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-warning card mb-3">
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
Adding someone as a friend enables them to initiate transactions coming
|
||||||
|
from your account (while keeping your balance positive). This is
|
||||||
|
designed to simplify using note kfet transfers to transfer money between
|
||||||
|
users. The intent is that one person can make all transfers for a group of
|
||||||
|
friends without needing additional rights among them.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card bg-light mb-3">
|
||||||
|
<h3 class="card-header text-center">
|
||||||
|
{% trans "People having you as a friend" %}
|
||||||
|
</h3>
|
||||||
|
{% render_table trusted_by %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extrajavascript %}
|
||||||
|
<script src="{% static "member/js/trust.js" %}"></script>
|
||||||
|
<script src="{% static "js/autocomplete_model.js" %}"></script>
|
||||||
|
{% endblock%}
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from datetime import date
|
from datetime import date
|
||||||
|
@ -183,7 +183,7 @@ class TestMemberships(TestCase):
|
|||||||
club = Club.objects.get(name="Kfet")
|
club = Club.objects.get(name="Kfet")
|
||||||
else:
|
else:
|
||||||
club = Club.objects.create(
|
club = Club.objects.create(
|
||||||
name="Second club " + ("with BDE" if bde_parent else "without BDE"),
|
name="Second club without BDE",
|
||||||
parent_club=None,
|
parent_club=None,
|
||||||
email="newclub@example.com",
|
email="newclub@example.com",
|
||||||
require_memberships=True,
|
require_memberships=True,
|
||||||
@ -291,7 +291,7 @@ class TestMemberships(TestCase):
|
|||||||
|
|
||||||
response = self.client.post(reverse("member:club_manage_roles", args=(self.membership.pk,)), data=dict(
|
response = self.client.post(reverse("member:club_manage_roles", args=(self.membership.pk,)), data=dict(
|
||||||
roles=[role.id for role in Role.objects.filter(
|
roles=[role.id for role in Role.objects.filter(
|
||||||
Q(name="Membre de club") | Q(name="Tré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.assertRedirects(response, self.user.profile.get_absolute_url(), 302, 200)
|
||||||
self.membership.refresh_from_db()
|
self.membership.refresh_from_db()
|
||||||
@ -335,6 +335,7 @@ class TestMemberships(TestCase):
|
|||||||
ml_sports_registration=True,
|
ml_sports_registration=True,
|
||||||
ml_art_registration=True,
|
ml_art_registration=True,
|
||||||
report_frequency=7,
|
report_frequency=7,
|
||||||
|
VSS_charter_read=True
|
||||||
))
|
))
|
||||||
self.assertRedirects(response, self.user.profile.get_absolute_url(), 302, 200)
|
self.assertRedirects(response, self.user.profile.get_absolute_url(), 302, 200)
|
||||||
self.assertTrue(User.objects.filter(username="toto changed").exists())
|
self.assertTrue(User.objects.filter(username="toto changed").exists())
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
@ -23,5 +23,6 @@ urlpatterns = [
|
|||||||
path('user/<int:pk>/update/', views.UserUpdateView.as_view(), name="user_update_profile"),
|
path('user/<int:pk>/update/', views.UserUpdateView.as_view(), name="user_update_profile"),
|
||||||
path('user/<int:pk>/update_pic/', views.ProfilePictureUpdateView.as_view(), name="user_update_pic"),
|
path('user/<int:pk>/update_pic/', views.ProfilePictureUpdateView.as_view(), name="user_update_pic"),
|
||||||
path('user/<int:pk>/aliases/', views.ProfileAliasView.as_view(), name="user_alias"),
|
path('user/<int:pk>/aliases/', views.ProfileAliasView.as_view(), name="user_alias"),
|
||||||
|
path('user/<int:pk>/trust', views.ProfileTrustView.as_view(), name="user_trust"),
|
||||||
path('manage-auth-token/', views.ManageAuthTokens.as_view(), name='auth_token'),
|
path('manage-auth-token/', views.ManageAuthTokens.as_view(), name='auth_token'),
|
||||||
]
|
]
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from datetime import timedelta, date
|
from datetime import timedelta, date
|
||||||
@ -16,17 +16,18 @@ from django.utils import timezone
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.generic import DetailView, UpdateView, TemplateView
|
from django.views.generic import DetailView, UpdateView, TemplateView
|
||||||
from django.views.generic.edit import FormMixin
|
from django.views.generic.edit import FormMixin
|
||||||
from django_tables2.views import SingleTableView
|
from django_tables2.views import MultiTableMixin, SingleTableMixin, SingleTableView
|
||||||
from rest_framework.authtoken.models import Token
|
from rest_framework.authtoken.models import Token
|
||||||
from note.models import Alias, NoteUser
|
from api.viewsets import is_regex
|
||||||
|
from note.models import Alias, NoteClub, NoteUser, Trust
|
||||||
from note.models.transactions import Transaction, SpecialTransaction
|
from note.models.transactions import Transaction, SpecialTransaction
|
||||||
from note.tables import HistoryTable, AliasTable
|
from note.tables import HistoryTable, AliasTable, TrustTable, TrustedTable
|
||||||
from note_kfet.middlewares import _set_current_request
|
from note_kfet.middlewares import _set_current_request
|
||||||
from permission.backends import PermissionBackend
|
from permission.backends import PermissionBackend
|
||||||
from permission.models import Role
|
from permission.models import Role
|
||||||
from permission.views import ProtectQuerysetMixin, ProtectedCreateView
|
from permission.views import ProtectQuerysetMixin, ProtectedCreateView
|
||||||
|
|
||||||
from .forms import UserForm, ProfileForm, ImageForm, ClubForm, MembershipForm,\
|
from .forms import UserForm, ProfileForm, ImageForm, ClubForm, MembershipForm, \
|
||||||
CustomAuthenticationForm, MembershipRolesForm
|
CustomAuthenticationForm, MembershipRolesForm
|
||||||
from .models import Club, Membership
|
from .models import Club, Membership
|
||||||
from .tables import ClubTable, UserTable, MembershipTable, ClubManagerTable
|
from .tables import ClubTable, UserTable, MembershipTable, ClubManagerTable
|
||||||
@ -165,7 +166,8 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
|||||||
# Display only the most recent membership
|
# Display only the most recent membership
|
||||||
club_list = club_list.distinct("club__name")\
|
club_list = club_list.distinct("club__name")\
|
||||||
if settings.DATABASES["default"]["ENGINE"] == 'django.db.backends.postgresql' else club_list
|
if settings.DATABASES["default"]["ENGINE"] == 'django.db.backends.postgresql' else club_list
|
||||||
membership_table = MembershipTable(data=club_list, prefix='membership-')
|
club_list_order_by = self.request.GET.getlist("membership-sort", ("club__name", "-date_start"))
|
||||||
|
membership_table = MembershipTable(data=club_list, prefix='membership-', order_by=club_list_order_by)
|
||||||
membership_table.paginate(per_page=10, page=self.request.GET.get("membership-page", 1))
|
membership_table.paginate(per_page=10, page=self.request.GET.get("membership-page", 1))
|
||||||
context['club_list'] = membership_table
|
context['club_list'] = membership_table
|
||||||
|
|
||||||
@ -174,7 +176,7 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
|||||||
modified_note = NoteUser.objects.get(pk=user.note.pk)
|
modified_note = NoteUser.objects.get(pk=user.note.pk)
|
||||||
# Don't log these tests
|
# Don't log these tests
|
||||||
modified_note._no_signal = True
|
modified_note._no_signal = True
|
||||||
modified_note.is_active = True
|
modified_note.is_active = False
|
||||||
modified_note.inactivity_reason = 'manual'
|
modified_note.inactivity_reason = 'manual'
|
||||||
context["can_lock_note"] = user.note.is_active and PermissionBackend\
|
context["can_lock_note"] = user.note.is_active and PermissionBackend\
|
||||||
.check_perm(self.request, "note.change_noteuser_is_active", modified_note)
|
.check_perm(self.request, "note.change_noteuser_is_active", modified_note)
|
||||||
@ -183,14 +185,14 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
|||||||
modified_note._force_save = True
|
modified_note._force_save = True
|
||||||
modified_note.save()
|
modified_note.save()
|
||||||
context["can_force_lock"] = user.note.is_active and PermissionBackend\
|
context["can_force_lock"] = user.note.is_active and PermissionBackend\
|
||||||
.check_perm(self.request, "note.change_note_is_active", modified_note)
|
.check_perm(self.request, "note.change_noteuser_is_active", modified_note)
|
||||||
old_note._force_save = True
|
old_note._force_save = True
|
||||||
old_note._no_signal = True
|
old_note._no_signal = True
|
||||||
old_note.save()
|
old_note.save()
|
||||||
modified_note.refresh_from_db()
|
modified_note.refresh_from_db()
|
||||||
modified_note.is_active = True
|
modified_note.is_active = True
|
||||||
context["can_unlock_note"] = not user.note.is_active and PermissionBackend\
|
context["can_unlock_note"] = not user.note.is_active and PermissionBackend\
|
||||||
.check_perm(self.request, "note.change_note_is_active", modified_note)
|
.check_perm(self.request, "note.change_noteuser_is_active", modified_note)
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
@ -219,16 +221,20 @@ class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
|||||||
if "search" in self.request.GET and self.request.GET["search"]:
|
if "search" in self.request.GET and self.request.GET["search"]:
|
||||||
pattern = self.request.GET["search"]
|
pattern = self.request.GET["search"]
|
||||||
|
|
||||||
|
# Check if this is a valid regex. If not, we won't check regex
|
||||||
|
valid_regex = is_regex(pattern)
|
||||||
|
suffix = "__iregex" if valid_regex else "__istartswith"
|
||||||
|
prefix = "^" if valid_regex else ""
|
||||||
qs = qs.filter(
|
qs = qs.filter(
|
||||||
username__iregex="^" + pattern
|
Q(**{f"username{suffix}": prefix + pattern})
|
||||||
).union(
|
).union(
|
||||||
qs.filter(
|
qs.filter(
|
||||||
(Q(alias__iregex="^" + pattern)
|
(Q(**{f"alias{suffix}": prefix + pattern})
|
||||||
| Q(normalized_alias__iregex="^" + Alias.normalize(pattern))
|
| Q(**{f"normalized_alias{suffix}": prefix + Alias.normalize(pattern)})
|
||||||
| Q(last_name__iregex="^" + pattern)
|
| Q(**{f"last_name{suffix}": prefix + pattern})
|
||||||
| Q(first_name__iregex="^" + pattern)
|
| Q(**{f"first_name{suffix}": prefix + pattern})
|
||||||
| Q(email__istartswith=pattern))
|
| Q(email__istartswith=pattern))
|
||||||
& ~Q(username__iregex="^" + pattern)
|
& ~Q(**{f"username{suffix}": prefix + pattern})
|
||||||
), all=True)
|
), all=True)
|
||||||
else:
|
else:
|
||||||
qs = qs.none()
|
qs = qs.none()
|
||||||
@ -243,7 +249,53 @@ class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
class ProfileTrustView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, DetailView):
|
||||||
|
"""
|
||||||
|
View and manage user trust relationships
|
||||||
|
"""
|
||||||
|
model = User
|
||||||
|
template_name = 'member/profile_trust.html'
|
||||||
|
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)
|
||||||
|
|
||||||
|
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
|
||||||
|
))
|
||||||
|
context["widget"] = {
|
||||||
|
"name": "trusted",
|
||||||
|
"resetable": True,
|
||||||
|
"attrs": {
|
||||||
|
"class": "autocomplete form-control",
|
||||||
|
"id": "trusted",
|
||||||
|
"api_url": "/api/note/alias/?note__polymorphic_ctype__model=noteuser",
|
||||||
|
"name_field": "name",
|
||||||
|
"placeholder": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableMixin, DetailView):
|
||||||
"""
|
"""
|
||||||
View and manage user aliases.
|
View and manage user aliases.
|
||||||
"""
|
"""
|
||||||
@ -252,11 +304,15 @@ class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
|||||||
context_object_name = 'user_object'
|
context_object_name = 'user_object'
|
||||||
extra_context = {"title": _("Note aliases")}
|
extra_context = {"title": _("Note aliases")}
|
||||||
|
|
||||||
|
table_class = AliasTable
|
||||||
|
context_table_name = "aliases"
|
||||||
|
|
||||||
|
def get_table_data(self):
|
||||||
|
return self.object.note.alias.filter(PermissionBackend.filter_queryset(self.request, Alias, "view")).distinct() \
|
||||||
|
.order_by('normalized_name')
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
note = context['object'].note
|
|
||||||
context["aliases"] = AliasTable(
|
|
||||||
note.alias.filter(PermissionBackend.filter_queryset(self.request, Alias, "view")).distinct().all())
|
|
||||||
context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_alias", Alias(
|
context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_alias", Alias(
|
||||||
note=context["object"].note,
|
note=context["object"].note,
|
||||||
name="",
|
name="",
|
||||||
@ -291,12 +347,15 @@ class PictureUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin, Det
|
|||||||
"""Save image to note"""
|
"""Save image to note"""
|
||||||
image = form.cleaned_data['image']
|
image = form.cleaned_data['image']
|
||||||
|
|
||||||
# Rename as a PNG or GIF
|
if image is None:
|
||||||
extension = image.name.split(".")[-1]
|
image = "pic/default.png"
|
||||||
if extension == "gif":
|
|
||||||
image.name = "{}_pic.gif".format(self.object.note.pk)
|
|
||||||
else:
|
else:
|
||||||
image.name = "{}_pic.png".format(self.object.note.pk)
|
# Rename as a PNG or GIF
|
||||||
|
extension = image.name.split(".")[-1]
|
||||||
|
if extension == "gif":
|
||||||
|
image.name = "{}_pic.gif".format(self.object.note.pk)
|
||||||
|
else:
|
||||||
|
image.name = "{}_pic.png".format(self.object.note.pk)
|
||||||
|
|
||||||
# Save
|
# Save
|
||||||
self.object.note.display_image = image
|
self.object.note.display_image = image
|
||||||
@ -372,10 +431,15 @@ class ClubListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
|||||||
if "search" in self.request.GET:
|
if "search" in self.request.GET:
|
||||||
pattern = self.request.GET["search"]
|
pattern = self.request.GET["search"]
|
||||||
|
|
||||||
|
# Check if this is a valid regex. If not, we won't check regex
|
||||||
|
valid_regex = is_regex(pattern)
|
||||||
|
suffix = "__iregex" if valid_regex else "__istartswith"
|
||||||
|
prefix = "^" if valid_regex else ""
|
||||||
|
|
||||||
qs = qs.filter(
|
qs = qs.filter(
|
||||||
Q(name__iregex=pattern)
|
Q(**{f"name{suffix}": prefix + pattern})
|
||||||
| Q(note__alias__name__iregex=pattern)
|
| Q(**{f"note__alias__name{suffix}": prefix + pattern})
|
||||||
| Q(note__alias__normalized_name__iregex=Alias.normalize(pattern))
|
| Q(**{f"note__alias__normalized_name{suffix}": prefix + Alias.normalize(pattern)})
|
||||||
)
|
)
|
||||||
|
|
||||||
return qs
|
return qs
|
||||||
@ -403,14 +467,18 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
|||||||
"""
|
"""
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
club = context["club"]
|
club = self.object
|
||||||
|
context["note"] = club.note
|
||||||
|
|
||||||
if PermissionBackend.check_perm(self.request, "member.change_club_membership_start", club):
|
if PermissionBackend.check_perm(self.request, "member.change_club_membership_start", club):
|
||||||
club.update_membership_dates()
|
club.update_membership_dates()
|
||||||
|
|
||||||
# managers list
|
# managers list
|
||||||
managers = Membership.objects.filter(club=self.object, roles__name="Bureau de club",
|
managers = Membership.objects.filter(club=self.object, roles__name="Bureau de club",
|
||||||
date_start__lte=date.today(), date_end__gte=date.today())\
|
date_start__lte=date.today(), date_end__gte=date.today())\
|
||||||
.order_by('user__last_name').all()
|
.order_by('user__last_name').all()
|
||||||
context["managers"] = ClubManagerTable(data=managers, prefix="managers-")
|
managers_order_by = self.request.GET.getlist("managers-sort", ('user__last_name'))
|
||||||
|
context["managers"] = ClubManagerTable(data=managers, prefix="managers-", order_by=managers_order_by)
|
||||||
# transaction history
|
# transaction history
|
||||||
club_transactions = Transaction.objects.all().filter(Q(source=club.note) | Q(destination=club.note))\
|
club_transactions = Transaction.objects.all().filter(Q(source=club.note) | Q(destination=club.note))\
|
||||||
.filter(PermissionBackend.filter_queryset(self.request, Transaction, "view"))\
|
.filter(PermissionBackend.filter_queryset(self.request, Transaction, "view"))\
|
||||||
@ -428,7 +496,8 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
|||||||
club_member = club_member.distinct("user__username")\
|
club_member = club_member.distinct("user__username")\
|
||||||
if settings.DATABASES["default"]["ENGINE"] == 'django.db.backends.postgresql' else club_member
|
if settings.DATABASES["default"]["ENGINE"] == 'django.db.backends.postgresql' else club_member
|
||||||
|
|
||||||
membership_table = MembershipTable(data=club_member, prefix="membership-")
|
membership_order_by = self.request.GET.getlist("membership-sort", ("user__username", "-date_start"))
|
||||||
|
membership_table = MembershipTable(data=club_member, prefix="membership-", order_by=membership_order_by)
|
||||||
membership_table.paginate(per_page=5, page=self.request.GET.get('membership-page', 1))
|
membership_table.paginate(per_page=5, page=self.request.GET.get('membership-page', 1))
|
||||||
context['member_list'] = membership_table
|
context['member_list'] = membership_table
|
||||||
|
|
||||||
@ -443,10 +512,33 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
|||||||
context["can_add_members"] = PermissionBackend()\
|
context["can_add_members"] = PermissionBackend()\
|
||||||
.has_perm(self.request.user, "member.add_membership", empty_membership)
|
.has_perm(self.request.user, "member.add_membership", empty_membership)
|
||||||
|
|
||||||
|
# Check permissions to see if the authenticated user can lock/unlock the note
|
||||||
|
with transaction.atomic():
|
||||||
|
modified_note = NoteClub.objects.get(pk=club.note.pk)
|
||||||
|
# Don't log these tests
|
||||||
|
modified_note._no_signal = True
|
||||||
|
modified_note.is_active = False
|
||||||
|
modified_note.inactivity_reason = 'manual'
|
||||||
|
context["can_lock_note"] = club.note.is_active and PermissionBackend \
|
||||||
|
.check_perm(self.request, "note.change_noteclub_is_active", modified_note)
|
||||||
|
old_note = NoteClub.objects.select_for_update().get(pk=club.note.pk)
|
||||||
|
modified_note.inactivity_reason = 'forced'
|
||||||
|
modified_note._force_save = True
|
||||||
|
modified_note.save()
|
||||||
|
context["can_force_lock"] = club.note.is_active and PermissionBackend \
|
||||||
|
.check_perm(self.request, "note.change_noteclub_is_active", modified_note)
|
||||||
|
old_note._force_save = True
|
||||||
|
old_note._no_signal = True
|
||||||
|
old_note.save()
|
||||||
|
modified_note.refresh_from_db()
|
||||||
|
modified_note.is_active = True
|
||||||
|
context["can_unlock_note"] = not club.note.is_active and PermissionBackend \
|
||||||
|
.check_perm(self.request, "note.change_noteclub_is_active", modified_note)
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class ClubAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
class ClubAliasView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableMixin, DetailView):
|
||||||
"""
|
"""
|
||||||
Manage aliases of a club.
|
Manage aliases of a club.
|
||||||
"""
|
"""
|
||||||
@ -455,11 +547,16 @@ class ClubAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
|||||||
context_object_name = 'club'
|
context_object_name = 'club'
|
||||||
extra_context = {"title": _("Note aliases")}
|
extra_context = {"title": _("Note aliases")}
|
||||||
|
|
||||||
|
table_class = AliasTable
|
||||||
|
context_table_name = "aliases"
|
||||||
|
|
||||||
|
def get_table_data(self):
|
||||||
|
return self.object.note.alias.filter(
|
||||||
|
PermissionBackend.filter_queryset(self.request, Alias, "view")).distinct()
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
note = context['object'].note
|
|
||||||
context["aliases"] = AliasTable(note.alias.filter(
|
|
||||||
PermissionBackend.filter_queryset(self.request, Alias, "view")).distinct().all())
|
|
||||||
context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_alias", Alias(
|
context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_alias", Alias(
|
||||||
note=context["object"].note,
|
note=context["object"].note,
|
||||||
name="",
|
name="",
|
||||||
@ -692,6 +789,10 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
|
|||||||
club = old_membership.club
|
club = old_membership.club
|
||||||
user = old_membership.user
|
user = old_membership.user
|
||||||
|
|
||||||
|
# Update club membership date
|
||||||
|
if PermissionBackend.check_perm(self.request, "member.change_club_membership_start", club):
|
||||||
|
club.update_membership_dates()
|
||||||
|
|
||||||
form.instance.club = club
|
form.instance.club = club
|
||||||
|
|
||||||
# Get form data
|
# Get form data
|
||||||
@ -759,8 +860,8 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
|
|||||||
|
|
||||||
ret = super().form_valid(form)
|
ret = super().form_valid(form)
|
||||||
|
|
||||||
member_role = Role.objects.filter(Q(name="Adhérent BDE") | 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 Kfet") | 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()
|
if club.name == "Kfet"else Role.objects.filter(name="Membre de club").all()
|
||||||
# Set the same roles as before
|
# Set the same roles as before
|
||||||
if old_membership:
|
if old_membership:
|
||||||
@ -796,7 +897,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
|
|||||||
membership.refresh_from_db()
|
membership.refresh_from_db()
|
||||||
if old_membership.exists():
|
if old_membership.exists():
|
||||||
membership.roles.set(old_membership.get().roles.all())
|
membership.roles.set(old_membership.get().roles.all())
|
||||||
membership.roles.set(Role.objects.filter(Q(name="Adhérent 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()
|
membership.save()
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
@ -844,10 +945,15 @@ class ClubMembersListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableV
|
|||||||
|
|
||||||
if 'search' in self.request.GET:
|
if 'search' in self.request.GET:
|
||||||
pattern = self.request.GET['search']
|
pattern = self.request.GET['search']
|
||||||
|
|
||||||
|
# Check if this is a valid regex. If not, we won't check regex
|
||||||
|
valid_regex = is_regex(pattern)
|
||||||
|
suffix = "__iregex" if valid_regex else "__istartswith"
|
||||||
|
prefix = "^" if valid_regex else ""
|
||||||
qs = qs.filter(
|
qs = qs.filter(
|
||||||
Q(user__first_name__iregex='^' + pattern)
|
Q(**{f"user__first_name{suffix}": prefix + pattern})
|
||||||
| Q(user__last_name__iregex='^' + pattern)
|
| Q(**{f"user__last_name{suffix}": prefix + pattern})
|
||||||
| Q(user__note__alias__normalized_name__iregex='^' + Alias.normalize(pattern))
|
| Q(**{f"user__note__alias__normalized_name{suffix}": prefix + Alias.normalize(pattern)})
|
||||||
)
|
)
|
||||||
|
|
||||||
only_active = "only_active" not in self.request.GET or self.request.GET["only_active"] != '0'
|
only_active = "only_active" not in self.request.GET or self.request.GET["only_active"] != '0'
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
default_app_config = 'note.apps.NoteConfig'
|
default_app_config = 'note.apps.NoteConfig'
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
@ -7,7 +7,7 @@ from polymorphic.admin import PolymorphicChildModelAdmin, \
|
|||||||
PolymorphicChildModelFilter, PolymorphicParentModelAdmin
|
PolymorphicChildModelFilter, PolymorphicParentModelAdmin
|
||||||
from note_kfet.admin import admin_site
|
from note_kfet.admin import admin_site
|
||||||
|
|
||||||
from .models.notes import Alias, Note, NoteClub, NoteSpecial, NoteUser
|
from .models.notes import Alias, Note, NoteClub, NoteSpecial, NoteUser, Trust
|
||||||
from .models.transactions import Transaction, TemplateCategory, TransactionTemplate, \
|
from .models.transactions import Transaction, TemplateCategory, TransactionTemplate, \
|
||||||
RecurrentTransaction, MembershipTransaction, SpecialTransaction
|
RecurrentTransaction, MembershipTransaction, SpecialTransaction
|
||||||
from .templatetags.pretty_money import pretty_money
|
from .templatetags.pretty_money import pretty_money
|
||||||
@ -21,6 +21,16 @@ class AliasInlines(admin.TabularInline):
|
|||||||
model = Alias
|
model = Alias
|
||||||
|
|
||||||
|
|
||||||
|
class TrustInlines(admin.TabularInline):
|
||||||
|
"""
|
||||||
|
Define trusts when editing the trusting note
|
||||||
|
"""
|
||||||
|
model = Trust
|
||||||
|
fk_name = "trusting"
|
||||||
|
extra = 0
|
||||||
|
readonly_fields = ("trusted",)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Note, site=admin_site)
|
@admin.register(Note, site=admin_site)
|
||||||
class NoteAdmin(PolymorphicParentModelAdmin):
|
class NoteAdmin(PolymorphicParentModelAdmin):
|
||||||
"""
|
"""
|
||||||
@ -92,7 +102,7 @@ class NoteUserAdmin(PolymorphicChildModelAdmin):
|
|||||||
"""
|
"""
|
||||||
Child for an user note, see NoteAdmin
|
Child for an user note, see NoteAdmin
|
||||||
"""
|
"""
|
||||||
inlines = (AliasInlines,)
|
inlines = (AliasInlines, TrustInlines)
|
||||||
|
|
||||||
# We can't change user after creation or the balance
|
# We can't change user after creation or the balance
|
||||||
readonly_fields = ('user', 'balance')
|
readonly_fields = ('user', 'balance')
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -11,8 +11,9 @@ from member.models import Membership
|
|||||||
from note_kfet.middlewares import get_current_request
|
from note_kfet.middlewares import get_current_request
|
||||||
from permission.backends import PermissionBackend
|
from permission.backends import PermissionBackend
|
||||||
from rest_framework.utils import model_meta
|
from rest_framework.utils import model_meta
|
||||||
|
from rest_framework.validators import UniqueTogetherValidator
|
||||||
|
|
||||||
from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias
|
from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias, Trust
|
||||||
from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction, TemplateCategory, \
|
from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction, TemplateCategory, \
|
||||||
RecurrentTransaction, SpecialTransaction
|
RecurrentTransaction, SpecialTransaction
|
||||||
|
|
||||||
@ -77,6 +78,20 @@ class NoteUserSerializer(serializers.ModelSerializer):
|
|||||||
return str(obj)
|
return str(obj)
|
||||||
|
|
||||||
|
|
||||||
|
class TrustSerializer(serializers.ModelSerializer):
|
||||||
|
"""
|
||||||
|
REST API Serializer for Trusts.
|
||||||
|
The djangorestframework plugin will analyse the model `Trust` and parse all fields in the API.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Trust
|
||||||
|
fields = '__all__'
|
||||||
|
validators = [UniqueTogetherValidator(
|
||||||
|
queryset=Trust.objects.all(), fields=('trusting', 'trusted'),
|
||||||
|
message=_("This friendship already exists"))]
|
||||||
|
|
||||||
|
|
||||||
class AliasSerializer(serializers.ModelSerializer):
|
class AliasSerializer(serializers.ModelSerializer):
|
||||||
"""
|
"""
|
||||||
REST API Serializer for Aliases.
|
REST API Serializer for Aliases.
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from .views import NotePolymorphicViewSet, AliasViewSet, ConsumerViewSet, \
|
from .views import NotePolymorphicViewSet, AliasViewSet, ConsumerViewSet, \
|
||||||
TemplateCategoryViewSet, TransactionViewSet, TransactionTemplateViewSet
|
TemplateCategoryViewSet, TransactionViewSet, TransactionTemplateViewSet, \
|
||||||
|
TrustViewSet
|
||||||
|
|
||||||
|
|
||||||
def register_note_urls(router, path):
|
def register_note_urls(router, path):
|
||||||
@ -11,6 +12,7 @@ def register_note_urls(router, path):
|
|||||||
"""
|
"""
|
||||||
router.register(path + '/note', NotePolymorphicViewSet)
|
router.register(path + '/note', NotePolymorphicViewSet)
|
||||||
router.register(path + '/alias', AliasViewSet)
|
router.register(path + '/alias', AliasViewSet)
|
||||||
|
router.register(path + '/trust', TrustViewSet)
|
||||||
router.register(path + '/consumer', ConsumerViewSet)
|
router.register(path + '/consumer', ConsumerViewSet)
|
||||||
|
|
||||||
router.register(path + '/transaction/category', TemplateCategoryViewSet)
|
router.register(path + '/transaction/category', TemplateCategoryViewSet)
|
||||||
|
@ -1,21 +1,22 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
import re
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
from rest_framework.filters import OrderingFilter, SearchFilter
|
from rest_framework.filters import OrderingFilter
|
||||||
from rest_framework import viewsets
|
from rest_framework import status, viewsets
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework import status
|
from api.filters import RegexSafeSearchFilter
|
||||||
from api.viewsets import ReadProtectedModelViewSet, ReadOnlyProtectedModelViewSet
|
from api.viewsets import ReadProtectedModelViewSet, ReadOnlyProtectedModelViewSet, \
|
||||||
|
is_regex
|
||||||
from permission.backends import PermissionBackend
|
from permission.backends import PermissionBackend
|
||||||
|
|
||||||
from .serializers import NotePolymorphicSerializer, AliasSerializer, ConsumerSerializer,\
|
from .serializers import NotePolymorphicSerializer, AliasSerializer, ConsumerSerializer, \
|
||||||
TemplateCategorySerializer, TransactionTemplateSerializer, TransactionPolymorphicSerializer
|
TemplateCategorySerializer, TransactionTemplateSerializer, TransactionPolymorphicSerializer, \
|
||||||
from ..models.notes import Note, Alias, NoteUser, NoteClub, NoteSpecial
|
TrustSerializer
|
||||||
|
from ..models.notes import Note, Alias, NoteUser, NoteClub, NoteSpecial, Trust
|
||||||
from ..models.transactions import TransactionTemplate, Transaction, TemplateCategory
|
from ..models.transactions import TransactionTemplate, Transaction, TemplateCategory
|
||||||
|
|
||||||
|
|
||||||
@ -28,7 +29,7 @@ class NotePolymorphicViewSet(ReadProtectedModelViewSet):
|
|||||||
"""
|
"""
|
||||||
queryset = Note.objects.order_by('id')
|
queryset = Note.objects.order_by('id')
|
||||||
serializer_class = NotePolymorphicSerializer
|
serializer_class = NotePolymorphicSerializer
|
||||||
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter, OrderingFilter]
|
||||||
filterset_fields = ['alias__name', 'polymorphic_ctype', 'is_active', 'balance', 'last_negative', 'created_at', ]
|
filterset_fields = ['alias__name', 'polymorphic_ctype', 'is_active', 'balance', 'last_negative', 'created_at', ]
|
||||||
search_fields = ['$alias__normalized_name', '$alias__name', '$polymorphic_ctype__model',
|
search_fields = ['$alias__normalized_name', '$alias__name', '$polymorphic_ctype__model',
|
||||||
'$noteuser__user__last_name', '$noteuser__user__first_name', '$noteuser__user__email',
|
'$noteuser__user__last_name', '$noteuser__user__first_name', '$noteuser__user__email',
|
||||||
@ -47,24 +48,58 @@ class NotePolymorphicViewSet(ReadProtectedModelViewSet):
|
|||||||
.distinct()
|
.distinct()
|
||||||
|
|
||||||
alias = self.request.query_params.get("alias", ".*")
|
alias = self.request.query_params.get("alias", ".*")
|
||||||
|
# Check if this is a valid regex. If not, we won't check regex
|
||||||
|
valid_regex = is_regex(alias)
|
||||||
|
suffix = '__iregex' if valid_regex else '__istartswith'
|
||||||
|
alias_prefix = '^' if valid_regex else ''
|
||||||
queryset = queryset.filter(
|
queryset = queryset.filter(
|
||||||
Q(alias__name__iregex="^" + alias)
|
Q(**{f"alias__name{suffix}": alias_prefix + alias})
|
||||||
| Q(alias__normalized_name__iregex="^" + Alias.normalize(alias))
|
| Q(**{f"alias__normalized_name{suffix}": alias_prefix + Alias.normalize(alias)})
|
||||||
| Q(alias__normalized_name__iregex="^" + alias.lower())
|
| Q(**{f"alias__normalized_name{suffix}": alias_prefix + alias.lower()})
|
||||||
)
|
)
|
||||||
|
|
||||||
return queryset.order_by("id")
|
return queryset.order_by("id")
|
||||||
|
|
||||||
|
|
||||||
|
class TrustViewSet(ReadProtectedModelViewSet):
|
||||||
|
"""
|
||||||
|
REST Trust View set.
|
||||||
|
The djangorestframework plugin will get all `Trust` objects, serialize it to JSON with the given serializer,
|
||||||
|
then render it on /api/note/trust/
|
||||||
|
"""
|
||||||
|
queryset = Trust.objects
|
||||||
|
serializer_class = TrustSerializer
|
||||||
|
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']
|
||||||
|
ordering_fields = ['trusting', 'trusted', ]
|
||||||
|
|
||||||
|
def get_serializer_class(self):
|
||||||
|
serializer_class = self.serializer_class
|
||||||
|
if self.request.method in ['PUT', 'PATCH']:
|
||||||
|
# trust relationship can't change people involved
|
||||||
|
serializer_class.Meta.read_only_fields = ('trusting', 'trusting',)
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
class AliasViewSet(ReadProtectedModelViewSet):
|
class AliasViewSet(ReadProtectedModelViewSet):
|
||||||
"""
|
"""
|
||||||
REST API View set.
|
REST API View set.
|
||||||
The djangorestframework plugin will get all `Alias` objects, serialize it to JSON with the given serializer,
|
The djangorestframework plugin will get all `Alias` objects, serialize it to JSON with the given serializer,
|
||||||
then render it on /api/aliases/
|
then render it on /api/note/alias/
|
||||||
"""
|
"""
|
||||||
queryset = Alias.objects
|
queryset = Alias.objects
|
||||||
serializer_class = AliasSerializer
|
serializer_class = AliasSerializer
|
||||||
filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
|
filter_backends = [RegexSafeSearchFilter, DjangoFilterBackend, OrderingFilter]
|
||||||
search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ]
|
search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ]
|
||||||
filterset_fields = ['name', 'normalized_name', 'note', 'note__noteuser__user',
|
filterset_fields = ['name', 'normalized_name', 'note', 'note__noteuser__user',
|
||||||
'note__noteclub__club', 'note__polymorphic_ctype__model', ]
|
'note__noteclub__club', 'note__polymorphic_ctype__model', ]
|
||||||
@ -95,18 +130,22 @@ class AliasViewSet(ReadProtectedModelViewSet):
|
|||||||
|
|
||||||
alias = self.request.query_params.get("alias", None)
|
alias = self.request.query_params.get("alias", None)
|
||||||
if alias:
|
if alias:
|
||||||
|
# Check if this is a valid regex. If not, we won't check regex
|
||||||
|
valid_regex = is_regex(alias)
|
||||||
|
suffix = '__iregex' if valid_regex else '__istartswith'
|
||||||
|
alias_prefix = '^' if valid_regex else ''
|
||||||
queryset = queryset.filter(
|
queryset = queryset.filter(
|
||||||
name__iregex="^" + alias
|
**{f"name{suffix}": alias_prefix + alias}
|
||||||
).union(
|
).union(
|
||||||
queryset.filter(
|
queryset.filter(
|
||||||
Q(normalized_name__iregex="^" + Alias.normalize(alias))
|
Q(**{f"normalized_name{suffix}": alias_prefix + Alias.normalize(alias)})
|
||||||
& ~Q(name__iregex="^" + alias)
|
& ~Q(**{f"name{suffix}": alias_prefix + alias})
|
||||||
),
|
),
|
||||||
all=True).union(
|
all=True).union(
|
||||||
queryset.filter(
|
queryset.filter(
|
||||||
Q(normalized_name__iregex="^" + alias.lower())
|
Q(**{f"normalized_name{suffix}": "^" + alias.lower()})
|
||||||
& ~Q(normalized_name__iregex="^" + Alias.normalize(alias))
|
& ~Q(**{f"normalized_name{suffix}": "^" + Alias.normalize(alias)})
|
||||||
& ~Q(name__iregex="^" + alias)
|
& ~Q(**{f"name{suffix}": "^" + alias})
|
||||||
),
|
),
|
||||||
all=True)
|
all=True)
|
||||||
|
|
||||||
@ -116,7 +155,7 @@ class AliasViewSet(ReadProtectedModelViewSet):
|
|||||||
class ConsumerViewSet(ReadOnlyProtectedModelViewSet):
|
class ConsumerViewSet(ReadOnlyProtectedModelViewSet):
|
||||||
queryset = Alias.objects
|
queryset = Alias.objects
|
||||||
serializer_class = ConsumerSerializer
|
serializer_class = ConsumerSerializer
|
||||||
filter_backends = [SearchFilter, OrderingFilter, DjangoFilterBackend]
|
filter_backends = [RegexSafeSearchFilter, OrderingFilter, DjangoFilterBackend]
|
||||||
search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ]
|
search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ]
|
||||||
filterset_fields = ['name', 'normalized_name', 'note', 'note__noteuser__user',
|
filterset_fields = ['name', 'normalized_name', 'note', 'note__noteuser__user',
|
||||||
'note__noteclub__club', 'note__polymorphic_ctype__model', ]
|
'note__noteclub__club', 'note__polymorphic_ctype__model', ]
|
||||||
@ -135,11 +174,7 @@ class ConsumerViewSet(ReadOnlyProtectedModelViewSet):
|
|||||||
|
|
||||||
alias = self.request.query_params.get("alias", None)
|
alias = self.request.query_params.get("alias", None)
|
||||||
# Check if this is a valid regex. If not, we won't check regex
|
# Check if this is a valid regex. If not, we won't check regex
|
||||||
try:
|
valid_regex = is_regex(alias)
|
||||||
re.compile(alias)
|
|
||||||
valid_regex = True
|
|
||||||
except (re.error, TypeError):
|
|
||||||
valid_regex = False
|
|
||||||
suffix = '__iregex' if valid_regex else '__istartswith'
|
suffix = '__iregex' if valid_regex else '__istartswith'
|
||||||
alias_prefix = '^' if valid_regex else ''
|
alias_prefix = '^' if valid_regex else ''
|
||||||
queryset = queryset.prefetch_related('note')
|
queryset = queryset.prefetch_related('note')
|
||||||
@ -176,7 +211,7 @@ class TemplateCategoryViewSet(ReadProtectedModelViewSet):
|
|||||||
"""
|
"""
|
||||||
queryset = TemplateCategory.objects.order_by('name')
|
queryset = TemplateCategory.objects.order_by('name')
|
||||||
serializer_class = TemplateCategorySerializer
|
serializer_class = TemplateCategorySerializer
|
||||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||||
filterset_fields = ['name', 'templates', 'templates__name']
|
filterset_fields = ['name', 'templates', 'templates__name']
|
||||||
search_fields = ['$name', '$templates__name', ]
|
search_fields = ['$name', '$templates__name', ]
|
||||||
|
|
||||||
@ -189,7 +224,7 @@ class TransactionTemplateViewSet(viewsets.ModelViewSet):
|
|||||||
"""
|
"""
|
||||||
queryset = TransactionTemplate.objects.order_by('name')
|
queryset = TransactionTemplate.objects.order_by('name')
|
||||||
serializer_class = TransactionTemplateSerializer
|
serializer_class = TransactionTemplateSerializer
|
||||||
filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
|
filter_backends = [RegexSafeSearchFilter, DjangoFilterBackend, OrderingFilter]
|
||||||
filterset_fields = ['name', 'amount', 'display', 'category', 'category__name', ]
|
filterset_fields = ['name', 'amount', 'display', 'category', 'category__name', ]
|
||||||
search_fields = ['$name', '$category__name', ]
|
search_fields = ['$name', '$category__name', ]
|
||||||
ordering_fields = ['amount', ]
|
ordering_fields = ['amount', ]
|
||||||
@ -203,7 +238,7 @@ class TransactionViewSet(ReadProtectedModelViewSet):
|
|||||||
"""
|
"""
|
||||||
queryset = Transaction.objects.order_by('-created_at')
|
queryset = Transaction.objects.order_by('-created_at')
|
||||||
serializer_class = TransactionPolymorphicSerializer
|
serializer_class = TransactionPolymorphicSerializer
|
||||||
filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
|
filter_backends = [RegexSafeSearchFilter, DjangoFilterBackend, OrderingFilter]
|
||||||
filterset_fields = ['source', 'source_alias', 'source__alias__name', 'source__alias__normalized_name',
|
filterset_fields = ['source', 'source_alias', 'source__alias__name', 'source__alias__normalized_name',
|
||||||
'destination', 'destination_alias', 'destination__alias__name',
|
'destination', 'destination_alias', 'destination__alias__name',
|
||||||
'destination__alias__normalized_name', 'quantity', 'polymorphic_ctype', 'amount',
|
'destination__alias__normalized_name', 'quantity', 'polymorphic_ctype', 'amount',
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ def create_special_notes(apps, schema_editor):
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('note', '0001_initial'),
|
('note', '0001_initial'),
|
||||||
|
('logs', '0001_initial'),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
27
apps/note/migrations/0006_trust.py
Normal file
27
apps/note/migrations/0006_trust.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# Generated by Django 2.2.24 on 2021-09-05 19:16
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('note', '0005_auto_20210313_1235'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Trust',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('trusted', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='trusted', to='note.Note', verbose_name='trusted')),
|
||||||
|
('trusting', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='trusting', to='note.Note', verbose_name='trusting')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'frienship',
|
||||||
|
'verbose_name_plural': 'friendships',
|
||||||
|
'unique_together': {('trusting', 'trusted')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
@ -1,13 +1,13 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from .notes import Alias, Note, NoteClub, NoteSpecial, NoteUser
|
from .notes import Alias, Note, NoteClub, NoteSpecial, NoteUser, Trust
|
||||||
from .transactions import MembershipTransaction, Transaction, \
|
from .transactions import MembershipTransaction, Transaction, \
|
||||||
TemplateCategory, TransactionTemplate, RecurrentTransaction, SpecialTransaction
|
TemplateCategory, TransactionTemplate, RecurrentTransaction, SpecialTransaction
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
# Notes
|
# Notes
|
||||||
'Alias', 'Note', 'NoteClub', 'NoteSpecial', 'NoteUser',
|
'Alias', 'Trust', 'Note', 'NoteClub', 'NoteSpecial', 'NoteUser',
|
||||||
# Transactions
|
# Transactions
|
||||||
'MembershipTransaction', 'Transaction', 'TemplateCategory', 'TransactionTemplate',
|
'MembershipTransaction', 'Transaction', 'TemplateCategory', 'TransactionTemplate',
|
||||||
'RecurrentTransaction', 'SpecialTransaction',
|
'RecurrentTransaction', 'SpecialTransaction',
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
import unicodedata
|
import unicodedata
|
||||||
@ -217,6 +217,38 @@ class NoteSpecial(Note):
|
|||||||
return self.special_type
|
return self.special_type
|
||||||
|
|
||||||
|
|
||||||
|
class Trust(models.Model):
|
||||||
|
"""
|
||||||
|
A one-sided trust relationship bertween two users
|
||||||
|
|
||||||
|
If another user considers you as your friend, you can transfer money from
|
||||||
|
them
|
||||||
|
"""
|
||||||
|
|
||||||
|
trusting = models.ForeignKey(
|
||||||
|
Note,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='trusting',
|
||||||
|
verbose_name=_('trusting')
|
||||||
|
)
|
||||||
|
|
||||||
|
trusted = models.ForeignKey(
|
||||||
|
Note,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='trusted',
|
||||||
|
verbose_name=_('trusted')
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("frienship")
|
||||||
|
verbose_name_plural = _("friendships")
|
||||||
|
unique_together = ("trusting", "trusted")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return _("Friendship between {trusting} and {trusted}").format(
|
||||||
|
trusting=str(self.trusting), trusted=str(self.trusted))
|
||||||
|
|
||||||
|
|
||||||
class Alias(models.Model):
|
class Alias(models.Model):
|
||||||
"""
|
"""
|
||||||
points toward a :model:`note.NoteUser` or :model;`note.NoteClub` instance.
|
points toward a :model:`note.NoteUser` or :model;`note.NoteClub` instance.
|
||||||
@ -261,6 +293,11 @@ class Alias(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
self.clean()
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def normalize(string):
|
def normalize(string):
|
||||||
"""
|
"""
|
||||||
@ -289,11 +326,6 @@ class Alias(models.Model):
|
|||||||
pass
|
pass
|
||||||
self.normalized_name = normalized_name
|
self.normalized_name = normalized_name
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
self.clean()
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
|
|
||||||
def delete(self, using=None, keep_parents=False):
|
def delete(self, using=None, keep_parents=False):
|
||||||
if self.name == str(self.note):
|
if self.name == str(self.note):
|
||||||
raise ValidationError(_("You can't delete your main alias."),
|
raise ValidationError(_("You can't delete your main alias."),
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
@ -59,6 +59,7 @@ class TransactionTemplate(models.Model):
|
|||||||
amount = models.PositiveIntegerField(
|
amount = models.PositiveIntegerField(
|
||||||
verbose_name=_('amount'),
|
verbose_name=_('amount'),
|
||||||
)
|
)
|
||||||
|
|
||||||
category = models.ForeignKey(
|
category = models.ForeignKey(
|
||||||
TemplateCategory,
|
TemplateCategory,
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
@ -87,12 +88,12 @@ class TransactionTemplate(models.Model):
|
|||||||
verbose_name = _("transaction template")
|
verbose_name = _("transaction template")
|
||||||
verbose_name_plural = _("transaction templates")
|
verbose_name_plural = _("transaction templates")
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse('note:template_update', args=(self.pk,))
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('note:template_update', args=(self.pk,))
|
||||||
|
|
||||||
|
|
||||||
class Transaction(PolymorphicModel):
|
class Transaction(PolymorphicModel):
|
||||||
"""
|
"""
|
||||||
@ -101,7 +102,6 @@ class Transaction(PolymorphicModel):
|
|||||||
amount is store in centimes of currency, making it a positive integer
|
amount is store in centimes of currency, making it a positive integer
|
||||||
value. (from someone to someone else)
|
value. (from someone to someone else)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
source = models.ForeignKey(
|
source = models.ForeignKey(
|
||||||
Note,
|
Note,
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
@ -166,6 +166,50 @@ class Transaction(PolymorphicModel):
|
|||||||
models.Index(fields=['destination']),
|
models.Index(fields=['destination']),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.__class__.__name__ + " from " + str(self.source) + " to " + str(self.destination) + " of "\
|
||||||
|
+ pretty_money(self.quantity * self.amount) + ("" if self.valid else " invalid")
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
When saving, also transfer money between two notes
|
||||||
|
"""
|
||||||
|
if self.source.pk == self.destination.pk:
|
||||||
|
# When source == destination, no money is transferred and no transaction is created
|
||||||
|
return
|
||||||
|
|
||||||
|
self.source = Note.objects.select_for_update().get(pk=self.source_id)
|
||||||
|
self.destination = Note.objects.select_for_update().get(pk=self.destination_id)
|
||||||
|
|
||||||
|
# Check that the amounts stay between big integer bounds
|
||||||
|
diff_source, diff_dest = self.validate()
|
||||||
|
|
||||||
|
if not (hasattr(self, '_force_save') and self._force_save) \
|
||||||
|
and (not self.source.is_active or not self.destination.is_active):
|
||||||
|
raise ValidationError(_("The transaction can't be saved since the source note "
|
||||||
|
"or the destination note is not active."))
|
||||||
|
|
||||||
|
# If the aliases are not entered, we assume that the used alias is the name of the note
|
||||||
|
if not self.source_alias:
|
||||||
|
self.source_alias = str(self.source)
|
||||||
|
|
||||||
|
if not self.destination_alias:
|
||||||
|
self.destination_alias = str(self.destination)
|
||||||
|
|
||||||
|
# We save first the transaction, in case of the user has no right to transfer money
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
# Save notes
|
||||||
|
self.source.refresh_from_db()
|
||||||
|
self.source.balance += diff_source
|
||||||
|
self.source._force_save = True
|
||||||
|
self.source.save()
|
||||||
|
self.destination.refresh_from_db()
|
||||||
|
self.destination.balance += diff_dest
|
||||||
|
self.destination._force_save = True
|
||||||
|
self.destination.save()
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
previous_source_balance = self.source.balance
|
previous_source_balance = self.source.balance
|
||||||
previous_dest_balance = self.destination.balance
|
previous_dest_balance = self.destination.balance
|
||||||
@ -208,46 +252,6 @@ class Transaction(PolymorphicModel):
|
|||||||
|
|
||||||
return source_balance - previous_source_balance, dest_balance - previous_dest_balance
|
return source_balance - previous_source_balance, dest_balance - previous_dest_balance
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
When saving, also transfer money between two notes
|
|
||||||
"""
|
|
||||||
if self.source.pk == self.destination.pk:
|
|
||||||
# When source == destination, no money is transferred and no transaction is created
|
|
||||||
return
|
|
||||||
|
|
||||||
self.source = Note.objects.select_for_update().get(pk=self.source_id)
|
|
||||||
self.destination = Note.objects.select_for_update().get(pk=self.destination_id)
|
|
||||||
|
|
||||||
# Check that the amounts stay between big integer bounds
|
|
||||||
diff_source, diff_dest = self.validate()
|
|
||||||
|
|
||||||
if not (hasattr(self, '_force_save') and self._force_save) \
|
|
||||||
and (not self.source.is_active or not self.destination.is_active):
|
|
||||||
raise ValidationError(_("The transaction can't be saved since the source note "
|
|
||||||
"or the destination note is not active."))
|
|
||||||
|
|
||||||
# If the aliases are not entered, we assume that the used alias is the name of the note
|
|
||||||
if not self.source_alias:
|
|
||||||
self.source_alias = str(self.source)
|
|
||||||
|
|
||||||
if not self.destination_alias:
|
|
||||||
self.destination_alias = str(self.destination)
|
|
||||||
|
|
||||||
# We save first the transaction, in case of the user has no right to transfer money
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
|
|
||||||
# Save notes
|
|
||||||
self.source.refresh_from_db()
|
|
||||||
self.source.balance += diff_source
|
|
||||||
self.source._force_save = True
|
|
||||||
self.source.save()
|
|
||||||
self.destination.refresh_from_db()
|
|
||||||
self.destination.balance += diff_dest
|
|
||||||
self.destination._force_save = True
|
|
||||||
self.destination.save()
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def total(self):
|
def total(self):
|
||||||
return self.amount * self.quantity
|
return self.amount * self.quantity
|
||||||
@ -256,46 +260,40 @@ class Transaction(PolymorphicModel):
|
|||||||
def type(self):
|
def type(self):
|
||||||
return _('Transfer')
|
return _('Transfer')
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.__class__.__name__ + " from " + str(self.source) + " to " + str(self.destination) + " of "\
|
|
||||||
+ pretty_money(self.quantity * self.amount) + ("" if self.valid else " invalid")
|
|
||||||
|
|
||||||
|
|
||||||
class RecurrentTransaction(Transaction):
|
class RecurrentTransaction(Transaction):
|
||||||
"""
|
"""
|
||||||
Special type of :model:`note.Transaction` associated to a :model:`note.TransactionTemplate`.
|
Special type of :model:`note.Transaction` associated to a :model:`note.TransactionTemplate`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
template = models.ForeignKey(
|
template = models.ForeignKey(
|
||||||
TransactionTemplate,
|
TransactionTemplate,
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("recurrent transaction")
|
||||||
|
verbose_name_plural = _("recurrent transactions")
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
self.clean()
|
||||||
|
return super().save(*args, **kwargs)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
if self.template.destination != self.destination and not (hasattr(self, '_force_save') and self._force_save):
|
if self.template.destination != self.destination and not (hasattr(self, '_force_save') and self._force_save):
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
_("The destination of this transaction must equal to the destination of the template."))
|
_("The destination of this transaction must equal to the destination of the template."))
|
||||||
return super().clean()
|
return super().clean()
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
self.clean()
|
|
||||||
return super().save(*args, **kwargs)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def type(self):
|
def type(self):
|
||||||
return _('Template')
|
return _('Template')
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _("recurrent transaction")
|
|
||||||
verbose_name_plural = _("recurrent transactions")
|
|
||||||
|
|
||||||
|
|
||||||
class SpecialTransaction(Transaction):
|
class SpecialTransaction(Transaction):
|
||||||
"""
|
"""
|
||||||
Special type of :model:`note.Transaction` associated to transactions with special notes
|
Special type of :model:`note.Transaction` associated to transactions with special notes
|
||||||
"""
|
"""
|
||||||
|
|
||||||
last_name = models.CharField(
|
last_name = models.CharField(
|
||||||
max_length=255,
|
max_length=255,
|
||||||
verbose_name=_("name"),
|
verbose_name=_("name"),
|
||||||
@ -312,6 +310,15 @@ class SpecialTransaction(Transaction):
|
|||||||
blank=True,
|
blank=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Special transaction")
|
||||||
|
verbose_name_plural = _("Special transactions")
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
self.clean()
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def type(self):
|
def type(self):
|
||||||
return _('Credit') if isinstance(self.source, NoteSpecial) else _("Debit")
|
return _('Credit') if isinstance(self.source, NoteSpecial) else _("Debit")
|
||||||
@ -325,13 +332,8 @@ class SpecialTransaction(Transaction):
|
|||||||
def clean(self):
|
def clean(self):
|
||||||
# SpecialTransaction are only possible with NoteSpecial object
|
# SpecialTransaction are only possible with NoteSpecial object
|
||||||
if self.is_credit() == self.is_debit():
|
if self.is_credit() == self.is_debit():
|
||||||
raise(ValidationError(_("A special transaction is only possible between a"
|
raise ValidationError(_("A special transaction is only possible between a"
|
||||||
" Note associated to a payment method and a User or a Club")))
|
" Note associated to a payment method and a User or a Club"))
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
self.clean()
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def validate_payment_form(form):
|
def validate_payment_form(form):
|
||||||
@ -363,17 +365,11 @@ class SpecialTransaction(Transaction):
|
|||||||
|
|
||||||
return not error
|
return not error
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _("Special transaction")
|
|
||||||
verbose_name_plural = _("Special transactions")
|
|
||||||
|
|
||||||
|
|
||||||
class MembershipTransaction(Transaction):
|
class MembershipTransaction(Transaction):
|
||||||
"""
|
"""
|
||||||
Special type of :model:`note.Transaction` associated to a :model:`member.Membership`.
|
Special type of :model:`note.Transaction` associated to a :model:`member.Membership`.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
membership = models.OneToOneField(
|
membership = models.OneToOneField(
|
||||||
'member.Membership',
|
'member.Membership',
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
// Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// When a transaction is performed, lock the interface to prevent spam clicks.
|
// When a transaction is performed, lock the interface to prevent spam clicks.
|
||||||
@ -221,7 +221,7 @@ function consume (source, source_alias, dest, quantity, amount, reason, type, ca
|
|||||||
.done(function () {
|
.done(function () {
|
||||||
if (!isNaN(source.balance)) {
|
if (!isNaN(source.balance)) {
|
||||||
const newBalance = source.balance - quantity * amount
|
const newBalance = source.balance - quantity * amount
|
||||||
if (newBalance <= -5000) {
|
if (newBalance <= -2000) {
|
||||||
addMsg(interpolate(gettext('Warning, the transaction from the note %s succeed, ' +
|
addMsg(interpolate(gettext('Warning, the transaction from the note %s succeed, ' +
|
||||||
'but the emitter note %s is very negative.'), [source_alias, source_alias]), 'danger', 30000)
|
'but the emitter note %s is very negative.'), [source_alias, source_alias]), 'danger', 30000)
|
||||||
} else if (newBalance < 0) {
|
} else if (newBalance < 0) {
|
||||||
@ -258,3 +258,39 @@ function consume (source, source_alias, dest, quantity, amount, reason, type, ca
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var searchbar = document.getElementById("search-input")
|
||||||
|
var search_results = document.getElementById("search-results")
|
||||||
|
|
||||||
|
var old_pattern = null;
|
||||||
|
var firstMatch = null;
|
||||||
|
/**
|
||||||
|
* Updates the button search tab
|
||||||
|
* @param force Forces the update even if the pattern didn't change
|
||||||
|
*/
|
||||||
|
function updateSearch(force = false) {
|
||||||
|
let pattern = searchbar.value
|
||||||
|
if (pattern === "")
|
||||||
|
firstMatch = null;
|
||||||
|
if ((pattern === old_pattern || pattern === "") && !force)
|
||||||
|
return;
|
||||||
|
firstMatch = null;
|
||||||
|
const re = new RegExp(pattern, "i");
|
||||||
|
Array.from(search_results.children).forEach(function(b) {
|
||||||
|
if (re.test(b.innerText)) {
|
||||||
|
b.hidden = false;
|
||||||
|
if (firstMatch === null) {
|
||||||
|
firstMatch = b;
|
||||||
|
}
|
||||||
|
} else
|
||||||
|
b.hidden = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
searchbar.addEventListener("input", function (e) {
|
||||||
|
debounce(updateSearch)()
|
||||||
|
});
|
||||||
|
searchbar.addEventListener("keyup", function (e) {
|
||||||
|
if (firstMatch && e.key === "Enter")
|
||||||
|
firstMatch.click()
|
||||||
|
});
|
||||||
|
@ -314,7 +314,7 @@ $('#btn_transfer').click(function () {
|
|||||||
|
|
||||||
if (!isNaN(source.note.balance)) {
|
if (!isNaN(source.note.balance)) {
|
||||||
const newBalance = source.note.balance - source.quantity * dest.quantity * amount
|
const newBalance = source.note.balance - source.quantity * dest.quantity * amount
|
||||||
if (newBalance <= -5000) {
|
if (newBalance <= -2000) {
|
||||||
addMsg(interpolate(gettext('Warning, the transaction of %s from the note %s to the note %s succeed, but the emitter note %s is very negative.'),
|
addMsg(interpolate(gettext('Warning, the transaction of %s from the note %s to the note %s succeed, but the emitter note %s is very negative.'),
|
||||||
[pretty_money(source.quantity * dest.quantity * amount), source.name, dest.name, source.name]), 'danger', 10000)
|
[pretty_money(source.quantity * dest.quantity * amount), source.name, dest.name, source.name]), 'danger', 10000)
|
||||||
reset()
|
reset()
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
import html
|
import html
|
||||||
|
|
||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
from django.utils.html import format_html
|
from django.utils.html import format_html, mark_safe
|
||||||
from django_tables2.utils import A
|
from django_tables2.utils import A
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from note_kfet.middlewares import get_current_request
|
from note_kfet.middlewares import get_current_request
|
||||||
from permission.backends import PermissionBackend
|
from permission.backends import PermissionBackend
|
||||||
|
|
||||||
from .models.notes import Alias
|
from .models.notes import Alias, Trust
|
||||||
from .models.transactions import Transaction, TransactionTemplate
|
from .models.transactions import Transaction, TransactionTemplate
|
||||||
from .templatetags.pretty_money import pretty_money
|
from .templatetags.pretty_money import pretty_money
|
||||||
|
|
||||||
@ -148,6 +148,71 @@ DELETE_TEMPLATE = """
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class TrustTable(tables.Table):
|
||||||
|
class Meta:
|
||||||
|
attrs = {
|
||||||
|
'class': 'table table condensed table-striped',
|
||||||
|
'id': "trust_table"
|
||||||
|
}
|
||||||
|
model = Trust
|
||||||
|
fields = ("trusted",)
|
||||||
|
template_name = 'django_tables2/bootstrap4.html'
|
||||||
|
|
||||||
|
show_header = False
|
||||||
|
trusted = 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(), "note.delete_trust", record)
|
||||||
|
else '')}},
|
||||||
|
verbose_name=_("Delete"),)
|
||||||
|
|
||||||
|
|
||||||
|
class TrustedTable(tables.Table):
|
||||||
|
class Meta:
|
||||||
|
attrs = {
|
||||||
|
'class': 'table table condensed table-striped',
|
||||||
|
'id': 'trusted_table'
|
||||||
|
}
|
||||||
|
Model = Trust
|
||||||
|
fields = ("trusting",)
|
||||||
|
template_name = "django_tables2/bootstrap4.html"
|
||||||
|
|
||||||
|
show_header = False
|
||||||
|
trusting = tables.Column(attrs={
|
||||||
|
'td': {'class': 'text-center', 'width': '100%'}})
|
||||||
|
|
||||||
|
trust_back = tables.Column(
|
||||||
|
verbose_name=_("Trust back"),
|
||||||
|
accessor="pk",
|
||||||
|
attrs={
|
||||||
|
'td': {
|
||||||
|
'class': '',
|
||||||
|
'id': lambda record: "trust_back_" + str(record.pk),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def render_trust_back(self, record):
|
||||||
|
user_note = record.trusted
|
||||||
|
trusting_note = record.trusting
|
||||||
|
if Trust.objects.filter(trusted=trusting_note, trusting=user_note):
|
||||||
|
return ""
|
||||||
|
val = '<button id="'
|
||||||
|
val += str(record.pk)
|
||||||
|
val += '" class="btn btn-success btn-sm text-nowrap" \
|
||||||
|
onclick="create_trust(' + str(record.trusted.pk) + ',' + \
|
||||||
|
str(record.trusting.pk) + ')">'
|
||||||
|
val += str(_("Add back"))
|
||||||
|
val += '</button>'
|
||||||
|
return mark_safe(val)
|
||||||
|
|
||||||
|
|
||||||
class AliasTable(tables.Table):
|
class AliasTable(tables.Table):
|
||||||
class Meta:
|
class Meta:
|
||||||
attrs = {
|
attrs = {
|
||||||
@ -197,6 +262,17 @@ class ButtonTable(tables.Table):
|
|||||||
verbose_name=_("Edit"),
|
verbose_name=_("Edit"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
hideshow = tables.Column(
|
||||||
|
verbose_name=_("Hide/Show"),
|
||||||
|
accessor="pk",
|
||||||
|
attrs={
|
||||||
|
'td': {
|
||||||
|
'class': 'col-sm-1',
|
||||||
|
'id': lambda record: "hideshow_" + str(record.pk),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
delete_col = tables.TemplateColumn(template_code=DELETE_TEMPLATE,
|
delete_col = tables.TemplateColumn(template_code=DELETE_TEMPLATE,
|
||||||
extra_context={"delete_trans": _('delete')},
|
extra_context={"delete_trans": _('delete')},
|
||||||
attrs={'td': {'class': 'col-sm-1'}},
|
attrs={'td': {'class': 'col-sm-1'}},
|
||||||
@ -204,3 +280,16 @@ class ButtonTable(tables.Table):
|
|||||||
|
|
||||||
def render_amount(self, value):
|
def render_amount(self, value):
|
||||||
return pretty_money(value)
|
return pretty_money(value)
|
||||||
|
|
||||||
|
def order_category(self, queryset, is_descending):
|
||||||
|
return queryset.order_by(f"{'-' if is_descending else ''}category__name"), True
|
||||||
|
|
||||||
|
def render_hideshow(self, record):
|
||||||
|
val = '<button id="'
|
||||||
|
val += str(record.pk)
|
||||||
|
val += '" class="btn btn-secondary btn-sm" \
|
||||||
|
onclick="hideshow(' + str(record.id) + ',' + \
|
||||||
|
str(record.display).lower() + ')">'
|
||||||
|
val += str(_("Hide/Show"))
|
||||||
|
val += '</button>'
|
||||||
|
return mark_safe(val)
|
||||||
|
@ -103,6 +103,11 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link font-weight-bold" data-toggle="tab" href="#search">
|
||||||
|
{% trans "Search" %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -123,6 +128,20 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
<div class="tab-pane" id="search">
|
||||||
|
<input class="form-control mx-auto d-block mb-3"
|
||||||
|
placeholder="{% trans "Search button..." %}" type="search" id="search-input"/>
|
||||||
|
<div class="d-inline-flex flex-wrap justify-content-center" id="search-results">
|
||||||
|
{% for button in all_buttons %}
|
||||||
|
{% if button.display %}
|
||||||
|
<button class="btn btn-outline-dark rounded-0 flex-fill" hidden
|
||||||
|
id="search_button{{ button.id }}" name="button" value="{{ button.name }}">
|
||||||
|
{{ button.name }} ({{ button.amount | pretty_money }})
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -163,7 +182,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
{% for button in highlighted %}
|
{% for button in highlighted %}
|
||||||
{% if button.display %}
|
{% if button.display %}
|
||||||
$("#highlighted_button{{ button.id }}").click(function() {
|
document.getElementById("highlighted_button{{ button.id }}").addEventListener("click", function() {
|
||||||
addConso({{ button.destination_id }}, {{ button.amount }},
|
addConso({{ button.destination_id }}, {{ button.amount }},
|
||||||
{{ polymorphic_ctype }}, {{ button.category_id }}, "{{ button.category.name|escapejs }}",
|
{{ polymorphic_ctype }}, {{ button.category_id }}, "{{ button.category.name|escapejs }}",
|
||||||
{{ button.id }}, "{{ button.name|escapejs }}");
|
{{ button.id }}, "{{ button.name|escapejs }}");
|
||||||
@ -174,7 +193,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
{% for category in categories %}
|
{% for category in categories %}
|
||||||
{% for button in category.templates_filtered %}
|
{% for button in category.templates_filtered %}
|
||||||
{% if button.display %}
|
{% if button.display %}
|
||||||
$("#button{{ button.id }}").click(function() {
|
document.getElementById("button{{ button.id }}").addEventListener("click", function() {
|
||||||
addConso({{ button.destination_id }}, {{ button.amount }},
|
addConso({{ button.destination_id }}, {{ button.amount }},
|
||||||
{{ polymorphic_ctype }}, {{ button.category_id }}, "{{ button.category.name|escapejs }}",
|
{{ polymorphic_ctype }}, {{ button.category_id }}, "{{ button.category.name|escapejs }}",
|
||||||
{{ button.id }}, "{{ button.name|escapejs }}");
|
{{ button.id }}, "{{ button.name|escapejs }}");
|
||||||
@ -182,5 +201,15 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
|
{% for button in all_buttons %}
|
||||||
|
{% if button.display %}
|
||||||
|
document.getElementById("search_button{{ button.id }}").addEventListener("click", function() {
|
||||||
|
addConso({{ button.destination_id }}, {{ button.amount }},
|
||||||
|
{{ polymorphic_ctype }}, {{ button.category_id }}, "{{ button.category.name|escapejs }}",
|
||||||
|
{{ button.id }}, "{{ button.name|escapejs }}");
|
||||||
|
});
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -22,8 +22,8 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<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 € depuis plus de 24h.
|
est inférieur à 0 €.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
@ -43,4 +43,4 @@
|
|||||||
{% trans "Mail generated by the Note Kfet on the" %} {% now "j F Y à H:i:s" %}
|
{% trans "Mail generated by the Note Kfet on the" %} {% now "j F Y à H:i:s" %}
|
||||||
</p>
|
</p>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -9,7 +9,7 @@ Ce mail t'a été envoyé parce que le solde de ta Note Kfet
|
|||||||
|
|
||||||
Ton solde actuel est de {{ note.balance|pretty_money }}.
|
Ton solde actuel est de {{ note.balance|pretty_money }}.
|
||||||
|
|
||||||
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·e·s dont le solde
|
||||||
est inférieur à 0 € depuis plus de 24h.
|
est inférieur à 0 € depuis plus de 24h.
|
||||||
|
|
||||||
Si tu ne comprends pas ton solde, tu peux consulter ton historique
|
Si tu ne comprends pas ton solde, tu peux consulter ton historique
|
||||||
@ -22,4 +22,4 @@ virement bancaire.
|
|||||||
--
|
--
|
||||||
Le BDE
|
Le BDE
|
||||||
|
|
||||||
{% trans "Mail generated by the Note Kfet on the" %} {% now "j F Y à H:i:s" %}
|
{% trans "Mail generated by the Note Kfet on the" %} {% now "j F Y à H:i:s" %}
|
||||||
|
@ -31,29 +31,29 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
|
|
||||||
{% block extrajavascript %}
|
{% block extrajavascript %}
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
|
function refreshMatchedWords() {
|
||||||
|
$("tr").each(function() {
|
||||||
|
let pattern = $('#search_field').val();
|
||||||
|
if (pattern) {
|
||||||
|
$(this).find("td:eq(0), td:eq(1), td:eq(3), td:eq(6)").each(function () {
|
||||||
|
$(this).html($(this).text().replace(new RegExp(pattern, 'i'), "<mark>$&</mark>"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function reloadTable() {
|
||||||
|
let pattern = $('#search_field').val();
|
||||||
|
$("#buttons_table").load(location.pathname + "?search=" + pattern.replace(" ", "%20") + " #buttons_table", refreshMatchedWords);
|
||||||
|
}
|
||||||
|
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
let searchbar_obj = $("#search_field");
|
let searchbar_obj = $("#search_field");
|
||||||
let timer_on = false;
|
let timer_on = false;
|
||||||
let timer;
|
let timer;
|
||||||
|
|
||||||
function refreshMatchedWords() {
|
|
||||||
$("tr").each(function() {
|
|
||||||
let pattern = searchbar_obj.val();
|
|
||||||
if (pattern) {
|
|
||||||
$(this).find("td:eq(0), td:eq(1), td:eq(3), td:eq(6)").each(function () {
|
|
||||||
$(this).html($(this).text().replace(new RegExp(pattern, 'i'), "<mark>$&</mark>"));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshMatchedWords();
|
refreshMatchedWords();
|
||||||
|
|
||||||
function reloadTable() {
|
|
||||||
let pattern = searchbar_obj.val();
|
|
||||||
$("#buttons_table").load(location.pathname + "?search=" + pattern.replace(" ", "%20") + " #buttons_table", refreshMatchedWords);
|
|
||||||
}
|
|
||||||
|
|
||||||
searchbar_obj.keyup(function() {
|
searchbar_obj.keyup(function() {
|
||||||
if (timer_on)
|
if (timer_on)
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
@ -77,5 +77,28 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
addMsg('{% trans "Unable to delete button "%} #' + button_id, 'danger')
|
addMsg('{% trans "Unable to delete button "%} #' + button_id, 'danger')
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// on click of button "hide/show", call the API
|
||||||
|
function hideshow(id, displayed) {
|
||||||
|
$.ajax({
|
||||||
|
url: '/api/note/transaction/template/' + id + '/',
|
||||||
|
type: 'PATCH',
|
||||||
|
dataType: 'json',
|
||||||
|
headers: {
|
||||||
|
'X-CSRFTOKEN': CSRF_TOKEN
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
display: !displayed
|
||||||
|
},
|
||||||
|
success: function() {
|
||||||
|
if(displayed)
|
||||||
|
addMsg("{% trans "Button hidden"%}", 'success', 1000)
|
||||||
|
else addMsg("{% trans "Button displayed"%}", 'success', 1000)
|
||||||
|
reloadTable()
|
||||||
|
},
|
||||||
|
error: function (err) {
|
||||||
|
addMsg("{% trans "An error occured"%}", 'danger')
|
||||||
|
}})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django import template
|
from django import template
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django import template
|
from django import template
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from api.tests import TestAPI
|
from api.tests import TestAPI
|
||||||
@ -10,7 +10,7 @@ from django.urls import reverse
|
|||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from permission.models import Role
|
from permission.models import Role
|
||||||
|
|
||||||
from ..api.views import AliasViewSet, ConsumerViewSet, NotePolymorphicViewSet, TemplateCategoryViewSet,\
|
from ..api.views import AliasViewSet, ConsumerViewSet, NotePolymorphicViewSet, TemplateCategoryViewSet, \
|
||||||
TransactionTemplateViewSet, TransactionViewSet
|
TransactionTemplateViewSet, TransactionViewSet
|
||||||
from ..models import NoteUser, Transaction, TemplateCategory, TransactionTemplate, RecurrentTransaction, \
|
from ..models import NoteUser, Transaction, TemplateCategory, TransactionTemplate, RecurrentTransaction, \
|
||||||
MembershipTransaction, SpecialTransaction, NoteSpecial, Alias, Note
|
MembershipTransaction, SpecialTransaction, NoteSpecial, Alias, Note
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
import json
|
import json
|
||||||
@ -10,12 +10,13 @@ from django.core.exceptions import PermissionDenied
|
|||||||
from django.db.models import Q, F
|
from django.db.models import Q, F
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.generic import CreateView, UpdateView, DetailView
|
from django.views.generic import CreateView, UpdateView, DetailView
|
||||||
from django_tables2 import SingleTableView
|
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
|
from django_tables2 import SingleTableView
|
||||||
from activity.models import Entry
|
from activity.models import Entry
|
||||||
from note_kfet.inputs import AmountInput
|
from api.viewsets import is_regex
|
||||||
from permission.backends import PermissionBackend
|
from permission.backends import PermissionBackend
|
||||||
from permission.views import ProtectQuerysetMixin
|
from permission.views import ProtectQuerysetMixin
|
||||||
|
from note_kfet.inputs import AmountInput
|
||||||
|
|
||||||
from .forms import TransactionTemplateForm, SearchTransactionForm
|
from .forms import TransactionTemplateForm, SearchTransactionForm
|
||||||
from .models import TemplateCategory, Transaction, TransactionTemplate, RecurrentTransaction, NoteSpecial, Note
|
from .models import TemplateCategory, Transaction, TransactionTemplate, RecurrentTransaction, NoteSpecial, Note
|
||||||
@ -89,11 +90,15 @@ class TransactionTemplateListView(ProtectQuerysetMixin, LoginRequiredMixin, Sing
|
|||||||
qs = super().get_queryset().distinct()
|
qs = super().get_queryset().distinct()
|
||||||
if "search" in self.request.GET:
|
if "search" in self.request.GET:
|
||||||
pattern = self.request.GET["search"]
|
pattern = self.request.GET["search"]
|
||||||
|
|
||||||
|
# Check if this is a valid regex. If not, we won't check regex
|
||||||
|
valid_regex = is_regex(pattern)
|
||||||
|
suffix = "__iregex" if valid_regex else "__icontains"
|
||||||
qs = qs.filter(
|
qs = qs.filter(
|
||||||
Q(name__iregex="^" + pattern)
|
Q(**{f"name{suffix}": pattern})
|
||||||
| Q(destination__club__name__iregex="^" + pattern)
|
| Q(**{f"destination__club__name{suffix}": pattern})
|
||||||
| Q(category__name__iregex="^" + pattern)
|
| Q(**{f"category__name{suffix}": pattern})
|
||||||
| Q(description__iregex=pattern)
|
| Q(**{f"description{suffix}": pattern})
|
||||||
)
|
)
|
||||||
|
|
||||||
qs = qs.order_by('-display', 'category__name', 'destination__club__name', 'name')
|
qs = qs.order_by('-display', 'category__name', 'destination__club__name', 'name')
|
||||||
@ -190,6 +195,10 @@ class ConsoView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
|||||||
).order_by('name').all()
|
).order_by('name').all()
|
||||||
context['polymorphic_ctype'] = ContentType.objects.get_for_model(RecurrentTransaction).pk
|
context['polymorphic_ctype'] = ContentType.objects.get_for_model(RecurrentTransaction).pk
|
||||||
|
|
||||||
|
context['all_buttons'] = TransactionTemplate.objects.filter(
|
||||||
|
PermissionBackend.filter_queryset(self.request, TransactionTemplate, "view")
|
||||||
|
).filter(display=True).order_by('name').all()
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
@ -219,7 +228,10 @@ class TransactionSearchView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView
|
|||||||
if "type" in data and data["type"]:
|
if "type" in data and data["type"]:
|
||||||
transactions = transactions.filter(polymorphic_ctype__in=data["type"])
|
transactions = transactions.filter(polymorphic_ctype__in=data["type"])
|
||||||
if "reason" in data and data["reason"]:
|
if "reason" in data and data["reason"]:
|
||||||
transactions = transactions.filter(reason__iregex=data["reason"])
|
# Check if this is a valid regex. If not, we won't check regex
|
||||||
|
valid_regex = is_regex(data["reason"])
|
||||||
|
suffix = "__iregex" if valid_regex else "__istartswith"
|
||||||
|
transactions = transactions.filter(Q(**{f"reason{suffix}": data["reason"]}))
|
||||||
if "valid" in data and data["valid"]:
|
if "valid" in data and data["valid"]:
|
||||||
transactions = transactions.filter(valid=data["valid"])
|
transactions = transactions.filter(valid=data["valid"])
|
||||||
if "amount_gte" in data and data["amount_gte"]:
|
if "amount_gte" in data and data["amount_gte"]:
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
default_app_config = 'permission.apps.PermissionConfig'
|
default_app_config = 'permission.apps.PermissionConfig'
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-lateré
|
# SPDX-License-Identifier: GPL-3.0-or-lateré
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from .views import PermissionViewSet, RoleViewSet
|
from .views import PermissionViewSet, RoleViewSet
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from api.viewsets import ReadOnlyProtectedModelViewSet
|
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
from rest_framework.filters import SearchFilter
|
from api.filters import RegexSafeSearchFilter
|
||||||
|
from api.viewsets import ReadOnlyProtectedModelViewSet
|
||||||
|
|
||||||
from .serializers import PermissionSerializer, RoleSerializer
|
from .serializers import PermissionSerializer, RoleSerializer
|
||||||
from ..models import Permission, Role
|
from ..models import Permission, Role
|
||||||
@ -17,9 +17,9 @@ class PermissionViewSet(ReadOnlyProtectedModelViewSet):
|
|||||||
"""
|
"""
|
||||||
queryset = Permission.objects.order_by('id')
|
queryset = Permission.objects.order_by('id')
|
||||||
serializer_class = PermissionSerializer
|
serializer_class = PermissionSerializer
|
||||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||||
filterset_fields = ['model', 'type', 'query', 'mask', 'field', 'permanent', ]
|
filterset_fields = ['model', 'type', 'query', 'mask', 'field', 'permanent', ]
|
||||||
search_fields = ['$model__name', '$query', '$description', ]
|
search_fields = ['$model__model', '$query', '$description', ]
|
||||||
|
|
||||||
|
|
||||||
class RoleViewSet(ReadOnlyProtectedModelViewSet):
|
class RoleViewSet(ReadOnlyProtectedModelViewSet):
|
||||||
@ -30,6 +30,6 @@ class RoleViewSet(ReadOnlyProtectedModelViewSet):
|
|||||||
"""
|
"""
|
||||||
queryset = Role.objects.order_by('id')
|
queryset = Role.objects.order_by('id')
|
||||||
serializer_class = RoleSerializer
|
serializer_class = RoleSerializer
|
||||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||||
filterset_fields = ['name', 'permissions', 'for_club', 'memberships__user', ]
|
filterset_fields = ['name', 'permissions', 'for_club', 'memberships__user', ]
|
||||||
search_fields = ['$name', '$for_club__name', ]
|
search_fields = ['$name', '$for_club__name', ]
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from datetime import date
|
from datetime import date
|
||||||
@ -198,6 +198,41 @@ class PermissionBackend(ModelBackend):
|
|||||||
def has_module_perms(self, user_obj, app_label):
|
def has_module_perms(self, user_obj, app_label):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@memoize
|
||||||
|
def has_model_perm(request, model, type):
|
||||||
|
"""
|
||||||
|
Check is the given user has the permission over a given model for a given action.
|
||||||
|
The result is then memoized.
|
||||||
|
:param request: The current request
|
||||||
|
:param model: The model that the permissions shoud apply
|
||||||
|
:param type: The type of the permissions: view, change, add or delete
|
||||||
|
For view action, it is consider possible if user can view or change the model
|
||||||
|
"""
|
||||||
|
# Requested by a shell
|
||||||
|
if request is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
user_obj = request.user
|
||||||
|
sess = request.session
|
||||||
|
|
||||||
|
if hasattr(request, 'auth') and request.auth is not None and hasattr(request.auth, 'scope'):
|
||||||
|
# OAuth2 Authentication
|
||||||
|
user_obj = request.auth.user
|
||||||
|
|
||||||
|
if user_obj is None or user_obj.is_anonymous:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if user_obj.is_superuser and sess.get("permission_mask", -1) >= 42:
|
||||||
|
return True
|
||||||
|
|
||||||
|
ct = ContentType.objects.get_for_model(model)
|
||||||
|
if any(PermissionBackend.permissions(request, ct, type)):
|
||||||
|
return True
|
||||||
|
if type == "view" and any(PermissionBackend.permissions(request, ct, "change")):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
def get_all_permissions(self, user_obj, obj=None):
|
def get_all_permissions(self, user_obj, obj=None):
|
||||||
ct = ContentType.objects.get_for_model(obj)
|
ct = ContentType.objects.get_for_model(obj)
|
||||||
return list(self.permissions(get_current_request(), ct, "view"))
|
return list(self.permissions(get_current_request(), ct, "view"))
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user