mirror of
https://gitlab.crans.org/bde/nk20
synced 2025-06-21 09:58:23 +02:00
Compare commits
441 Commits
faster_ci
...
076d065ffa
Author | SHA1 | Date | |
---|---|---|---|
076d065ffa | |||
2da77d9c17 | |||
01584d6330 | |||
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 | |||
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
|
|||
0bd447b608 | |||
3f3c93d928 | |||
340c90f5d3 | |||
ca2b9f061c | |||
a05dfcbf3d
|
|||
ba3c0fb18d
|
|||
ab69963ea1 | |||
654c01631a
|
|||
d94cc2a7ad
|
|||
69bb38297f
|
|||
9628560d64
|
|||
df3bb71357
|
|||
2a216fd994
|
|||
8dd2619013
|
|||
62431a4910
|
|||
946bc1e497 | |||
d4896bfd76
|
|||
23f46cc598
|
|||
d1a9f21b56 | |||
d809b2595a
|
|||
97803ac983 | |||
b951c4aa05 | |||
69b3d2ac9c
|
|||
f29054558a
|
|||
11dd8adbb7 | |||
d437f2bdbd
|
|||
ac8453b04c
|
|||
6b4d18f4b3 | |||
668cfa71a7 | |||
161db0b00b
|
|||
8638c16b34
|
|||
9583cec3ff
|
|||
1ef25924a0
|
|||
e89383e3f4
|
|||
79a116d9c6
|
|||
aa75ce5c7a
|
|||
a3a9dfc812
|
|||
76531595ad
|
|||
a0b920ac94
|
|||
ab2e580e68
|
|||
0234f19a33
|
|||
1a4b7c83e8
|
|||
4c17e2a92b
|
|||
e68afc7d0a
|
|||
c6e3b54f94
|
|||
7e6a14296a | |||
780f78b385 | |||
4e3c32eb5e
|
|||
ef118c2445
|
|||
600ba15faa
|
|||
944bb127e2
|
|||
f6d042c998
|
|||
bb9a0a2593
|
|||
61feac13c7
|
|||
81e708a7e3
|
|||
3532846c87
|
|||
49551e88f8
|
|||
db936bf75a
|
|||
5828a20383 | |||
cea3138daf | |||
fb98d9cd8b
|
|||
0dd3da5c01
|
|||
af4be98b5b
|
|||
be6059eba6
|
|||
5793b83de7
|
|||
2c02c747f4
|
|||
a78f3b7caa
|
|||
1ee40cb94e
|
|||
bd035744a4
|
|||
7edd622755
|
|||
8fd5b6ee01
|
|||
03411ac9bd
|
|||
d965732b65
|
|||
048266ed61
|
|||
b27341009e
|
|||
da1e15c5e6
|
|||
4b03a78ad6
|
|||
fb6e3c3de0
|
|||
391f3bde8f
|
|||
ad04e45992
|
|||
4e1ba1447a
|
|||
b646f549d6
|
|||
ba9ef0371a
|
|||
881cd88f48
|
|||
b4ed354b73 | |||
e5051ab018
|
|||
bb69627ac5
|
|||
ffaa020310
|
|||
6d2b7054e2
|
|||
d888d5863a
|
|||
dbc7b3444b
|
|||
f25eb1d2c5
|
|||
a2a749e1ca
|
|||
5bf6a5501d
|
|||
9523b5f05f
|
|||
5eb3ffca66 | |||
9930c48253 | |||
d902e63a0c
|
|||
48b0bade51
|
|||
f75dbc4525
|
|||
fbf64db16e
|
|||
a3fd8ba063
|
|||
9b26207515
|
|||
7ea36a5415
|
|||
898f6d52bf
|
|||
8be16e7b58
|
|||
ea092803d7
|
|||
5e9f36ef1a
|
|||
b4d87bc6b5
|
|||
dd639d829e
|
|||
7b809ff3a6 | |||
d36edfc063
|
|||
cf87da096f
|
|||
e452b7acbf
|
|||
74ab4df9fe
|
|||
451851c955
|
|||
789ca149af | |||
7d3f1930b8 | |||
e8f4ca1e09
|
|||
733f145be3
|
|||
48c37353ea
|
|||
8056dc096d
|
|||
6d5b69cd26
|
|||
a7bdffd71a
|
|||
0887e4bbde
|
|||
199f4ca1f2
|
|||
802a6c68cb
|
|||
41a0b3a1c1
|
|||
aa35724be2
|
|||
9086d33158
|
|||
43d214b982
|
|||
b93e4a8d11
|
|||
b9a9704061
|
|||
fee52f326a
|
|||
317966d5c1 | |||
9f0a22d3d1
|
|||
a5ecdd100c | |||
f60691846b
|
|||
d5ecb72a71
|
|||
8cf9dfb9b9
|
|||
c3ab61bd04
|
|||
0b4b6dcb3e | |||
0d5f6c0332 | |||
7b28938cde
|
|||
35ffb36fbd
|
|||
08ba0b263a | |||
c4c4e9594f | |||
4166823d55 | |||
dc0f3dbcef | |||
4583958f50 | |||
b3abe9ab18 | |||
27f23b48b6 | |||
67e170d4a6 | |||
8f895dc4d7 | |||
1187577728 | |||
8a58af3b31 | |||
0c23625147 | |||
21219b9c62 | |||
5ab8beecef | |||
1ca5133026 | |||
93bc6bb245 | |||
952c4383e7 | |||
15dd2b8f0c
|
|||
c540b6334c
|
|||
bab394908d | |||
0b93968b9e | |||
97375ef6c0
|
|||
36cfcd533f
|
|||
21dbc53615
|
|||
e6f10ebdac
|
|||
47968844ce
|
|||
a435460e29
|
|||
b7c4360108
|
|||
8d8c417c50
|
|||
2b189af25b
|
|||
5a07c8a94f
|
|||
6cc1857eb6
|
|||
601534d610
|
|||
c271593839
|
|||
f351794aa0
|
|||
2793fee58c | |||
7a715df121
|
|||
9308878054
|
|||
b5ccf5b800
|
|||
5e63254439 | |||
da96506218
|
|||
b4714b896a | |||
cdb2647a4d
|
|||
cc12e3ec63
|
|||
be168c5ada
|
|||
b46ae6f856
|
|||
ec0bcbf015
|
|||
81303b8ef8 | |||
910b98fefc
|
|||
5a7a219ba8
|
|||
116451603c | |||
b2437ef9b5
|
|||
d8c9618772
|
|||
c825dee95a
|
|||
73d27e820b
|
|||
40e1b42078
|
|||
72806f0ace
|
|||
b244e01231
|
|||
76d1784aea
|
|||
56c5fa4057
|
|||
b5ef937a03
|
|||
e95a8b6e18
|
|||
635adf1360
|
@ -1,3 +0,0 @@
|
|||||||
skip_list:
|
|
||||||
- command-instead-of-shell # Use shell only when shell functionality is required
|
|
||||||
- experimental # all rules tagged as experimental
|
|
@ -10,7 +10,6 @@ DJANGO_SECRET_KEY=CHANGE_ME
|
|||||||
DJANGO_SETTINGS_MODULE=note_kfet.settings
|
DJANGO_SETTINGS_MODULE=note_kfet.settings
|
||||||
CONTACT_EMAIL=tresorerie.bde@localhost
|
CONTACT_EMAIL=tresorerie.bde@localhost
|
||||||
NOTE_URL=localhost
|
NOTE_URL=localhost
|
||||||
DOMAIN=localhost
|
|
||||||
|
|
||||||
# Config for mails. Only used in production
|
# Config for mails. Only used in production
|
||||||
NOTE_MAIL=notekfet@localhost
|
NOTE_MAIL=notekfet@localhost
|
||||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -42,11 +42,13 @@ map.json
|
|||||||
backups/
|
backups/
|
||||||
/static/
|
/static/
|
||||||
/media/
|
/media/
|
||||||
|
/tmp/
|
||||||
|
|
||||||
# Virtualenv
|
# Virtualenv
|
||||||
env/
|
env/
|
||||||
venv/
|
venv/
|
||||||
db.sqlite3
|
db.sqlite3
|
||||||
|
shell.nix
|
||||||
|
|
||||||
# ansibles customs host
|
# ansibles customs host
|
||||||
ansible/host_vars/*.yaml
|
ansible/host_vars/*.yaml
|
||||||
|
@ -8,27 +8,55 @@ variables:
|
|||||||
GIT_SUBMODULE_STRATEGY: recursive
|
GIT_SUBMODULE_STRATEGY: recursive
|
||||||
|
|
||||||
# Debian Buster
|
# Debian Buster
|
||||||
py37-django22:
|
# py37-django22:
|
||||||
stage: test
|
# stage: test
|
||||||
image: otthorn/nk20_ci_37
|
# image: debian:buster-backports
|
||||||
script: tox -e py37-django22
|
# before_script:
|
||||||
|
# - >
|
||||||
|
# apt-get update &&
|
||||||
|
# apt-get install --no-install-recommends -t buster-backports -y
|
||||||
|
# python3-django python3-django-crispy-forms
|
||||||
|
# python3-django-extensions python3-django-filters python3-django-polymorphic
|
||||||
|
# python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil
|
||||||
|
# python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache
|
||||||
|
# python3-bs4 python3-setuptools tox texlive-xetex
|
||||||
|
# script: tox -e py37-django22
|
||||||
|
|
||||||
# Ubuntu 20.04
|
# Ubuntu 20.04
|
||||||
py38-django22:
|
py38-django22:
|
||||||
stage: test
|
stage: test
|
||||||
image: otthorn/nk20_ci_38
|
image: ubuntu:20.04
|
||||||
|
before_script:
|
||||||
|
# Fix tzdata prompt
|
||||||
|
- ln -sf /usr/share/zoneinfo/Europe/Paris /etc/localtime && echo Europe/Paris > /etc/timezone
|
||||||
|
- >
|
||||||
|
apt-get update &&
|
||||||
|
apt-get install --no-install-recommends -y
|
||||||
|
python3-django python3-django-crispy-forms
|
||||||
|
python3-django-extensions python3-django-filters python3-django-polymorphic
|
||||||
|
python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil
|
||||||
|
python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache
|
||||||
|
python3-bs4 python3-setuptools tox texlive-xetex
|
||||||
script: tox -e py38-django22
|
script: tox -e py38-django22
|
||||||
|
|
||||||
# Debian Bullseye
|
# Debian Bullseye
|
||||||
py39-django22:
|
py39-django22:
|
||||||
stage: test
|
stage: test
|
||||||
image: otthorn/nk20_ci_39
|
image: debian:bullseye
|
||||||
|
before_script:
|
||||||
|
- >
|
||||||
|
apt-get update &&
|
||||||
|
apt-get install --no-install-recommends -y
|
||||||
|
python3-django python3-django-crispy-forms
|
||||||
|
python3-django-extensions python3-django-filters python3-django-polymorphic
|
||||||
|
python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil
|
||||||
|
python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache
|
||||||
|
python3-bs4 python3-setuptools tox texlive-xetex
|
||||||
script: tox -e py39-django22
|
script: tox -e py39-django22
|
||||||
|
|
||||||
# Tox linter
|
|
||||||
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
|
||||||
@ -36,20 +64,6 @@ linters:
|
|||||||
# Be nice to new contributors, but please use `tox`
|
# Be nice to new contributors, but please use `tox`
|
||||||
allow_failure: true
|
allow_failure: true
|
||||||
|
|
||||||
# Ansible linter
|
|
||||||
ansible-linter:
|
|
||||||
stage: quality-assurance
|
|
||||||
image: otthorn/nk20_ci_ansiblelint
|
|
||||||
script: ansible-lint ansible/
|
|
||||||
|
|
||||||
# Docker linter
|
|
||||||
docker-linter:
|
|
||||||
stage: quality-assurance
|
|
||||||
image: hadolint/hadolint
|
|
||||||
script:
|
|
||||||
- hadolint -c .hadolint Dockerfile
|
|
||||||
- hadolint -c .hadolint docker_ci/Dockerfile.*
|
|
||||||
|
|
||||||
# Compile documentation
|
# Compile documentation
|
||||||
documentation:
|
documentation:
|
||||||
stage: docs
|
stage: docs
|
||||||
|
@ -1,4 +0,0 @@
|
|||||||
ignored:
|
|
||||||
- DL3008 # Do not force to pin version in apt (Debian)
|
|
||||||
- DL3013 # Do not force to pin version in pip (PyPI)
|
|
||||||
- DL3018 # Do not force to pin version in apk (Alpine)
|
|
@ -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
|
||||||
|
|
||||||
@ -279,7 +279,8 @@ Le cahier des charges initial est disponible [sur le Wiki Crans](https://wiki.cr
|
|||||||
La documentation des classes et fonctions est directement dans le code et est explorable à partir de la partie documentation de l'interface d'administration de Django.
|
La documentation des classes et fonctions est directement dans le code et est explorable à partir de la partie documentation de l'interface d'administration de Django.
|
||||||
**Commentez votre code !**
|
**Commentez votre code !**
|
||||||
|
|
||||||
La documentation plus haut niveau sur le développement est disponible sur [le Wiki associé au dépôt Git](https://gitlab.crans.org/bde/nk20/-/wikis/home).
|
La documentation plus haut niveau sur le développement et sur l'utilisation
|
||||||
|
est disponible sur <https://note.crans.org/doc> et également dans le dossier `docs`.
|
||||||
|
|
||||||
## FAQ
|
## FAQ
|
||||||
|
|
||||||
|
@ -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,6 +0,0 @@
|
|||||||
---
|
|
||||||
note:
|
|
||||||
server_name: note-beta.crans.org
|
|
||||||
git_branch: beta
|
|
||||||
cron_enabled: false
|
|
||||||
email: notekfet2020@lists.crans.org
|
|
@ -2,5 +2,6 @@
|
|||||||
note:
|
note:
|
||||||
server_name: note-dev.crans.org
|
server_name: note-dev.crans.org
|
||||||
git_branch: beta
|
git_branch: beta
|
||||||
|
serve_static: false
|
||||||
cron_enabled: false
|
cron_enabled: false
|
||||||
email: notekfet2020@lists.crans.org
|
email: notekfet2020@lists.crans.org
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
note:
|
note:
|
||||||
server_name: note.crans.org
|
server_name: note.crans.org
|
||||||
git_branch: master
|
git_branch: main
|
||||||
|
serve_static: true
|
||||||
cron_enabled: true
|
cron_enabled: true
|
||||||
email: notekfet2020@lists.crans.org
|
email: notekfet2020@lists.crans.org
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
[dev]
|
[dev]
|
||||||
bde-note-dev.adh.crans.org
|
bde-note-dev.adh.crans.org
|
||||||
bde-nk20-beta.adh.crans.org
|
|
||||||
|
|
||||||
[prod]
|
[prod]
|
||||||
bde-note.adh.crans.org
|
bde-note.adh.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
|
||||||
|
@ -41,6 +41,7 @@ server {
|
|||||||
# max upload size
|
# max upload size
|
||||||
client_max_body_size 75M; # adjust to taste
|
client_max_body_size 75M; # adjust to taste
|
||||||
|
|
||||||
|
{% if note.serve_static %}
|
||||||
# Django media
|
# Django media
|
||||||
location /media {
|
location /media {
|
||||||
alias /var/www/note_kfet/media; # your Django project's media files - amend as required
|
alias /var/www/note_kfet/media; # your Django project's media files - amend as required
|
||||||
@ -50,6 +51,7 @@ server {
|
|||||||
alias /var/www/note_kfet/static; # your Django project's static files - amend as required
|
alias /var/www/note_kfet/static; # your Django project's static files - amend as required
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
location /doc {
|
location /doc {
|
||||||
alias /var/www/documentation; # The documentation of the project
|
alias /var/www/documentation; # The documentation of the project
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2020 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-2020 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-2020 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-2020 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
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from api.viewsets import ReadProtectedModelViewSet
|
from api.viewsets import ReadProtectedModelViewSet
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2020 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-2020 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
|
||||||
@ -11,7 +11,7 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from member.models import Club
|
from member.models import Club
|
||||||
from note.models import Note, NoteUser
|
from note.models import Note, NoteUser
|
||||||
from note_kfet.inputs import Autocomplete, DateTimePickerInput
|
from note_kfet.inputs import Autocomplete, DateTimePickerInput
|
||||||
from note_kfet.middlewares import get_current_authenticated_user
|
from note_kfet.middlewares import get_current_request
|
||||||
from permission.backends import PermissionBackend
|
from permission.backends import PermissionBackend
|
||||||
|
|
||||||
from .models import Activity, Guest
|
from .models import Activity, Guest
|
||||||
@ -24,10 +24,16 @@ class ActivityForm(forms.ModelForm):
|
|||||||
self.fields["attendees_club"].initial = Club.objects.get(name="Kfet")
|
self.fields["attendees_club"].initial = Club.objects.get(name="Kfet")
|
||||||
self.fields["attendees_club"].widget.attrs["placeholder"] = "Kfet"
|
self.fields["attendees_club"].widget.attrs["placeholder"] = "Kfet"
|
||||||
clubs = list(Club.objects.filter(PermissionBackend
|
clubs = list(Club.objects.filter(PermissionBackend
|
||||||
.filter_queryset(get_current_authenticated_user(), Club, "view")).all())
|
.filter_queryset(get_current_request(), Club, "view")).all())
|
||||||
shuffle(clubs)
|
shuffle(clubs)
|
||||||
self.fields["organizer"].widget.attrs["placeholder"] = ", ".join(club.name for club in clubs[:4]) + ", ..."
|
self.fields["organizer"].widget.attrs["placeholder"] = ", ".join(club.name for club in clubs[:4]) + ", ..."
|
||||||
|
|
||||||
|
def clean_organizer(self):
|
||||||
|
organizer = self.cleaned_data['organizer']
|
||||||
|
if not organizer.note.is_active:
|
||||||
|
self.add_error('organiser', _('The note of this club is inactive.'))
|
||||||
|
return organizer
|
||||||
|
|
||||||
def clean_date_end(self):
|
def clean_date_end(self):
|
||||||
date_end = self.cleaned_data["date_end"]
|
date_end = self.cleaned_data["date_end"]
|
||||||
date_start = self.cleaned_data["date_start"]
|
date_start = self.cleaned_data["date_start"]
|
||||||
|
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'),
|
||||||
|
),
|
||||||
|
]
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2020 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
|
||||||
@ -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):
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
# Copyright (C) 2018-2020 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 format_html
|
from django.utils.html import escape
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
from django_tables2 import A
|
from django_tables2 import A
|
||||||
@ -52,8 +54,8 @@ class GuestTable(tables.Table):
|
|||||||
def render_entry(self, record):
|
def render_entry(self, record):
|
||||||
if record.has_entry:
|
if record.has_entry:
|
||||||
return str(_("Entered on ") + str(_("{:%Y-%m-%d %H:%M:%S}").format(record.entry.time, )))
|
return str(_("Entered on ") + str(_("{:%Y-%m-%d %H:%M:%S}").format(record.entry.time, )))
|
||||||
return format_html('<button id="{id}" class="btn btn-danger btn-sm" onclick="remove_guest(this.id)"> '
|
return mark_safe('<button id="{id}" class="btn btn-danger btn-sm" onclick="remove_guest(this.id)"> '
|
||||||
'{delete_trans}</button>'.format(id=record.id, delete_trans=_("remove").capitalize()))
|
'{delete_trans}</button>'.format(id=record.id, delete_trans=_("remove").capitalize()))
|
||||||
|
|
||||||
|
|
||||||
def get_row_class(record):
|
def get_row_class(record):
|
||||||
@ -91,7 +93,7 @@ class EntryTable(tables.Table):
|
|||||||
if hasattr(record, 'username'):
|
if hasattr(record, 'username'):
|
||||||
username = record.username
|
username = record.username
|
||||||
if username != value:
|
if username != value:
|
||||||
return format_html(value + " <em>aka.</em> " + username)
|
return mark_safe(escape(value) + " <em>aka.</em> " + escape(username))
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def render_balance(self, value):
|
def render_balance(self, value):
|
||||||
|
@ -63,7 +63,12 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
refreshBalance();
|
refreshBalance();
|
||||||
}
|
}
|
||||||
|
|
||||||
alias_obj.keyup(reloadTable);
|
alias_obj.keyup(function(event) {
|
||||||
|
let code = event.originalEvent.keyCode
|
||||||
|
if (65 <= code <= 122 || code === 13) {
|
||||||
|
debounce(reloadTable)()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
$(document).ready(init);
|
$(document).ready(init);
|
||||||
|
|
||||||
|
@ -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 %}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2020 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-2020 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-2020 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
|
||||||
@ -66,21 +66,20 @@ class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView
|
|||||||
ordering = ('-date_start',)
|
ordering = ('-date_start',)
|
||||||
extra_context = {"title": _("Activities")}
|
extra_context = {"title": _("Activities")}
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self, **kwargs):
|
||||||
return super().get_queryset().distinct()
|
return super().get_queryset(**kwargs).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)
|
||||||
|
|
||||||
upcoming_activities = Activity.objects.filter(date_end__gt=timezone.now())
|
upcoming_activities = Activity.objects.filter(date_end__gt=timezone.now())
|
||||||
context['upcoming'] = ActivityTable(
|
context['upcoming'] = ActivityTable(
|
||||||
data=upcoming_activities.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view")),
|
data=upcoming_activities.filter(PermissionBackend.filter_queryset(self.request, Activity, "view")),
|
||||||
prefix='upcoming-',
|
prefix='upcoming-',
|
||||||
|
order_by='date_start',
|
||||||
)
|
)
|
||||||
|
|
||||||
started_activities = Activity.objects\
|
started_activities = self.get_queryset().filter(open=True, valid=True).distinct().all()
|
||||||
.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view"))\
|
|
||||||
.filter(open=True, valid=True).all()
|
|
||||||
context["started_activities"] = started_activities
|
context["started_activities"] = started_activities
|
||||||
|
|
||||||
return context
|
return context
|
||||||
@ -98,7 +97,7 @@ class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
|||||||
context = super().get_context_data()
|
context = super().get_context_data()
|
||||||
|
|
||||||
table = GuestTable(data=Guest.objects.filter(activity=self.object)
|
table = GuestTable(data=Guest.objects.filter(activity=self.object)
|
||||||
.filter(PermissionBackend.filter_queryset(self.request.user, Guest, "view")))
|
.filter(PermissionBackend.filter_queryset(self.request, Guest, "view")))
|
||||||
context["guests"] = table
|
context["guests"] = table
|
||||||
|
|
||||||
context["activity_started"] = timezone.now() > timezone.localtime(self.object.date_start)
|
context["activity_started"] = timezone.now() > timezone.localtime(self.object.date_start)
|
||||||
@ -144,15 +143,15 @@ class ActivityInviteView(ProtectQuerysetMixin, ProtectedCreateView):
|
|||||||
|
|
||||||
def get_form(self, form_class=None):
|
def get_form(self, form_class=None):
|
||||||
form = super().get_form(form_class)
|
form = super().get_form(form_class)
|
||||||
form.activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view"))\
|
form.activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request, Activity, "view"))\
|
||||||
.get(pk=self.kwargs["pk"])
|
.filter(pk=self.kwargs["pk"]).first()
|
||||||
form.fields["inviter"].initial = self.request.user.note
|
form.fields["inviter"].initial = self.request.user.note
|
||||||
return form
|
return form
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
form.instance.activity = Activity.objects\
|
form.instance.activity = Activity.objects\
|
||||||
.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view")).get(pk=self.kwargs["pk"])
|
.filter(PermissionBackend.filter_queryset(self.request, Activity, "view")).get(pk=self.kwargs["pk"])
|
||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
||||||
def get_success_url(self, **kwargs):
|
def get_success_url(self, **kwargs):
|
||||||
@ -170,10 +169,13 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
|
|||||||
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)
|
||||||
if not PermissionBackend.check_perm(self.request.user, "activity.add_entry", sample_entry):
|
if not PermissionBackend.check_perm(self.request, "activity.add_entry", sample_entry):
|
||||||
raise PermissionDenied(_("You are not allowed to display the entry interface for this activity."))
|
raise PermissionDenied(_("You are not allowed to display the entry interface for this activity."))
|
||||||
|
|
||||||
if not activity.activity_type.manage_entries:
|
if not activity.activity_type.manage_entries:
|
||||||
@ -191,8 +193,8 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
|
|||||||
guest_qs = Guest.objects\
|
guest_qs = Guest.objects\
|
||||||
.annotate(balance=F("inviter__balance"), note_name=F("inviter__user__username"))\
|
.annotate(balance=F("inviter__balance"), note_name=F("inviter__user__username"))\
|
||||||
.filter(activity=activity)\
|
.filter(activity=activity)\
|
||||||
.filter(PermissionBackend.filter_queryset(self.request.user, Guest, "view"))\
|
.filter(PermissionBackend.filter_queryset(self.request, Guest, "view"))\
|
||||||
.order_by('last_name', 'first_name').distinct()
|
.order_by('last_name', 'first_name')
|
||||||
|
|
||||||
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"]
|
||||||
@ -206,7 +208,7 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
guest_qs = guest_qs.none()
|
guest_qs = guest_qs.none()
|
||||||
return guest_qs
|
return guest_qs.distinct()
|
||||||
|
|
||||||
def get_invited_note(self, activity):
|
def get_invited_note(self, activity):
|
||||||
"""
|
"""
|
||||||
@ -230,7 +232,7 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Filter with permission backend
|
# Filter with permission backend
|
||||||
note_qs = note_qs.filter(PermissionBackend.filter_queryset(self.request.user, Alias, "view"))
|
note_qs = note_qs.filter(PermissionBackend.filter_queryset(self.request, Alias, "view"))
|
||||||
|
|
||||||
if "search" in self.request.GET and self.request.GET["search"]:
|
if "search" in self.request.GET and self.request.GET["search"]:
|
||||||
pattern = self.request.GET["search"]
|
pattern = self.request.GET["search"]
|
||||||
@ -256,7 +258,7 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
|
|||||||
"""
|
"""
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request.user, 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
|
context["activity"] = activity
|
||||||
|
|
||||||
@ -281,9 +283,9 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
|
|||||||
context["notespecial_ctype"] = ContentType.objects.get_for_model(NoteSpecial).pk
|
context["notespecial_ctype"] = ContentType.objects.get_for_model(NoteSpecial).pk
|
||||||
|
|
||||||
activities_open = Activity.objects.filter(open=True).filter(
|
activities_open = Activity.objects.filter(open=True).filter(
|
||||||
PermissionBackend.filter_queryset(self.request.user, Activity, "view")).distinct().all()
|
PermissionBackend.filter_queryset(self.request, Activity, "view")).distinct().all()
|
||||||
context["activities_open"] = [a for a in activities_open
|
context["activities_open"] = [a for a in activities_open
|
||||||
if PermissionBackend.check_perm(self.request.user,
|
if PermissionBackend.check_perm(self.request,
|
||||||
"activity.add_entry",
|
"activity.add_entry",
|
||||||
Entry(activity=a, note=self.request.user.note,))]
|
Entry(activity=a, note=self.request.user.note,))]
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2020 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-2020 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
|
||||||
|
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,13 +1,20 @@
|
|||||||
# Copyright (C) 2018-2020 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
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from rest_framework.serializers import ModelSerializer
|
from django.utils import timezone
|
||||||
|
from rest_framework import serializers
|
||||||
|
from member.api.serializers import ProfileSerializer, MembershipSerializer
|
||||||
|
from member.models import Membership
|
||||||
|
from note.api.serializers import NoteSerializer
|
||||||
|
from note.models import Alias
|
||||||
|
from note_kfet.middlewares import get_current_request
|
||||||
|
from permission.backends import PermissionBackend
|
||||||
|
|
||||||
|
|
||||||
class UserSerializer(ModelSerializer):
|
class UserSerializer(serializers.ModelSerializer):
|
||||||
"""
|
"""
|
||||||
REST API Serializer for Users.
|
REST API Serializer for Users.
|
||||||
The djangorestframework plugin will analyse the model `User` and parse all fields in the API.
|
The djangorestframework plugin will analyse the model `User` and parse all fields in the API.
|
||||||
@ -22,7 +29,7 @@ class UserSerializer(ModelSerializer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ContentTypeSerializer(ModelSerializer):
|
class ContentTypeSerializer(serializers.ModelSerializer):
|
||||||
"""
|
"""
|
||||||
REST API Serializer for Users.
|
REST API Serializer for Users.
|
||||||
The djangorestframework plugin will analyse the model `User` and parse all fields in the API.
|
The djangorestframework plugin will analyse the model `User` and parse all fields in the API.
|
||||||
@ -31,3 +38,54 @@ class ContentTypeSerializer(ModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = ContentType
|
model = ContentType
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
|
|
||||||
|
|
||||||
|
class OAuthSerializer(serializers.ModelSerializer):
|
||||||
|
"""
|
||||||
|
Informations that are transmitted by OAuth.
|
||||||
|
For now, this includes user, profile and valid memberships.
|
||||||
|
This should be better managed later.
|
||||||
|
"""
|
||||||
|
normalized_name = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
profile = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
note = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
memberships = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
def get_normalized_name(self, obj):
|
||||||
|
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):
|
||||||
|
# Display only memberships that we are allowed to see.
|
||||||
|
return serializers.ListSerializer(child=MembershipSerializer()).to_representation(
|
||||||
|
obj.memberships.filter(date_start__lte=timezone.now(), date_end__gte=timezone.now())
|
||||||
|
.filter(PermissionBackend.filter_queryset(get_current_request(), Membership, 'view')))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = (
|
||||||
|
'id',
|
||||||
|
'username',
|
||||||
|
'normalized_name',
|
||||||
|
'first_name',
|
||||||
|
'last_name',
|
||||||
|
'email',
|
||||||
|
'is_superuser',
|
||||||
|
'is_active',
|
||||||
|
'is_staff',
|
||||||
|
'profile',
|
||||||
|
'note',
|
||||||
|
'memberships',
|
||||||
|
)
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
# Copyright (C) 2018-2020 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
|
||||||
from datetime import datetime, date
|
from datetime import datetime, date
|
||||||
|
from decimal import Decimal
|
||||||
from urllib.parse import quote_plus
|
from urllib.parse import quote_plus
|
||||||
from warnings import warn
|
from warnings import warn
|
||||||
|
|
||||||
@ -152,6 +153,8 @@ class TestAPI(TestCase):
|
|||||||
value = value.isoformat()
|
value = value.isoformat()
|
||||||
elif isinstance(value, ImageFieldFile):
|
elif isinstance(value, ImageFieldFile):
|
||||||
value = value.name
|
value = value.name
|
||||||
|
elif isinstance(value, Decimal):
|
||||||
|
value = str(value)
|
||||||
query = json.dumps({field.name: value})
|
query = json.dumps({field.name: value})
|
||||||
|
|
||||||
# Create sample permission
|
# Create sample permission
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.conf.urls import url, include
|
from django.conf.urls import url, include
|
||||||
from rest_framework import routers
|
from rest_framework import routers
|
||||||
|
|
||||||
|
from .views import UserInformationView
|
||||||
from .viewsets import ContentTypeViewSet, UserViewSet
|
from .viewsets import ContentTypeViewSet, UserViewSet
|
||||||
|
|
||||||
# Routers provide an easy way of automatically determining the URL conf.
|
# Routers provide an easy way of automatically determining the URL conf.
|
||||||
@ -47,5 +48,6 @@ app_name = 'api'
|
|||||||
# Additionally, we include login URLs for the browsable API.
|
# Additionally, we include login URLs for the browsable API.
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url('^', include(router.urls)),
|
url('^', include(router.urls)),
|
||||||
|
url('^me/', UserInformationView.as_view()),
|
||||||
url('^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
|
url('^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
|
||||||
]
|
]
|
||||||
|
20
apps/api/views.py
Normal file
20
apps/api/views.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from rest_framework.generics import RetrieveAPIView
|
||||||
|
|
||||||
|
from .serializers import OAuthSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class UserInformationView(RetrieveAPIView):
|
||||||
|
"""
|
||||||
|
These fields are give to OAuth authenticators.
|
||||||
|
"""
|
||||||
|
serializer_class = OAuthSerializer
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return User.objects.filter(pk=self.request.user.pk)
|
||||||
|
|
||||||
|
def get_object(self):
|
||||||
|
return self.request.user
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2020 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
|
||||||
@ -9,7 +9,6 @@ from django.contrib.auth.models import User
|
|||||||
from rest_framework.filters import SearchFilter
|
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_kfet.middlewares import get_current_session
|
|
||||||
from note.models import Alias
|
from note.models import Alias
|
||||||
|
|
||||||
from .serializers import UserSerializer, ContentTypeSerializer
|
from .serializers import UserSerializer, ContentTypeSerializer
|
||||||
@ -25,9 +24,7 @@ class ReadProtectedModelViewSet(ModelViewSet):
|
|||||||
self.model = ContentType.objects.get_for_model(self.serializer_class.Meta.model).model_class()
|
self.model = ContentType.objects.get_for_model(self.serializer_class.Meta.model).model_class()
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
user = self.request.user
|
return self.queryset.filter(PermissionBackend.filter_queryset(self.request, self.model, "view")).distinct()
|
||||||
get_current_session().setdefault("permission_mask", 42)
|
|
||||||
return self.queryset.filter(PermissionBackend.filter_queryset(user, self.model, "view")).distinct()
|
|
||||||
|
|
||||||
|
|
||||||
class ReadOnlyProtectedModelViewSet(ReadOnlyModelViewSet):
|
class ReadOnlyProtectedModelViewSet(ReadOnlyModelViewSet):
|
||||||
@ -40,9 +37,7 @@ class ReadOnlyProtectedModelViewSet(ReadOnlyModelViewSet):
|
|||||||
self.model = ContentType.objects.get_for_model(self.serializer_class.Meta.model).model_class()
|
self.model = ContentType.objects.get_for_model(self.serializer_class.Meta.model).model_class()
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
user = self.request.user
|
return self.queryset.filter(PermissionBackend.filter_queryset(self.request, self.model, "view")).distinct()
|
||||||
get_current_session().setdefault("permission_mask", 42)
|
|
||||||
return self.queryset.filter(PermissionBackend.filter_queryset(user, self.model, "view")).distinct()
|
|
||||||
|
|
||||||
|
|
||||||
class UserViewSet(ReadProtectedModelViewSet):
|
class UserViewSet(ReadProtectedModelViewSet):
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2020 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-2020 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-2020 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-2020 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-2020 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-2020 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,11 +1,11 @@
|
|||||||
# Copyright (C) 2018-2020 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
|
||||||
from rest_framework.renderers import JSONRenderer
|
from rest_framework.renderers import JSONRenderer
|
||||||
from rest_framework.serializers import ModelSerializer
|
from rest_framework.serializers import ModelSerializer
|
||||||
from note.models import NoteUser, Alias
|
from note.models import NoteUser, Alias
|
||||||
from note_kfet.middlewares import get_current_authenticated_user, get_current_ip
|
from note_kfet.middlewares import get_current_request
|
||||||
|
|
||||||
from .models import Changelog
|
from .models import Changelog
|
||||||
|
|
||||||
@ -57,9 +57,9 @@ def save_object(sender, instance, **kwargs):
|
|||||||
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 utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP
|
||||||
user, ip = get_current_authenticated_user(), get_current_ip()
|
request = get_current_request()
|
||||||
|
|
||||||
if user 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 dans la VM doit être un des alias note du respo info
|
||||||
@ -71,9 +71,23 @@ def save_object(sender, instance, **kwargs):
|
|||||||
# else:
|
# else:
|
||||||
if note.exists():
|
if note.exists():
|
||||||
user = note.get().user
|
user = note.get().user
|
||||||
|
else:
|
||||||
|
user = None
|
||||||
|
else:
|
||||||
|
user = request.user
|
||||||
|
if 'HTTP_X_REAL_IP' in request.META:
|
||||||
|
ip = request.META.get('HTTP_X_REAL_IP')
|
||||||
|
elif 'HTTP_X_FORWARDED_FOR' in request.META:
|
||||||
|
ip = request.META.get('HTTP_X_FORWARDED_FOR').split(', ')[0]
|
||||||
|
else:
|
||||||
|
ip = request.META.get('REMOTE_ADDR')
|
||||||
|
|
||||||
|
if not user.is_authenticated:
|
||||||
|
# For registration and OAuth2 purposes
|
||||||
|
user = None
|
||||||
|
|
||||||
# noinspection PyProtectedMember
|
# noinspection PyProtectedMember
|
||||||
if user is not None and instance._meta.label_lower == "auth.user" and previous:
|
if request is not None and instance._meta.label_lower == "auth.user" and previous:
|
||||||
# On n'enregistre pas les connexions
|
# On n'enregistre pas les connexions
|
||||||
if instance.last_login != previous.last_login:
|
if instance.last_login != previous.last_login:
|
||||||
return
|
return
|
||||||
@ -121,9 +135,9 @@ def delete_object(sender, instance, **kwargs):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP
|
# Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP
|
||||||
user, ip = get_current_authenticated_user(), get_current_ip()
|
request = get_current_request()
|
||||||
|
|
||||||
if user 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 dans la VM doit être un des alias note du respo info
|
||||||
@ -135,6 +149,20 @@ def delete_object(sender, instance, **kwargs):
|
|||||||
# else:
|
# else:
|
||||||
if note.exists():
|
if note.exists():
|
||||||
user = note.get().user
|
user = note.get().user
|
||||||
|
else:
|
||||||
|
user = None
|
||||||
|
else:
|
||||||
|
user = request.user
|
||||||
|
if 'HTTP_X_REAL_IP' in request.META:
|
||||||
|
ip = request.META.get('HTTP_X_REAL_IP')
|
||||||
|
elif 'HTTP_X_FORWARDED_FOR' in request.META:
|
||||||
|
ip = request.META.get('HTTP_X_FORWARDED_FOR').split(', ')[0]
|
||||||
|
else:
|
||||||
|
ip = request.META.get('REMOTE_ADDR')
|
||||||
|
|
||||||
|
if not user.is_authenticated:
|
||||||
|
# For registration and OAuth2 purposes
|
||||||
|
user = None
|
||||||
|
|
||||||
# On crée notre propre sérialiseur JSON pour pouvoir sauvegarder les modèles
|
# On crée notre propre sérialiseur JSON pour pouvoir sauvegarder les modèles
|
||||||
class CustomSerializer(ModelSerializer):
|
class CustomSerializer(ModelSerializer):
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2020 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-2020 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-2020 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-2020 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,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2020 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-2020 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
|
||||||
|
17
apps/member/auth.py
Normal file
17
apps/member/auth.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from cas_server.auth import DjangoAuthUser # pragma: no cover
|
||||||
|
from note.models import Alias
|
||||||
|
|
||||||
|
|
||||||
|
class CustomAuthUser(DjangoAuthUser): # pragma: no cover
|
||||||
|
"""
|
||||||
|
Override Django Auth User model to define a custom Matrix username.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def attributs(self):
|
||||||
|
d = super().attributs()
|
||||||
|
if self.user:
|
||||||
|
d["normalized_name"] = Alias.normalize(self.user.username)
|
||||||
|
return d
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2020 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(),
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
# Copyright (C) 2018-2020 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
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.hashers import PBKDF2PasswordHasher
|
from django.contrib.auth.hashers import PBKDF2PasswordHasher, mask_hash
|
||||||
from django.utils.crypto import constant_time_compare
|
from django.utils.crypto import constant_time_compare
|
||||||
from note_kfet.middlewares import get_current_authenticated_user, get_current_session
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from note_kfet.middlewares import get_current_request
|
||||||
|
|
||||||
|
|
||||||
class CustomNK15Hasher(PBKDF2PasswordHasher):
|
class CustomNK15Hasher(PBKDF2PasswordHasher):
|
||||||
@ -24,16 +26,22 @@ class CustomNK15Hasher(PBKDF2PasswordHasher):
|
|||||||
|
|
||||||
def must_update(self, encoded):
|
def must_update(self, encoded):
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
current_user = get_current_authenticated_user()
|
# Small hack to let superusers to impersonate people.
|
||||||
|
# Don't change their password.
|
||||||
|
request = get_current_request()
|
||||||
|
current_user = request.user
|
||||||
if current_user is not None and current_user.is_superuser:
|
if current_user is not None and current_user.is_superuser:
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def verify(self, password, encoded):
|
def verify(self, password, encoded):
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
current_user = get_current_authenticated_user()
|
# Small hack to let superusers to impersonate people.
|
||||||
|
# If a superuser is already connected, let him/her log in as another person.
|
||||||
|
request = get_current_request()
|
||||||
|
current_user = request.user
|
||||||
if current_user is not None and current_user.is_superuser\
|
if current_user is not None and current_user.is_superuser\
|
||||||
and get_current_session().get("permission_mask", -1) >= 42:
|
and request.session.get("permission_mask", -1) >= 42:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if '|' in encoded:
|
if '|' in encoded:
|
||||||
@ -41,6 +49,18 @@ class CustomNK15Hasher(PBKDF2PasswordHasher):
|
|||||||
return constant_time_compare(hashlib.sha256((salt + password).encode("utf-8")).hexdigest(), db_hashed_pass)
|
return constant_time_compare(hashlib.sha256((salt + password).encode("utf-8")).hexdigest(), db_hashed_pass)
|
||||||
return super().verify(password, encoded)
|
return super().verify(password, encoded)
|
||||||
|
|
||||||
|
def safe_summary(self, encoded):
|
||||||
|
# Displayed information in Django Admin.
|
||||||
|
if '|' in encoded:
|
||||||
|
salt, db_hashed_pass = encoded.split('$')[2].split('|')
|
||||||
|
return OrderedDict([
|
||||||
|
(_('algorithm'), 'custom_nk15'),
|
||||||
|
(_('iterations'), '1'),
|
||||||
|
(_('salt'), mask_hash(salt)),
|
||||||
|
(_('hash'), mask_hash(db_hashed_pass)),
|
||||||
|
])
|
||||||
|
return super().safe_summary(encoded)
|
||||||
|
|
||||||
|
|
||||||
class DebugSuperuserBackdoor(PBKDF2PasswordHasher):
|
class DebugSuperuserBackdoor(PBKDF2PasswordHasher):
|
||||||
"""
|
"""
|
||||||
@ -51,8 +71,11 @@ class DebugSuperuserBackdoor(PBKDF2PasswordHasher):
|
|||||||
|
|
||||||
def verify(self, password, encoded):
|
def verify(self, password, encoded):
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
current_user = get_current_authenticated_user()
|
# Small hack to let superusers to impersonate people.
|
||||||
|
# If a superuser is already connected, let him/her log in as another person.
|
||||||
|
request = get_current_request()
|
||||||
|
current_user = request.user
|
||||||
if current_user is not None and current_user.is_superuser\
|
if current_user is not None and current_user.is_superuser\
|
||||||
and get_current_session().get("permission_mask", -1) >= 42:
|
and request.session.get("permission_mask", -1) >= 42:
|
||||||
return True
|
return True
|
||||||
return super().verify(password, encoded)
|
return super().verify(password, encoded)
|
||||||
|
@ -19,8 +19,8 @@ def create_bde_and_kfet(apps, schema_editor):
|
|||||||
membership_fee_paid=500,
|
membership_fee_paid=500,
|
||||||
membership_fee_unpaid=500,
|
membership_fee_unpaid=500,
|
||||||
membership_duration=396,
|
membership_duration=396,
|
||||||
membership_start="2020-08-01",
|
membership_start="2021-08-01",
|
||||||
membership_end="2021-09-30",
|
membership_end="2022-09-30",
|
||||||
)
|
)
|
||||||
Club.objects.get_or_create(
|
Club.objects.get_or_create(
|
||||||
id=2,
|
id=2,
|
||||||
@ -31,8 +31,8 @@ def create_bde_and_kfet(apps, schema_editor):
|
|||||||
membership_fee_paid=3500,
|
membership_fee_paid=3500,
|
||||||
membership_fee_unpaid=3500,
|
membership_fee_unpaid=3500,
|
||||||
membership_duration=396,
|
membership_duration=396,
|
||||||
membership_start="2020-08-01",
|
membership_start="2021-08-01",
|
||||||
membership_end="2021-09-30",
|
membership_end="2022-09-30",
|
||||||
)
|
)
|
||||||
|
|
||||||
NoteClub.objects.get_or_create(
|
NoteClub.objects.get_or_create(
|
||||||
|
23
apps/member/migrations/0007_auto_20210313_1235.py
Normal file
23
apps/member/migrations/0007_auto_20210313_1235.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 2.2.19 on 2021-03-13 11:35
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('member', '0006_create_note_account_bde_membership'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='membership',
|
||||||
|
name='roles',
|
||||||
|
field=models.ManyToManyField(related_name='memberships', to='permission.Role', verbose_name='roles'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='profile',
|
||||||
|
name='promotion',
|
||||||
|
field=models.PositiveSmallIntegerField(default=2021, help_text='Year of entry to the school (None if not ENS student)', null=True, verbose_name='promotion'),
|
||||||
|
),
|
||||||
|
]
|
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'),
|
||||||
|
),
|
||||||
|
]
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2020 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,
|
||||||
@ -57,7 +56,7 @@ class Profile(models.Model):
|
|||||||
('A1', _("Mathematics (A1)")),
|
('A1', _("Mathematics (A1)")),
|
||||||
('A2', _("Physics (A2)")),
|
('A2', _("Physics (A2)")),
|
||||||
("A'2", _("Applied physics (A'2)")),
|
("A'2", _("Applied physics (A'2)")),
|
||||||
('A''2', _("Chemistry (A''2)")),
|
("A''2", _("Chemistry (A''2)")),
|
||||||
('A3', _("Biology (A3)")),
|
('A3', _("Biology (A3)")),
|
||||||
('B1234', _("SAPHIRE (B1234)")),
|
('B1234', _("SAPHIRE (B1234)")),
|
||||||
('B1', _("Mechanics (B1)")),
|
('B1', _("Mechanics (B1)")),
|
||||||
@ -74,7 +73,7 @@ class Profile(models.Model):
|
|||||||
|
|
||||||
promotion = models.PositiveSmallIntegerField(
|
promotion = models.PositiveSmallIntegerField(
|
||||||
null=True,
|
null=True,
|
||||||
default=datetime.date.today().year,
|
default=datetime.date.today().year if datetime.date.today().month >= 8 else datetime.date.today().year - 1,
|
||||||
verbose_name=_("promotion"),
|
verbose_name=_("promotion"),
|
||||||
help_text=_("Year of entry to the school (None if not ENS student)"),
|
help_text=_("Year of entry to the school (None if not ENS student)"),
|
||||||
)
|
)
|
||||||
@ -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,29 @@ 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()
|
||||||
|
|
||||||
|
while (today - self.membership_start).days >= 365:
|
||||||
|
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 +344,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):
|
||||||
"""
|
"""
|
||||||
@ -408,52 +481,6 @@ class Membership(models.Model):
|
|||||||
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.
|
|
||||||
"""
|
|
||||||
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.
|
||||||
@ -491,11 +518,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-2020 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-2020 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
|
||||||
@ -9,7 +9,7 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
from note.templatetags.pretty_money import pretty_money
|
from note.templatetags.pretty_money import pretty_money
|
||||||
from note_kfet.middlewares import get_current_authenticated_user
|
from note_kfet.middlewares import get_current_request
|
||||||
from permission.backends import PermissionBackend
|
from permission.backends import PermissionBackend
|
||||||
|
|
||||||
from .models import Club, Membership
|
from .models import Club, Membership
|
||||||
@ -31,7 +31,8 @@ class ClubTable(tables.Table):
|
|||||||
row_attrs = {
|
row_attrs = {
|
||||||
'class': 'table-row',
|
'class': 'table-row',
|
||||||
'id': lambda record: "row-" + str(record.pk),
|
'id': lambda record: "row-" + str(record.pk),
|
||||||
'data-href': lambda record: record.pk
|
'data-href': lambda record: record.pk,
|
||||||
|
'style': 'cursor:pointer',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -51,19 +52,19 @@ class UserTable(tables.Table):
|
|||||||
def render_email(self, record, value):
|
def render_email(self, record, value):
|
||||||
# Replace the email by a dash if the user can't see the profile detail
|
# Replace the email by a dash if the user can't see the profile detail
|
||||||
# Replace also the URL
|
# Replace also the URL
|
||||||
if not PermissionBackend.check_perm(get_current_authenticated_user(), "member.view_profile", record.profile):
|
if not PermissionBackend.check_perm(get_current_request(), "member.view_profile", record.profile):
|
||||||
value = "—"
|
value = "—"
|
||||||
record.email = value
|
record.email = value
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def render_section(self, record, value):
|
def render_section(self, record, value):
|
||||||
return value \
|
return value \
|
||||||
if PermissionBackend.check_perm(get_current_authenticated_user(), "member.view_profile", record.profile) \
|
if PermissionBackend.check_perm(get_current_request(), "member.view_profile", record.profile) \
|
||||||
else "—"
|
else "—"
|
||||||
|
|
||||||
def render_balance(self, record, value):
|
def render_balance(self, record, value):
|
||||||
return pretty_money(value)\
|
return pretty_money(value)\
|
||||||
if PermissionBackend.check_perm(get_current_authenticated_user(), "note.view_note", record.note) else "—"
|
if PermissionBackend.check_perm(get_current_request(), "note.view_note", record.note) else "—"
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
attrs = {
|
attrs = {
|
||||||
@ -74,7 +75,8 @@ class UserTable(tables.Table):
|
|||||||
model = User
|
model = User
|
||||||
row_attrs = {
|
row_attrs = {
|
||||||
'class': 'table-row',
|
'class': 'table-row',
|
||||||
'data-href': lambda record: record.pk
|
'data-href': lambda record: record.pk,
|
||||||
|
'style': 'cursor:pointer',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -93,7 +95,7 @@ class MembershipTable(tables.Table):
|
|||||||
def render_user(self, value):
|
def render_user(self, value):
|
||||||
# If the user has the right, link the displayed user with the page of its detail.
|
# If the user has the right, link the displayed user with the page of its detail.
|
||||||
s = value.username
|
s = value.username
|
||||||
if PermissionBackend.check_perm(get_current_authenticated_user(), "auth.view_user", value):
|
if PermissionBackend.check_perm(get_current_request(), "auth.view_user", value):
|
||||||
s = format_html("<a href={url}>{name}</a>",
|
s = format_html("<a href={url}>{name}</a>",
|
||||||
url=reverse_lazy('member:user_detail', kwargs={"pk": value.pk}), name=s)
|
url=reverse_lazy('member:user_detail', kwargs={"pk": value.pk}), name=s)
|
||||||
|
|
||||||
@ -102,7 +104,7 @@ class MembershipTable(tables.Table):
|
|||||||
def render_club(self, value):
|
def render_club(self, value):
|
||||||
# If the user has the right, link the displayed club with the page of its detail.
|
# If the user has the right, link the displayed club with the page of its detail.
|
||||||
s = value.name
|
s = value.name
|
||||||
if PermissionBackend.check_perm(get_current_authenticated_user(), "member.view_club", value):
|
if PermissionBackend.check_perm(get_current_request(), "member.view_club", value):
|
||||||
s = format_html("<a href={url}>{name}</a>",
|
s = format_html("<a href={url}>{name}</a>",
|
||||||
url=reverse_lazy('member:club_detail', kwargs={"pk": value.pk}), name=s)
|
url=reverse_lazy('member:club_detail', kwargs={"pk": value.pk}), name=s)
|
||||||
|
|
||||||
@ -118,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,
|
||||||
@ -127,7 +129,7 @@ class MembershipTable(tables.Table):
|
|||||||
date_end=date.today(),
|
date_end=date.today(),
|
||||||
fee=0,
|
fee=0,
|
||||||
)
|
)
|
||||||
if PermissionBackend.check_perm(get_current_authenticated_user(),
|
if PermissionBackend.check_perm(get_current_request(),
|
||||||
"member.add_membership", empty_membership): # If the user has right
|
"member.add_membership", empty_membership): # If the user has right
|
||||||
renew_url = reverse_lazy('member:club_renew_membership',
|
renew_url = reverse_lazy('member:club_renew_membership',
|
||||||
kwargs={"pk": record.pk})
|
kwargs={"pk": record.pk})
|
||||||
@ -142,7 +144,7 @@ class MembershipTable(tables.Table):
|
|||||||
# If the user has the right to manage the roles, display the link to manage them
|
# If the user has the right to manage the roles, display the link to manage them
|
||||||
roles = record.roles.all()
|
roles = record.roles.all()
|
||||||
s = ", ".join(str(role) for role in roles)
|
s = ", ".join(str(role) for role in roles)
|
||||||
if PermissionBackend.check_perm(get_current_authenticated_user(), "member.change_membership_roles", record):
|
if PermissionBackend.check_perm(get_current_request(), "member.change_membership_roles", record):
|
||||||
s = format_html("<a href='" + str(reverse_lazy("member:club_manage_roles", kwargs={"pk": record.pk}))
|
s = format_html("<a href='" + str(reverse_lazy("member:club_manage_roles", kwargs={"pk": record.pk}))
|
||||||
+ "'>" + s + "</a>")
|
+ "'>" + s + "</a>")
|
||||||
return s
|
return s
|
||||||
@ -165,7 +167,7 @@ class ClubManagerTable(tables.Table):
|
|||||||
def render_user(self, value):
|
def render_user(self, value):
|
||||||
# If the user has the right, link the displayed user with the page of its detail.
|
# If the user has the right, link the displayed user with the page of its detail.
|
||||||
s = value.username
|
s = value.username
|
||||||
if PermissionBackend.check_perm(get_current_authenticated_user(), "auth.view_user", value):
|
if PermissionBackend.check_perm(get_current_request(), "auth.view_user", value):
|
||||||
s = format_html("<a href={url}>{name}</a>",
|
s = format_html("<a href={url}>{name}</a>",
|
||||||
url=reverse_lazy('member:user_detail', kwargs={"pk": value.pk}), name=s)
|
url=reverse_lazy('member:user_detail', kwargs={"pk": value.pk}), name=s)
|
||||||
|
|
||||||
|
@ -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>
|
||||||
@ -39,13 +47,13 @@
|
|||||||
<dt class="col-xl-6">{% trans 'address'|capfirst %}</dt>
|
<dt class="col-xl-6">{% trans 'address'|capfirst %}</dt>
|
||||||
<dd class="col-xl-6">{{ user_object.profile.address }}</dd>
|
<dd class="col-xl-6">{{ user_object.profile.address }}</dd>
|
||||||
|
|
||||||
{% if user_object.note and "note.view_note"|has_perm:user_object.note %}
|
|
||||||
<dt class="col-xl-6">{% trans 'balance'|capfirst %}</dt>
|
|
||||||
<dd class="col-xl-6">{{ user_object.note.balance | pretty_money }}</dd>
|
|
||||||
|
|
||||||
<dt class="col-xl-6">{% trans 'paid'|capfirst %}</dt>
|
<dt class="col-xl-6">{% trans 'paid'|capfirst %}</dt>
|
||||||
<dd class="col-xl-6">{{ user_object.profile.paid|yesno }}</dd>
|
<dd class="col-xl-6">{{ user_object.profile.paid|yesno }}</dd>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if user_object.note and "note.view_note"|has_perm:user_object.note %}
|
||||||
|
<dt class="col-xl-6">{% trans 'balance'|capfirst %}</dt>
|
||||||
|
<dd class="col-xl-6">{{ user_object.note.balance | pretty_money }}</dd>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
|
@ -5,32 +5,98 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="alert alert-info">
|
<div class="row mt-4">
|
||||||
<h4>À quoi sert un jeton d'authentification ?</h4>
|
<div class="col-xl-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header text-center">
|
||||||
|
<h3>{% trans "Token authentication" %}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<h4>À quoi sert un jeton d'authentification ?</h4>
|
||||||
|
|
||||||
Un jeton vous permet de vous connecter à <a href="/api/">l'API de la Note Kfet</a>.<br />
|
Un jeton vous permet de vous connecter à <a href="/api/">l'API de la Note Kfet</a> via votre propre compte
|
||||||
Il suffit pour cela d'ajouter en en-tête de vos requêtes <code>Authorization: Token <TOKEN></code>
|
depuis un client externe.<br />
|
||||||
pour pouvoir vous identifier.<br /><br />
|
Il suffit pour cela d'ajouter en en-tête de vos requêtes <code>Authorization: Token <TOKEN></code>
|
||||||
|
pour pouvoir vous identifier.<br /><br />
|
||||||
|
|
||||||
Une documentation de l'API arrivera ultérieurement.
|
La documentation de l'API est disponible ici :
|
||||||
|
<a href="/doc/api/">{{ request.scheme }}://{{ request.get_host }}/doc/api/</a>.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<strong>{%trans 'Token' %} :</strong>
|
||||||
|
{% if 'show' in request.GET %}
|
||||||
|
{{ token.key }} (<a href="?">cacher</a>)
|
||||||
|
{% else %}
|
||||||
|
<em>caché</em> (<a href="?show">montrer</a>)
|
||||||
|
{% endif %}
|
||||||
|
<br />
|
||||||
|
<strong>{%trans 'Created' %} :</strong> {{ token.created }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<strong>{% trans "Warning" %} :</strong> regénérer le jeton va révoquer tout accès autorisé à l'API via ce jeton !
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer text-center">
|
||||||
|
<a href="?regenerate">
|
||||||
|
<button class="btn btn-primary">{% trans 'Regenerate token' %}</button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-xl-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header text-center">
|
||||||
|
<h3>{% trans "OAuth2 authentication" %}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<p>
|
||||||
|
La Note Kfet implémente également le protocole <a href="https://oauth.net/2/">OAuth2</a>, afin de
|
||||||
|
permettre à des applications tierces d'interagir avec la Note en récoltant des informations
|
||||||
|
(de connexion par exemple) voir en permettant des modifications à distance, par exemple lorsqu'il
|
||||||
|
s'agit d'avoir un site marchand sur lequel faire des transactions via la Note Kfet.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
L'usage de ce protocole est recommandé pour tout usage non personnel, car permet de mieux cibler
|
||||||
|
les droits dont on a besoin, en restreignant leur usage par jeton généré.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
La documentation vis-à-vis de l'usage de ce protocole est disponible ici :
|
||||||
|
<a href="/doc/external_services/oauth2/">{{ request.scheme }}://{{ request.get_host }}/doc/external_services/oauth2/</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
Liste des URL à communiquer à votre application :
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
{% trans "Authorization:" %}
|
||||||
|
<a href="{% url 'oauth2_provider:authorize' %}">{{ request.scheme }}://{{ request.get_host }}{% url 'oauth2_provider:authorize' %}</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{% trans "Token:" %}
|
||||||
|
<a href="{% url 'oauth2_provider:authorize' %}">{{ request.scheme }}://{{ request.get_host }}{% url 'oauth2_provider:token' %}</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{% trans "Revoke Token:" %}
|
||||||
|
<a href="{% url 'oauth2_provider:authorize' %}">{{ request.scheme }}://{{ request.get_host }}{% url 'oauth2_provider:revoke-token' %}</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{% trans "Introspect Token:" %}
|
||||||
|
<a href="{% url 'oauth2_provider:authorize' %}">{{ request.scheme }}://{{ request.get_host }}{% url 'oauth2_provider:introspect' %}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer text-center">
|
||||||
|
<a class="btn btn-primary" href="{% url 'oauth2_provider:list' %}">{% trans "Show my applications" %}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="alert alert-info">
|
|
||||||
<strong>{%trans 'Token' %} :</strong>
|
|
||||||
{% if 'show' in request.GET %}
|
|
||||||
{{ token.key }} (<a href="?">cacher</a>)
|
|
||||||
{% else %}
|
|
||||||
<em>caché</em> (<a href="?show">montrer</a>)
|
|
||||||
{% endif %}
|
|
||||||
<br />
|
|
||||||
<strong>{%trans 'Created' %} :</strong> {{ token.created }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="alert alert-warning">
|
|
||||||
<strong>Attention :</strong> regénérer le jeton va révoquer tout accès autorisé à l'API via ce jeton !
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<a href="?regenerate">
|
|
||||||
<button class="btn btn-primary">{% trans 'Regenerate token' %}</button>
|
|
||||||
</a>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -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-2020 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
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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
|
||||||
@ -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,
|
||||||
@ -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-2020 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-2020 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
|
||||||
@ -18,15 +18,15 @@ from django.views.generic import DetailView, UpdateView, TemplateView
|
|||||||
from django.views.generic.edit import FormMixin
|
from django.views.generic.edit import FormMixin
|
||||||
from django_tables2.views import SingleTableView
|
from django_tables2.views import SingleTableView
|
||||||
from rest_framework.authtoken.models import Token
|
from rest_framework.authtoken.models import Token
|
||||||
from note.models import Alias, NoteUser
|
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_user_and_ip
|
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
|
||||||
@ -41,7 +41,8 @@ class CustomLoginView(LoginView):
|
|||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
logout(self.request)
|
logout(self.request)
|
||||||
_set_current_user_and_ip(form.get_user(), self.request.session, None)
|
self.request.user = form.get_user()
|
||||||
|
_set_current_request(self.request)
|
||||||
self.request.session['permission_mask'] = form.cleaned_data['permission_mask'].rank
|
self.request.session['permission_mask'] = form.cleaned_data['permission_mask'].rank
|
||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
||||||
@ -70,7 +71,7 @@ class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
|||||||
form.fields['email'].required = True
|
form.fields['email'].required = True
|
||||||
form.fields['email'].help_text = _("This address must be valid.")
|
form.fields['email'].help_text = _("This address must be valid.")
|
||||||
|
|
||||||
if PermissionBackend.check_perm(self.request.user, "member.change_profile", context['user_object'].profile):
|
if PermissionBackend.check_perm(self.request, "member.change_profile", context['user_object'].profile):
|
||||||
context['profile_form'] = self.profile_form(instance=context['user_object'].profile,
|
context['profile_form'] = self.profile_form(instance=context['user_object'].profile,
|
||||||
data=self.request.POST if self.request.POST else None)
|
data=self.request.POST if self.request.POST else None)
|
||||||
if not self.object.profile.report_frequency:
|
if not self.object.profile.report_frequency:
|
||||||
@ -153,13 +154,13 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
|||||||
history_list = \
|
history_list = \
|
||||||
Transaction.objects.all().filter(Q(source=user.note) | Q(destination=user.note))\
|
Transaction.objects.all().filter(Q(source=user.note) | Q(destination=user.note))\
|
||||||
.order_by("-created_at")\
|
.order_by("-created_at")\
|
||||||
.filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view"))
|
.filter(PermissionBackend.filter_queryset(self.request, Transaction, "view"))
|
||||||
history_table = HistoryTable(history_list, prefix='transaction-')
|
history_table = HistoryTable(history_list, prefix='transaction-')
|
||||||
history_table.paginate(per_page=20, page=self.request.GET.get("transaction-page", 1))
|
history_table.paginate(per_page=20, page=self.request.GET.get("transaction-page", 1))
|
||||||
context['history_list'] = history_table
|
context['history_list'] = history_table
|
||||||
|
|
||||||
club_list = Membership.objects.filter(user=user, date_end__gte=date.today() - timedelta(days=15))\
|
club_list = Membership.objects.filter(user=user, date_end__gte=date.today() - timedelta(days=15))\
|
||||||
.filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view"))\
|
.filter(PermissionBackend.filter_queryset(self.request, Membership, "view"))\
|
||||||
.order_by("club__name", "-date_start")
|
.order_by("club__name", "-date_start")
|
||||||
# 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")\
|
||||||
@ -173,24 +174,23 @@ 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.user, "note.change_noteuser_is_active",
|
.check_perm(self.request, "note.change_noteuser_is_active", modified_note)
|
||||||
modified_note)
|
|
||||||
old_note = NoteUser.objects.select_for_update().get(pk=user.note.pk)
|
old_note = NoteUser.objects.select_for_update().get(pk=user.note.pk)
|
||||||
modified_note.inactivity_reason = 'forced'
|
modified_note.inactivity_reason = 'forced'
|
||||||
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.user, "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.user, "note.change_note_is_active", modified_note)
|
.check_perm(self.request, "note.change_noteuser_is_active", modified_note)
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
@ -237,12 +237,46 @@ class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
|||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
pre_registered_users = User.objects.filter(PermissionBackend.filter_queryset(self.request.user, User, "view"))\
|
pre_registered_users = User.objects.filter(PermissionBackend.filter_queryset(self.request, User, "view"))\
|
||||||
.filter(profile__registration_valid=False)
|
.filter(profile__registration_valid=False)
|
||||||
context["can_manage_registrations"] = pre_registered_users.exists()
|
context["can_manage_registrations"] = pre_registered_users.exists()
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class ProfileTrustView(ProtectQuerysetMixin, LoginRequiredMixin, 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")}
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
note = context['object'].note
|
||||||
|
context["trusting"] = TrustTable(
|
||||||
|
note.trusting.filter(PermissionBackend.filter_queryset(self.request, Trust, "view")).distinct().all())
|
||||||
|
context["trusted_by"] = TrustedTable(
|
||||||
|
note.trusted.filter(PermissionBackend.filter_queryset(self.request, Trust, "view")).distinct().all())
|
||||||
|
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, DetailView):
|
class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||||
"""
|
"""
|
||||||
View and manage user aliases.
|
View and manage user aliases.
|
||||||
@ -256,8 +290,9 @@ class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
|||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
note = context['object'].note
|
note = context['object'].note
|
||||||
context["aliases"] = AliasTable(
|
context["aliases"] = AliasTable(
|
||||||
note.alias.filter(PermissionBackend.filter_queryset(self.request.user, Alias, "view")).distinct().all())
|
note.alias.filter(PermissionBackend.filter_queryset(self.request, Alias, "view")).distinct()
|
||||||
context["can_create"] = PermissionBackend.check_perm(self.request.user, "note.add_alias", Alias(
|
.order_by('normalized_name').all())
|
||||||
|
context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_alias", Alias(
|
||||||
note=context["object"].note,
|
note=context["object"].note,
|
||||||
name="",
|
name="",
|
||||||
normalized_name="",
|
normalized_name="",
|
||||||
@ -291,12 +326,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
|
||||||
@ -382,7 +420,7 @@ class ClubListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
|||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context["can_add_club"] = PermissionBackend.check_perm(self.request.user, "member.add_club", Club(
|
context["can_add_club"] = PermissionBackend.check_perm(self.request, "member.add_club", Club(
|
||||||
name="",
|
name="",
|
||||||
email="club@example.com",
|
email="club@example.com",
|
||||||
))
|
))
|
||||||
@ -403,9 +441,12 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
|||||||
"""
|
"""
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
club = context["club"]
|
club = self.object
|
||||||
if PermissionBackend.check_perm(self.request.user, "member.change_club_membership_start", club):
|
context["note"] = club.note
|
||||||
|
|
||||||
|
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())\
|
||||||
@ -413,7 +454,7 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
|||||||
context["managers"] = ClubManagerTable(data=managers, prefix="managers-")
|
context["managers"] = ClubManagerTable(data=managers, prefix="managers-")
|
||||||
# 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.user, Transaction, "view"))\
|
.filter(PermissionBackend.filter_queryset(self.request, Transaction, "view"))\
|
||||||
.order_by('-created_at')
|
.order_by('-created_at')
|
||||||
history_table = HistoryTable(club_transactions, prefix="history-")
|
history_table = HistoryTable(club_transactions, prefix="history-")
|
||||||
history_table.paginate(per_page=20, page=self.request.GET.get('history-page', 1))
|
history_table.paginate(per_page=20, page=self.request.GET.get('history-page', 1))
|
||||||
@ -422,7 +463,7 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
|||||||
club_member = Membership.objects.filter(
|
club_member = Membership.objects.filter(
|
||||||
club=club,
|
club=club,
|
||||||
date_end__gte=date.today() - timedelta(days=15),
|
date_end__gte=date.today() - timedelta(days=15),
|
||||||
).filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view"))\
|
).filter(PermissionBackend.filter_queryset(self.request, Membership, "view"))\
|
||||||
.order_by("user__username", "-date_start")
|
.order_by("user__username", "-date_start")
|
||||||
# Display only the most recent membership
|
# Display only the most recent membership
|
||||||
club_member = club_member.distinct("user__username")\
|
club_member = club_member.distinct("user__username")\
|
||||||
@ -443,6 +484,29 @@ 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
|
||||||
|
|
||||||
|
|
||||||
@ -459,8 +523,8 @@ class ClubAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
|||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
note = context['object'].note
|
note = context['object'].note
|
||||||
context["aliases"] = AliasTable(note.alias.filter(
|
context["aliases"] = AliasTable(note.alias.filter(
|
||||||
PermissionBackend.filter_queryset(self.request.user, Alias, "view")).distinct().all())
|
PermissionBackend.filter_queryset(self.request, Alias, "view")).distinct().all())
|
||||||
context["can_create"] = PermissionBackend.check_perm(self.request.user, "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="",
|
||||||
normalized_name="",
|
normalized_name="",
|
||||||
@ -535,7 +599,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
|
|||||||
form = context['form']
|
form = context['form']
|
||||||
|
|
||||||
if "club_pk" in self.kwargs: # We create a new membership.
|
if "club_pk" in self.kwargs: # We create a new membership.
|
||||||
club = Club.objects.filter(PermissionBackend.filter_queryset(self.request.user, Club, "view"))\
|
club = Club.objects.filter(PermissionBackend.filter_queryset(self.request, Club, "view"))\
|
||||||
.get(pk=self.kwargs["club_pk"], weiclub=None)
|
.get(pk=self.kwargs["club_pk"], weiclub=None)
|
||||||
form.fields['credit_amount'].initial = club.membership_fee_paid
|
form.fields['credit_amount'].initial = club.membership_fee_paid
|
||||||
# Ensure that the user is member of the parent club and all its the family tree.
|
# Ensure that the user is member of the parent club and all its the family tree.
|
||||||
@ -625,9 +689,6 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
|
|||||||
# Retrieve form data
|
# Retrieve form data
|
||||||
credit_type = form.cleaned_data["credit_type"]
|
credit_type = form.cleaned_data["credit_type"]
|
||||||
credit_amount = form.cleaned_data["credit_amount"]
|
credit_amount = form.cleaned_data["credit_amount"]
|
||||||
last_name = form.cleaned_data["last_name"]
|
|
||||||
first_name = form.cleaned_data["first_name"]
|
|
||||||
bank = form.cleaned_data["bank"]
|
|
||||||
soge = form.cleaned_data["soge"] and not user.profile.soge and (club.name == "BDE" or club.name == "Kfet")
|
soge = form.cleaned_data["soge"] and not user.profile.soge and (club.name == "BDE" or club.name == "Kfet")
|
||||||
|
|
||||||
if not credit_type:
|
if not credit_type:
|
||||||
@ -658,8 +719,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
|
|||||||
if club.name != "Kfet" and club.parent_club and not Membership.objects.filter(
|
if club.name != "Kfet" and club.parent_club and not Membership.objects.filter(
|
||||||
user=form.instance.user,
|
user=form.instance.user,
|
||||||
club=club.parent_club,
|
club=club.parent_club,
|
||||||
date_start__lte=timezone.now(),
|
date_start__gte=club.parent_club.membership_start,
|
||||||
date_end__gte=club.parent_club.membership_end,
|
|
||||||
).exists():
|
).exists():
|
||||||
form.add_error('user', _('User is not a member of the parent club') + ' ' + club.parent_club.name)
|
form.add_error('user', _('User is not a member of the parent club') + ' ' + club.parent_club.name)
|
||||||
error = True
|
error = True
|
||||||
@ -674,17 +734,9 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
|
|||||||
.format(form.instance.club.membership_end))
|
.format(form.instance.club.membership_end))
|
||||||
error = True
|
error = True
|
||||||
|
|
||||||
if credit_amount:
|
if credit_amount and not SpecialTransaction.validate_payment_form(form):
|
||||||
if not last_name or not first_name or (not bank and credit_type.special_type == "Chèque"):
|
# Check that special information for payment are filled
|
||||||
if not last_name:
|
error = True
|
||||||
form.add_error('last_name', _("This field is required."))
|
|
||||||
error = True
|
|
||||||
if not first_name:
|
|
||||||
form.add_error('first_name', _("This field is required."))
|
|
||||||
error = True
|
|
||||||
if not bank and credit_type.special_type == "Chèque":
|
|
||||||
form.add_error('bank', _("This field is required."))
|
|
||||||
error = True
|
|
||||||
|
|
||||||
return not error
|
return not error
|
||||||
|
|
||||||
@ -695,7 +747,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
|
|||||||
"""
|
"""
|
||||||
# Get the club that is concerned by the membership
|
# Get the club that is concerned by the membership
|
||||||
if "club_pk" in self.kwargs: # get from url of new membership
|
if "club_pk" in self.kwargs: # get from url of new membership
|
||||||
club = Club.objects.filter(PermissionBackend.filter_queryset(self.request.user, Club, "view")) \
|
club = Club.objects.filter(PermissionBackend.filter_queryset(self.request, Club, "view")) \
|
||||||
.get(pk=self.kwargs["club_pk"])
|
.get(pk=self.kwargs["club_pk"])
|
||||||
user = form.instance.user
|
user = form.instance.user
|
||||||
old_membership = None
|
old_membership = None
|
||||||
@ -704,6 +756,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
|
||||||
@ -746,6 +802,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
|
|||||||
# When we renew the BDE membership, we update the profile section
|
# When we renew the BDE membership, we update the profile section
|
||||||
# that should happens at least once a year.
|
# that should happens at least once a year.
|
||||||
user.profile.section = user.profile.section_generated
|
user.profile.section = user.profile.section_generated
|
||||||
|
user.profile._force_save = True
|
||||||
user.profile.save()
|
user.profile.save()
|
||||||
|
|
||||||
# Credit note before the membership is created.
|
# Credit note before the membership is created.
|
||||||
@ -878,7 +935,7 @@ class ClubMembersListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableV
|
|||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
club = Club.objects.filter(
|
club = Club.objects.filter(
|
||||||
PermissionBackend.filter_queryset(self.request.user, Club, "view")
|
PermissionBackend.filter_queryset(self.request, Club, "view")
|
||||||
).get(pk=self.kwargs["pk"])
|
).get(pk=self.kwargs["pk"])
|
||||||
context["club"] = club
|
context["club"] = club
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2020 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-2020 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-2020 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
|
||||||
@ -8,11 +8,12 @@ from rest_framework.exceptions import ValidationError
|
|||||||
from rest_polymorphic.serializers import PolymorphicSerializer
|
from rest_polymorphic.serializers import PolymorphicSerializer
|
||||||
from member.api.serializers import MembershipSerializer
|
from member.api.serializers import MembershipSerializer
|
||||||
from member.models import Membership
|
from member.models import Membership
|
||||||
from note_kfet.middlewares import get_current_authenticated_user
|
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.
|
||||||
@ -126,7 +141,7 @@ class ConsumerSerializer(serializers.ModelSerializer):
|
|||||||
"""
|
"""
|
||||||
# If the user has no right to see the note, then we only display the note identifier
|
# If the user has no right to see the note, then we only display the note identifier
|
||||||
return NotePolymorphicSerializer().to_representation(obj.note)\
|
return NotePolymorphicSerializer().to_representation(obj.note)\
|
||||||
if PermissionBackend.check_perm(get_current_authenticated_user(), "note.view_note", obj.note)\
|
if PermissionBackend.check_perm(get_current_request(), "note.view_note", obj.note)\
|
||||||
else dict(
|
else dict(
|
||||||
id=obj.note.id,
|
id=obj.note.id,
|
||||||
name=str(obj.note),
|
name=str(obj.note),
|
||||||
@ -142,7 +157,7 @@ class ConsumerSerializer(serializers.ModelSerializer):
|
|||||||
def get_membership(self, obj):
|
def get_membership(self, obj):
|
||||||
if isinstance(obj.note, NoteUser):
|
if isinstance(obj.note, NoteUser):
|
||||||
memberships = Membership.objects.filter(
|
memberships = Membership.objects.filter(
|
||||||
PermissionBackend.filter_queryset(get_current_authenticated_user(), Membership, "view")).filter(
|
PermissionBackend.filter_queryset(get_current_request(), Membership, "view")).filter(
|
||||||
user=obj.note.user,
|
user=obj.note.user,
|
||||||
club=2, # Kfet
|
club=2, # Kfet
|
||||||
).order_by("-date_start")
|
).order_by("-date_start")
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
# Copyright (C) 2018-2020 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,5 +1,6 @@
|
|||||||
# Copyright (C) 2018-2020 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
|
||||||
@ -10,12 +11,12 @@ from rest_framework import viewsets
|
|||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from api.viewsets import ReadProtectedModelViewSet, ReadOnlyProtectedModelViewSet
|
from api.viewsets import ReadProtectedModelViewSet, ReadOnlyProtectedModelViewSet
|
||||||
from note_kfet.middlewares import get_current_session
|
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
@ -40,12 +41,11 @@ class NotePolymorphicViewSet(ReadProtectedModelViewSet):
|
|||||||
Parse query and apply filters.
|
Parse query and apply filters.
|
||||||
:return: The filtered set of requested notes
|
:return: The filtered set of requested notes
|
||||||
"""
|
"""
|
||||||
user = self.request.user
|
queryset = self.queryset.filter(PermissionBackend.filter_queryset(self.request, Note, "view")
|
||||||
get_current_session().setdefault("permission_mask", 42)
|
| PermissionBackend.filter_queryset(self.request, NoteUser, "view")
|
||||||
queryset = self.queryset.filter(PermissionBackend.filter_queryset(user, Note, "view")
|
| PermissionBackend.filter_queryset(self.request, NoteClub, "view")
|
||||||
| PermissionBackend.filter_queryset(user, NoteUser, "view")
|
| PermissionBackend.filter_queryset(self.request, NoteSpecial, "view"))\
|
||||||
| PermissionBackend.filter_queryset(user, NoteClub, "view")
|
.distinct()
|
||||||
| PermissionBackend.filter_queryset(user, NoteSpecial, "view")).distinct()
|
|
||||||
|
|
||||||
alias = self.request.query_params.get("alias", ".*")
|
alias = self.request.query_params.get("alias", ".*")
|
||||||
queryset = queryset.filter(
|
queryset = queryset.filter(
|
||||||
@ -57,24 +57,25 @@ class NotePolymorphicViewSet(ReadProtectedModelViewSet):
|
|||||||
return queryset.order_by("id")
|
return queryset.order_by("id")
|
||||||
|
|
||||||
|
|
||||||
class AliasViewSet(ReadProtectedModelViewSet):
|
class TrustViewSet(ReadProtectedModelViewSet):
|
||||||
"""
|
"""
|
||||||
REST API View set.
|
REST Trust View set.
|
||||||
The djangorestframework plugin will get all `Alias` objects, serialize it to JSON with the given serializer,
|
The djangorestframework plugin will get all `Trust` objects, serialize it to JSON with the given serializer,
|
||||||
then render it on /api/aliases/
|
then render it on /api/note/trust/
|
||||||
"""
|
"""
|
||||||
queryset = Alias.objects
|
queryset = Trust.objects
|
||||||
serializer_class = AliasSerializer
|
serializer_class = TrustSerializer
|
||||||
filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
|
filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
|
||||||
search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ]
|
search_fields = ['$trusting__alias__name', '$trusting__alias__normalized_name',
|
||||||
filterset_fields = ['note', 'note__noteuser__user', 'note__noteclub__club', 'note__polymorphic_ctype__model', ]
|
'$trusted__alias__name', '$trusted__alias__normalized_name']
|
||||||
ordering_fields = ['name', 'normalized_name', ]
|
filterset_fields = ['trusting', 'trusting__noteuser__user', 'trusted', 'trusted__noteuser__user']
|
||||||
|
ordering_fields = ['trusting', 'trusted', ]
|
||||||
|
|
||||||
def get_serializer_class(self):
|
def get_serializer_class(self):
|
||||||
serializer_class = self.serializer_class
|
serializer_class = self.serializer_class
|
||||||
if self.request.method in ['PUT', 'PATCH']:
|
if self.request.method in ['PUT', 'PATCH']:
|
||||||
# alias owner cannot be change once establish
|
# trust relationship can't change people involved
|
||||||
setattr(serializer_class.Meta, 'read_only_fields', ('note',))
|
serializer_class.Meta.read_only_fields = ('trusting', 'trusting',)
|
||||||
return serializer_class
|
return serializer_class
|
||||||
|
|
||||||
def destroy(self, request, *args, **kwargs):
|
def destroy(self, request, *args, **kwargs):
|
||||||
@ -82,7 +83,37 @@ class AliasViewSet(ReadProtectedModelViewSet):
|
|||||||
try:
|
try:
|
||||||
self.perform_destroy(instance)
|
self.perform_destroy(instance)
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
return Response({e.code: e.message}, status.HTTP_400_BAD_REQUEST)
|
return Response({e.code: str(e)}, status.HTTP_400_BAD_REQUEST)
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
|
class AliasViewSet(ReadProtectedModelViewSet):
|
||||||
|
"""
|
||||||
|
REST API View set.
|
||||||
|
The djangorestframework plugin will get all `Alias` objects, serialize it to JSON with the given serializer,
|
||||||
|
then render it on /api/note/aliases/
|
||||||
|
"""
|
||||||
|
queryset = Alias.objects
|
||||||
|
serializer_class = AliasSerializer
|
||||||
|
filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
|
||||||
|
search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ]
|
||||||
|
filterset_fields = ['name', 'normalized_name', 'note', 'note__noteuser__user',
|
||||||
|
'note__noteclub__club', 'note__polymorphic_ctype__model', ]
|
||||||
|
ordering_fields = ['name', 'normalized_name', ]
|
||||||
|
|
||||||
|
def get_serializer_class(self):
|
||||||
|
serializer_class = self.serializer_class
|
||||||
|
if self.request.method in ['PUT', 'PATCH']:
|
||||||
|
# alias owner cannot be change once establish
|
||||||
|
serializer_class.Meta.read_only_fields = ('note',)
|
||||||
|
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)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
@ -118,7 +149,8 @@ class ConsumerViewSet(ReadOnlyProtectedModelViewSet):
|
|||||||
serializer_class = ConsumerSerializer
|
serializer_class = ConsumerSerializer
|
||||||
filter_backends = [SearchFilter, OrderingFilter, DjangoFilterBackend]
|
filter_backends = [SearchFilter, OrderingFilter, DjangoFilterBackend]
|
||||||
search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ]
|
search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ]
|
||||||
filterset_fields = ['note', 'note__noteuser__user', 'note__noteclub__club', 'note__polymorphic_ctype__model', ]
|
filterset_fields = ['name', 'normalized_name', 'note', 'note__noteuser__user',
|
||||||
|
'note__noteclub__club', 'note__polymorphic_ctype__model', ]
|
||||||
ordering_fields = ['name', 'normalized_name', ]
|
ordering_fields = ['name', 'normalized_name', ]
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
@ -133,23 +165,31 @@ class ConsumerViewSet(ReadOnlyProtectedModelViewSet):
|
|||||||
if settings.DATABASES[queryset.db]["ENGINE"] == 'django.db.backends.postgresql' else queryset
|
if settings.DATABASES[queryset.db]["ENGINE"] == 'django.db.backends.postgresql' else queryset
|
||||||
|
|
||||||
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
|
||||||
|
try:
|
||||||
|
re.compile(alias)
|
||||||
|
valid_regex = True
|
||||||
|
except (re.error, TypeError):
|
||||||
|
valid_regex = False
|
||||||
|
suffix = '__iregex' if valid_regex else '__istartswith'
|
||||||
|
alias_prefix = '^' if valid_regex else ''
|
||||||
queryset = queryset.prefetch_related('note')
|
queryset = queryset.prefetch_related('note')
|
||||||
|
|
||||||
if alias:
|
if alias:
|
||||||
# We match first an alias if it is matched without normalization,
|
# We match first an alias if it is matched without normalization,
|
||||||
# then if the normalized pattern matches a normalized alias.
|
# then if the normalized pattern matches a normalized alias.
|
||||||
queryset = queryset.filter(
|
queryset = queryset.filter(
|
||||||
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_prefix + alias.lower()})
|
||||||
& ~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)
|
all=True)
|
||||||
|
|
||||||
@ -205,7 +245,5 @@ class TransactionViewSet(ReadProtectedModelViewSet):
|
|||||||
ordering_fields = ['created_at', 'amount', ]
|
ordering_fields = ['created_at', 'amount', ]
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
user = self.request.user
|
return self.model.objects.filter(PermissionBackend.filter_queryset(self.request, self.model, "view"))\
|
||||||
get_current_session().setdefault("permission_mask", 42)
|
|
||||||
return self.model.objects.filter(PermissionBackend.filter_queryset(user, self.model, "view"))\
|
|
||||||
.order_by("created_at", "id")
|
.order_by("created_at", "id")
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2020 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-2020 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
|
||||||
|
|
||||||
|
19
apps/note/migrations/0005_auto_20210313_1235.py
Normal file
19
apps/note/migrations/0005_auto_20210313_1235.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 2.2.19 on 2021-03-13 11:35
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('note', '0004_remove_null_tag_on_charfields'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='alias',
|
||||||
|
name='note',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='alias', to='note.Note'),
|
||||||
|
),
|
||||||
|
]
|
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-2020 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,10 +1,9 @@
|
|||||||
# Copyright (C) 2018-2020 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
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.conf.global_settings import DEFAULT_FROM_EMAIL
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.mail import send_mail
|
from django.core.mail import send_mail
|
||||||
from django.core.validators import RegexValidator
|
from django.core.validators import RegexValidator
|
||||||
@ -190,8 +189,8 @@ class NoteClub(Note):
|
|||||||
def send_mail_negative_balance(self):
|
def send_mail_negative_balance(self):
|
||||||
plain_text = render_to_string("note/mails/negative_balance.txt", dict(note=self))
|
plain_text = render_to_string("note/mails/negative_balance.txt", dict(note=self))
|
||||||
html = render_to_string("note/mails/negative_balance.html", dict(note=self))
|
html = render_to_string("note/mails/negative_balance.html", dict(note=self))
|
||||||
send_mail("[Note Kfet] Passage en négatif (club {})".format(self.club.name), plain_text, DEFAULT_FROM_EMAIL,
|
send_mail("[Note Kfet] Passage en négatif (club {})".format(self.club.name), plain_text,
|
||||||
[self.club.email], html_message=html)
|
settings.DEFAULT_FROM_EMAIL, [self.club.email], html_message=html)
|
||||||
|
|
||||||
|
|
||||||
class NoteSpecial(Note):
|
class NoteSpecial(Note):
|
||||||
@ -218,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.
|
||||||
@ -262,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):
|
||||||
"""
|
"""
|
||||||
@ -290,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-2020 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,25 +332,44 @@ 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
|
@staticmethod
|
||||||
def save(self, *args, **kwargs):
|
def validate_payment_form(form):
|
||||||
self.clean()
|
"""
|
||||||
super().save(*args, **kwargs)
|
Ensure that last name and first name are filled for a form that creates a SpecialTransaction,
|
||||||
|
and check that if the user pays with a check, then the bank field is filled.
|
||||||
|
|
||||||
class Meta:
|
Return True iff there is no error.
|
||||||
verbose_name = _("Special transaction")
|
Whenever there is an error, they are inserted in the form errors.
|
||||||
verbose_name_plural = _("Special transactions")
|
"""
|
||||||
|
|
||||||
|
credit_type = form.cleaned_data["credit_type"]
|
||||||
|
last_name = form.cleaned_data["last_name"]
|
||||||
|
first_name = form.cleaned_data["first_name"]
|
||||||
|
bank = form.cleaned_data["bank"]
|
||||||
|
|
||||||
|
error = False
|
||||||
|
|
||||||
|
if not last_name or not first_name or (not bank and credit_type.special_type == "Chèque"):
|
||||||
|
if not last_name:
|
||||||
|
form.add_error('last_name', _("This field is required."))
|
||||||
|
error = True
|
||||||
|
if not first_name:
|
||||||
|
form.add_error('first_name', _("This field is required."))
|
||||||
|
error = True
|
||||||
|
if not bank and credit_type.special_type == "Chèque":
|
||||||
|
form.add_error('bank', _("This field is required."))
|
||||||
|
error = True
|
||||||
|
|
||||||
|
return not error
|
||||||
|
|
||||||
|
|
||||||
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-2020 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-2020 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.
|
||||||
@ -28,7 +28,7 @@ $(document).ready(function () {
|
|||||||
|
|
||||||
// Switching in double consumptions mode should update the layout
|
// Switching in double consumptions mode should update the layout
|
||||||
$('#double_conso').change(function () {
|
$('#double_conso').change(function () {
|
||||||
$('#consos_list_div').removeClass('d-none')
|
document.getElementById('consos_list_div').classList.remove('d-none')
|
||||||
$('#infos_div').attr('class', 'col-sm-5 col-xl-6')
|
$('#infos_div').attr('class', 'col-sm-5 col-xl-6')
|
||||||
|
|
||||||
const note_list_obj = $('#note_list')
|
const note_list_obj = $('#note_list')
|
||||||
@ -37,7 +37,7 @@ $(document).ready(function () {
|
|||||||
note_list_obj.html('')
|
note_list_obj.html('')
|
||||||
|
|
||||||
buttons.forEach(function (button) {
|
buttons.forEach(function (button) {
|
||||||
$('#conso_button_' + button.id).click(function () {
|
document.getElementById(`conso_button_${button.id}`).addEventListener('click', () => {
|
||||||
if (LOCK) { return }
|
if (LOCK) { return }
|
||||||
removeNote(button, 'conso_button', buttons, 'consos_list')()
|
removeNote(button, 'conso_button', buttons, 'consos_list')()
|
||||||
})
|
})
|
||||||
@ -46,7 +46,7 @@ $(document).ready(function () {
|
|||||||
})
|
})
|
||||||
|
|
||||||
$('#single_conso').change(function () {
|
$('#single_conso').change(function () {
|
||||||
$('#consos_list_div').addClass('d-none')
|
document.getElementById('consos_list_div').classList.add('d-none')
|
||||||
$('#infos_div').attr('class', 'col-sm-5 col-md-4')
|
$('#infos_div').attr('class', 'col-sm-5 col-md-4')
|
||||||
|
|
||||||
const consos_list_obj = $('#consos_list')
|
const consos_list_obj = $('#consos_list')
|
||||||
@ -68,9 +68,9 @@ $(document).ready(function () {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Ensure we begin in single consumption. Fix issue with TurboLinks and BootstrapJS
|
// Ensure we begin in single consumption. Fix issue with TurboLinks and BootstrapJS
|
||||||
$("label[for='double_conso']").removeClass('active')
|
document.querySelector("label[for='double_conso']").classList.remove('active')
|
||||||
|
|
||||||
$('#consume_all').click(consumeAll)
|
document.getElementById("consume_all").addEventListener('click', consumeAll)
|
||||||
})
|
})
|
||||||
|
|
||||||
notes = []
|
notes = []
|
||||||
@ -127,11 +127,10 @@ function addConso (dest, amount, type, category_id, category_name, template_id,
|
|||||||
html += li('conso_button_' + button.id, button.name +
|
html += li('conso_button_' + button.id, button.name +
|
||||||
'<span class="badge badge-dark badge-pill">' + button.quantity + '</span>')
|
'<span class="badge badge-dark badge-pill">' + button.quantity + '</span>')
|
||||||
})
|
})
|
||||||
|
document.getElementById(list).innerHTML = html
|
||||||
|
|
||||||
$('#' + list).html(html)
|
buttons.forEach((button) => {
|
||||||
|
document.getElementById(`conso_button_${button.id}`).addEventListener('click', () => {
|
||||||
buttons.forEach(function (button) {
|
|
||||||
$('#conso_button_' + button.id).click(function () {
|
|
||||||
if (LOCK) { return }
|
if (LOCK) { return }
|
||||||
removeNote(button, 'conso_button', buttons, list)()
|
removeNote(button, 'conso_button', buttons, list)()
|
||||||
})
|
})
|
||||||
@ -146,12 +145,13 @@ function reset () {
|
|||||||
notes_display.length = 0
|
notes_display.length = 0
|
||||||
notes.length = 0
|
notes.length = 0
|
||||||
buttons.length = 0
|
buttons.length = 0
|
||||||
$('#note_list').html('')
|
document.getElementById('note_list').innerHTML = ''
|
||||||
$('#consos_list').html('')
|
document.getElementById('consos_list').innerHTML = ''
|
||||||
$('#note').val('')
|
document.getElementById('note').value = ''
|
||||||
$('#note').attr('data-original-title', '').tooltip('hide')
|
document.getElementById('note').dataset.originTitle = ''
|
||||||
$('#profile_pic').attr('src', '/static/member/img/default_picture.png')
|
$('#note').tooltip('hide')
|
||||||
$('#profile_pic_link').attr('href', '#')
|
document.getElementById('profile_pic').src = '/static/member/img/default_picture.png'
|
||||||
|
document.getElementById('profile_pic_link').href = '#'
|
||||||
refreshHistory()
|
refreshHistory()
|
||||||
refreshBalance()
|
refreshBalance()
|
||||||
LOCK = false
|
LOCK = false
|
||||||
@ -168,7 +168,7 @@ function consumeAll () {
|
|||||||
let error = false
|
let error = false
|
||||||
|
|
||||||
if (notes_display.length === 0) {
|
if (notes_display.length === 0) {
|
||||||
$('#note').addClass('is-invalid')
|
document.getElementById('note').classList.add('is-invalid')
|
||||||
$('#note_list').html(li('', '<strong>Ajoutez des émetteurs.</strong>', 'text-danger'))
|
$('#note_list').html(li('', '<strong>Ajoutez des émetteurs.</strong>', 'text-danger'))
|
||||||
error = true
|
error = true
|
||||||
}
|
}
|
||||||
@ -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()
|
||||||
|
});
|
||||||
|
@ -222,6 +222,13 @@ $(document).ready(function () {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Make transfer when pressing Enter on the amount section
|
||||||
|
$('#amount, #reason, #last_name, #first_name, #bank').keypress((event) => {
|
||||||
|
if (event.originalEvent.charCode === 13) {
|
||||||
|
$('#btn_transfer').click()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
$('#btn_transfer').click(function () {
|
$('#btn_transfer').click(function () {
|
||||||
if (LOCK) { return }
|
if (LOCK) { return }
|
||||||
|
|
||||||
@ -243,7 +250,7 @@ $('#btn_transfer').click(function () {
|
|||||||
error = true
|
error = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const amount = Math.floor(100 * amount_field.val())
|
const amount = Math.round(100 * amount_field.val())
|
||||||
if (amount > 2147483647) {
|
if (amount > 2147483647) {
|
||||||
amount_field.addClass('is-invalid')
|
amount_field.addClass('is-invalid')
|
||||||
$('#amount-required').html('<strong>' + gettext('The amount must stay under 21,474,836.47 €.') + '</strong>')
|
$('#amount-required').html('<strong>' + gettext('The amount must stay under 21,474,836.47 €.') + '</strong>')
|
||||||
@ -307,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()
|
||||||
@ -348,14 +355,14 @@ $('#btn_transfer').click(function () {
|
|||||||
destination_alias: dest.name
|
destination_alias: dest.name
|
||||||
}).done(function () {
|
}).done(function () {
|
||||||
addMsg(interpolate(gettext('Transfer of %s from %s to %s failed: %s'),
|
addMsg(interpolate(gettext('Transfer of %s from %s to %s failed: %s'),
|
||||||
[pretty_money(source.quantity * dest.quantity * amount), source.name, + dest.name, gettext('insufficient funds')]), 'danger', 10000)
|
[pretty_money(source.quantity * dest.quantity * amount), source.name, dest.name, gettext('insufficient funds')]), 'danger', 10000)
|
||||||
reset()
|
reset()
|
||||||
}).fail(function (err) {
|
}).fail(function (err) {
|
||||||
const errObj = JSON.parse(err.responseText)
|
const errObj = JSON.parse(err.responseText)
|
||||||
let error = errObj.detail ? errObj.detail : errObj.non_field_errors
|
let error = errObj.detail ? errObj.detail : errObj.non_field_errors
|
||||||
if (!error) { error = err.responseText }
|
if (!error) { error = err.responseText }
|
||||||
addMsg(interpolate(gettext('Transfer of %s from %s to %s failed: %s'),
|
addMsg(interpolate(gettext('Transfer of %s from %s to %s failed: %s'),
|
||||||
[pretty_money(source.quantity * dest.quantity * amount), source.name, + dest.name, error]), 'danger')
|
[pretty_money(source.quantity * dest.quantity * amount), source.name, dest.name, error]), 'danger')
|
||||||
LOCK = false
|
LOCK = false
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
# Copyright (C) 2018-2020 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_authenticated_user
|
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
|
||||||
|
|
||||||
@ -88,16 +88,16 @@ class HistoryTable(tables.Table):
|
|||||||
"class": lambda record:
|
"class": lambda record:
|
||||||
str(record.valid).lower()
|
str(record.valid).lower()
|
||||||
+ (' validate' if record.source.is_active and record.destination.is_active and PermissionBackend
|
+ (' validate' if record.source.is_active and record.destination.is_active and PermissionBackend
|
||||||
.check_perm(get_current_authenticated_user(), "note.change_transaction_invalidity_reason", record)
|
.check_perm(get_current_request(), "note.change_transaction_invalidity_reason", record)
|
||||||
else ''),
|
else ''),
|
||||||
"data-toggle": "tooltip",
|
"data-toggle": "tooltip",
|
||||||
"title": lambda record: (_("Click to invalidate") if record.valid else _("Click to validate"))
|
"title": lambda record: (_("Click to invalidate") if record.valid else _("Click to validate"))
|
||||||
if PermissionBackend.check_perm(get_current_authenticated_user(),
|
if PermissionBackend.check_perm(get_current_request(),
|
||||||
"note.change_transaction_invalidity_reason", record)
|
"note.change_transaction_invalidity_reason", record)
|
||||||
and record.source.is_active and record.destination.is_active else None,
|
and record.source.is_active and record.destination.is_active else None,
|
||||||
"onclick": lambda record: 'de_validate(' + str(record.id) + ', ' + str(record.valid).lower()
|
"onclick": lambda record: 'de_validate(' + str(record.id) + ', ' + str(record.valid).lower()
|
||||||
+ ', "' + str(record.__class__.__name__) + '")'
|
+ ', "' + str(record.__class__.__name__) + '")'
|
||||||
if PermissionBackend.check_perm(get_current_authenticated_user(),
|
if PermissionBackend.check_perm(get_current_request(),
|
||||||
"note.change_transaction_invalidity_reason", record)
|
"note.change_transaction_invalidity_reason", record)
|
||||||
and record.source.is_active and record.destination.is_active else None,
|
and record.source.is_active and record.destination.is_active else None,
|
||||||
"onmouseover": lambda record: '$("#invalidity_reason_'
|
"onmouseover": lambda record: '$("#invalidity_reason_'
|
||||||
@ -126,7 +126,7 @@ class HistoryTable(tables.Table):
|
|||||||
When the validation status is hovered, an input field is displayed to let the user specify an invalidity reason
|
When the validation status is hovered, an input field is displayed to let the user specify an invalidity reason
|
||||||
"""
|
"""
|
||||||
has_perm = PermissionBackend \
|
has_perm = PermissionBackend \
|
||||||
.check_perm(get_current_authenticated_user(), "note.change_transaction_invalidity_reason", record)
|
.check_perm(get_current_request(), "note.change_transaction_invalidity_reason", record)
|
||||||
|
|
||||||
val = "✔" if value else "✖"
|
val = "✔" if value else "✖"
|
||||||
|
|
||||||
@ -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 = {
|
||||||
@ -165,7 +230,7 @@ class AliasTable(tables.Table):
|
|||||||
extra_context={"delete_trans": _('delete')},
|
extra_context={"delete_trans": _('delete')},
|
||||||
attrs={'td': {'class': lambda record: 'col-sm-1' + (
|
attrs={'td': {'class': lambda record: 'col-sm-1' + (
|
||||||
' d-none' if not PermissionBackend.check_perm(
|
' d-none' if not PermissionBackend.check_perm(
|
||||||
get_current_authenticated_user(), "note.delete_alias",
|
get_current_request(), "note.delete_alias",
|
||||||
record) else '')}}, verbose_name=_("Delete"), )
|
record) else '')}}, verbose_name=_("Delete"), )
|
||||||
|
|
||||||
|
|
||||||
@ -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 %}
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
|
|
||||||
<p>
|
<p>
|
||||||
Par ailleurs, le BDE ne sert pas d'alcool aux adhérents dont le solde
|
Par ailleurs, le BDE ne sert pas d'alcool aux adhérents dont le solde
|
||||||
est inférieur à 0 € depuis plus de 24h.
|
est inférieur à 0 €.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
@ -43,4 +43,4 @@
|
|||||||
{% trans "Mail generated by the Note Kfet on the" %} {% now "j F Y à H:i:s" %}
|
{% trans "Mail generated by the Note Kfet on the" %} {% now "j F Y à H:i:s" %}
|
||||||
</p>
|
</p>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -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
|
||||||
|
@ -10,21 +10,25 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
{# bandeau transfert/crédit/débit/activité #}
|
{# bandeau transfert/crédit/débit/activité #}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xl-12">
|
<div class="col-xl-12">
|
||||||
<div class="btn-group btn-group-toggle btn-block" data-toggle="buttons">
|
<div class="btn-group btn-block">
|
||||||
<label for="type_transfer" class="btn btn-sm btn-outline-primary active">
|
<div class="btn-group btn-group-toggle btn-block" data-toggle="buttons">
|
||||||
<input type="radio" name="transaction_type" id="type_transfer">
|
<label for="type_transfer" class="btn btn-sm btn-outline-primary active">
|
||||||
{% trans "Transfer" %}
|
<input type="radio" name="transaction_type" id="type_transfer">
|
||||||
</label>
|
{% trans "Transfer" %}
|
||||||
{% if "note.notespecial"|not_empty_model_list %}
|
|
||||||
<label for="type_credit" class="btn btn-sm btn-outline-primary">
|
|
||||||
<input type="radio" name="transaction_type" id="type_credit">
|
|
||||||
{% trans "Credit" %}
|
|
||||||
</label>
|
</label>
|
||||||
<label for="type_debit" class="btn btn-sm btn-outline-primary">
|
{% if "note.notespecial"|not_empty_model_list %}
|
||||||
<input type="radio" name="transaction_type" id="type_debit">
|
<label for="type_credit" class="btn btn-sm btn-outline-primary">
|
||||||
{% trans "Debit" %}
|
<input type="radio" name="transaction_type" id="type_credit">
|
||||||
</label>
|
{% trans "Credit" %}
|
||||||
{% endif %}
|
</label>
|
||||||
|
<label for="type_debit" class="btn btn-sm btn-outline-primary">
|
||||||
|
<input type="radio" name="transaction_type" id="type_debit">
|
||||||
|
{% trans "Debit" %}
|
||||||
|
</label>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Add shortcuts for opened activites if necessary #}
|
||||||
{% for activity in activities_open %}
|
{% for activity in activities_open %}
|
||||||
<a href="{% url "activity:activity_entry" pk=activity.pk %}" class="btn btn-sm btn-outline-primary">
|
<a href="{% url "activity:activity_entry" pk=activity.pk %}" class="btn btn-sm btn-outline-primary">
|
||||||
{% trans "Entries" %} {{ activity.name }}
|
{% trans "Entries" %} {{ activity.name }}
|
||||||
@ -57,7 +61,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<ul class="list-group list-group-flush" id="source_note_list">
|
<ul class="list-group list-group-flush" id="source_note_list">
|
||||||
</ul>
|
</ul>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<select id="credit_type" class="custom-select d-none">
|
<select id="credit_type" class="form-control custom-select d-none">
|
||||||
{% for special_type in special_types %}
|
{% for special_type in special_types %}
|
||||||
<option value="{{ special_type.id }}">{{ special_type.special_type }}</option>
|
<option value="{{ special_type.id }}">{{ special_type.special_type }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@ -84,7 +88,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<ul class="list-group list-group-flush" id="dest_note_list">
|
<ul class="list-group list-group-flush" id="dest_note_list">
|
||||||
</ul>
|
</ul>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<select id="debit_type" class="custom-select d-none">
|
<select id="debit_type" class="form-control custom-select d-none">
|
||||||
{% for special_type in special_types %}
|
{% for special_type in special_types %}
|
||||||
<option value="{{ special_type.id }}">{{ special_type.special_type }}</option>
|
<option value="{{ special_type.id }}">{{ special_type.special_type }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -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-2020 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-2020 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-2020 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-2020 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-2020 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,12 @@ 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 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
|
||||||
@ -38,7 +38,7 @@ class TransactionCreateView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTabl
|
|||||||
def get_queryset(self, **kwargs):
|
def get_queryset(self, **kwargs):
|
||||||
# retrieves only Transaction that user has the right to see.
|
# retrieves only Transaction that user has the right to see.
|
||||||
return Transaction.objects.filter(
|
return Transaction.objects.filter(
|
||||||
PermissionBackend.filter_queryset(self.request.user, Transaction, "view")
|
PermissionBackend.filter_queryset(self.request, Transaction, "view")
|
||||||
).order_by("-created_at").all()[:20]
|
).order_by("-created_at").all()[:20]
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
@ -47,16 +47,16 @@ class TransactionCreateView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTabl
|
|||||||
context['polymorphic_ctype'] = ContentType.objects.get_for_model(Transaction).pk
|
context['polymorphic_ctype'] = ContentType.objects.get_for_model(Transaction).pk
|
||||||
context['special_polymorphic_ctype'] = ContentType.objects.get_for_model(SpecialTransaction).pk
|
context['special_polymorphic_ctype'] = ContentType.objects.get_for_model(SpecialTransaction).pk
|
||||||
context['special_types'] = NoteSpecial.objects\
|
context['special_types'] = NoteSpecial.objects\
|
||||||
.filter(PermissionBackend.filter_queryset(self.request.user, NoteSpecial, "view"))\
|
.filter(PermissionBackend.filter_queryset(self.request, NoteSpecial, "view"))\
|
||||||
.order_by("special_type").all()
|
.order_by("special_type").all()
|
||||||
|
|
||||||
# Add a shortcut for entry page for open activities
|
# Add a shortcut for entry page for open activities
|
||||||
if "activity" in settings.INSTALLED_APPS:
|
if "activity" in settings.INSTALLED_APPS:
|
||||||
from activity.models import Activity
|
from activity.models import Activity
|
||||||
activities_open = Activity.objects.filter(open=True).filter(
|
activities_open = Activity.objects.filter(open=True, activity_type__manage_entries=True).filter(
|
||||||
PermissionBackend.filter_queryset(self.request.user, Activity, "view")).distinct().all()
|
PermissionBackend.filter_queryset(self.request, Activity, "view")).distinct().all()
|
||||||
context["activities_open"] = [a for a in activities_open
|
context["activities_open"] = [a for a in activities_open
|
||||||
if PermissionBackend.check_perm(self.request.user,
|
if PermissionBackend.check_perm(self.request,
|
||||||
"activity.add_entry",
|
"activity.add_entry",
|
||||||
Entry(activity=a,
|
Entry(activity=a,
|
||||||
note=self.request.user.note, ))]
|
note=self.request.user.note, ))]
|
||||||
@ -90,9 +90,9 @@ class TransactionTemplateListView(ProtectQuerysetMixin, LoginRequiredMixin, Sing
|
|||||||
if "search" in self.request.GET:
|
if "search" in self.request.GET:
|
||||||
pattern = self.request.GET["search"]
|
pattern = self.request.GET["search"]
|
||||||
qs = qs.filter(
|
qs = qs.filter(
|
||||||
Q(name__iregex="^" + pattern)
|
Q(name__iregex=pattern)
|
||||||
| Q(destination__club__name__iregex="^" + pattern)
|
| Q(destination__club__name__iregex=pattern)
|
||||||
| Q(category__name__iregex="^" + pattern)
|
| Q(category__name__iregex=pattern)
|
||||||
| Q(description__iregex=pattern)
|
| Q(description__iregex=pattern)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -159,7 +159,7 @@ class ConsoView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
|||||||
return self.handle_no_permission()
|
return self.handle_no_permission()
|
||||||
|
|
||||||
templates = TransactionTemplate.objects.filter(
|
templates = TransactionTemplate.objects.filter(
|
||||||
PermissionBackend().filter_queryset(self.request.user, TransactionTemplate, "view")
|
PermissionBackend().filter_queryset(self.request, TransactionTemplate, "view")
|
||||||
)
|
)
|
||||||
if not templates.exists():
|
if not templates.exists():
|
||||||
raise PermissionDenied(_("You can't see any button."))
|
raise PermissionDenied(_("You can't see any button."))
|
||||||
@ -170,7 +170,7 @@ class ConsoView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
|||||||
restrict to the transaction history the user can see.
|
restrict to the transaction history the user can see.
|
||||||
"""
|
"""
|
||||||
return Transaction.objects.filter(
|
return Transaction.objects.filter(
|
||||||
PermissionBackend.filter_queryset(self.request.user, Transaction, "view")
|
PermissionBackend.filter_queryset(self.request, Transaction, "view")
|
||||||
).order_by("-created_at").all()[:20]
|
).order_by("-created_at").all()[:20]
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
@ -180,16 +180,20 @@ class ConsoView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
|||||||
# for each category, find which transaction templates the user can see.
|
# for each category, find which transaction templates the user can see.
|
||||||
for category in categories:
|
for category in categories:
|
||||||
category.templates_filtered = category.templates.filter(
|
category.templates_filtered = category.templates.filter(
|
||||||
PermissionBackend().filter_queryset(self.request.user, TransactionTemplate, "view")
|
PermissionBackend().filter_queryset(self.request, TransactionTemplate, "view")
|
||||||
).filter(display=True).order_by('name').all()
|
).filter(display=True).order_by('name').all()
|
||||||
|
|
||||||
context['categories'] = [cat for cat in categories if cat.templates_filtered]
|
context['categories'] = [cat for cat in categories if cat.templates_filtered]
|
||||||
# some transactiontemplate are put forward to find them easily
|
# some transactiontemplate are put forward to find them easily
|
||||||
context['highlighted'] = TransactionTemplate.objects.filter(highlighted=True).filter(
|
context['highlighted'] = TransactionTemplate.objects.filter(highlighted=True).filter(
|
||||||
PermissionBackend().filter_queryset(self.request.user, TransactionTemplate, "view")
|
PermissionBackend().filter_queryset(self.request, TransactionTemplate, "view")
|
||||||
).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
|
||||||
|
|
||||||
|
|
||||||
@ -209,7 +213,7 @@ class TransactionSearchView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView
|
|||||||
data = form.cleaned_data if form.is_valid() else {}
|
data = form.cleaned_data if form.is_valid() else {}
|
||||||
|
|
||||||
transactions = Transaction.objects.annotate(total_amount=F("quantity") * F("amount")).filter(
|
transactions = Transaction.objects.annotate(total_amount=F("quantity") * F("amount")).filter(
|
||||||
PermissionBackend.filter_queryset(self.request.user, Transaction, "view"))\
|
PermissionBackend.filter_queryset(self.request, Transaction, "view"))\
|
||||||
.filter(Q(source=self.object) | Q(destination=self.object)).order_by('-created_at')
|
.filter(Q(source=self.object) | Q(destination=self.object)).order_by('-created_at')
|
||||||
|
|
||||||
if "source" in data and data["source"]:
|
if "source" in data and data["source"]:
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2020 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'
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user