mirror of
https://gitlab.crans.org/bde/nk20
synced 2025-06-21 09:58:23 +02:00
Compare commits
571 Commits
Author | SHA1 | Date | |
---|---|---|---|
2ab5c4082a | |||
053225c6dc | |||
ac7b86651d | |||
21f5a5d566 | |||
ff9c78ed4e | |||
1e121297d1 | |||
28117c8c61 | |||
4be4a18dd1 | |||
27b00ba4f0 | |||
3fcbb4f310 | |||
d1c9a2a7f1 | |||
a673fd6871 | |||
a324d3a892 | |||
951ba74f8f | |||
abc4f14bd1 | |||
47138bafd4 | |||
a3920fcae3 | |||
ae4213d087 | |||
cbf92651f0 | |||
12c93ff9da | |||
354c79bb82 | |||
1ea7b3dda1 | |||
35ffbfcf55 | |||
162371042c | |||
581715d804 | |||
c7c6f0350f | |||
9d1024024b | |||
d595d908c6 | |||
734f5b242d | |||
b0c7d43a50 | |||
7322d55789 | |||
1a258dfe9e | |||
b8f81048a5 | |||
af819f45a1 | |||
076d065ffa | |||
2da77d9c17 | |||
01584d6330 | |||
4c0a5922c4 | |||
f90b28fc7c | |||
925e0f26f5 | |||
c912383f86 | |||
32830e43fd | |||
11c6a6fa7a | |||
201d6b114a | |||
19e77df299 | |||
5fd6ec5668 | |||
10a01c5bc2 | |||
989905ea64 | |||
0218d43a17 | |||
5d30b0e819 | |||
ec759dd3c0 | |||
2eb965291d | |||
7f182ee2ee | |||
3132aa4c38 | |||
c7eb774859 | |||
32f8d285b3 | |||
050256ea13 | |||
7afd15b1cc | |||
258361f116 | |||
a307530579 | |||
5de930bf40 | |||
f7ebe0e99b | |||
73de6e2176 | |||
201611b105 | |||
40c239e9da | |||
2aaab2b454 | |||
fc088dec86 | |||
2d60f1fd7b | |||
7b48b09329 | |||
ffac940511 | |||
50f98fd5ad | |||
402e19d1ce | |||
0b0394b61f | |||
98422d8259 | |||
29509b5b26 | |||
0d64ad31e0 | |||
5781cbd6a5 | |||
5295e61a00 | |||
e79ed6226a | |||
68152e6354 | |||
6c61daf1c5 | |||
b8cc297baf | |||
cd8224f2e0 | |||
3c882a7854 | |||
357e1bbaa2 | |||
f5c4c58525 | |||
dafb602b08 | |||
5b377e6a75 | |||
28bd62531e | |||
b3a31c27a5 | |||
c7a8e6a1a5 | |||
546a3a72b1 | |||
2e5664f79d | |||
e367666fe9 | |||
04a9b3daf0 | |||
d1df8f3eac | |||
a5221f66ef | |||
7d59cd6cd2 | |||
0db0474217 | |||
2b3eb15f59 | |||
a6b479db19 | |||
048d251f75 | |||
7b11cb0797 | |||
ff3c30517e | |||
f481ea6acb | |||
802fd8c2d7 | |||
5209a586a9 | |||
24f54ac876 | |||
988b4c9e88 | |||
e32c267995 | |||
5e39209ab1 | |||
08b2fabe07 | |||
405479e5ad | |||
0cc130092f | |||
ff6e207512 | |||
0f1e4d2e60 | |||
6255bcbbb1 | |||
d82a1001c4 | |||
31a54482f0 | |||
4ee02345d4 | |||
422c087d17 | |||
30d6e2c95e | |||
f3a3f07e38 | |||
a5e802f370 | |||
540f3bc354 | |||
2d19457506 | |||
72786d0d2b | |||
f099cbc879 | |||
977eb7c0d4 | |||
d81b1f2710 | |||
6a69590a82 | |||
7afc583282 | |||
4fb0b7d736 | |||
18a5b65a1c | |||
f545af4977 | |||
103e2d0635 | |||
aedf0e87ba | |||
dab45b5fd4 | |||
b3353b563c | |||
6bc52be707 | |||
834d68fe35 | |||
c6a2849d35 | |||
4ab22c92b3 | |||
c328c1457c | |||
96da7d01ae | |||
d27f942339 | |||
738d6c932d | |||
1760196578 | |||
13b9b6edea | |||
e06e3b2972 | |||
9596aa7b8c | |||
ba0d64f0d4 | |||
8d17801e28 | |||
609362c4f8 | |||
03d2d5f03e | |||
d2057a9f45 | |||
b6e68eeebe | |||
6410542027 | |||
6b1cd3ba7a | |||
9f114b8ca2 | |||
e0132b6dc8 | |||
f1cc82fab3 | |||
644cf14c4b | |||
f19a489313 | |||
dedd6c69cc | |||
b42f5afeab | |||
31e67ae3f6 | |||
b08da7a727 | |||
451aa64f33 | |||
3c99b0f3e9 | |||
201a179947 | |||
96784aee3b | |||
981c4d0300 | |||
11223430fd | |||
7aeb977e72 | |||
52fef1df42 | |||
16f8a60a3f | |||
2839d3de1e | |||
30afa6da0a | |||
84fc77696f | |||
19fc620d1f | |||
d5819ac562 | |||
a79df8f1f6 | |||
364b18e188 | |||
10a883b2e5 | |||
1410ab6c4f | |||
623dd61be6 | |||
48a0a87e7c | |||
563f525b11 | |||
63c1d74f1a | |||
c42fb380a6 | |||
c636d52a73 | |||
6a9021ec14 | |||
9c9149b53a | |||
cb74311e7b | |||
9d7dd566c9 | |||
6bceb394c5 | |||
62cf8f9d84 | |||
9944ebcaad | |||
8537f043f7 | |||
2dd1c3fb89 | |||
c8665c5798 | |||
e9f1b6f52d | |||
1d95ae4810 | |||
c89a95f8d2 | |||
73640b1dfa | |||
84b16ab603 | |||
6a1b51dbbf | |||
c441a43a8b | |||
87f3b51b04 | |||
0a853fd3e6 | |||
c429734810 | |||
5d759111b6 | |||
70baf7566c | |||
eb355f547c | |||
7068170f18 | |||
45ee9a8941 | |||
454ea19603 | |||
5a77a66391 | |||
761fc170eb
|
|||
ac23d7eb54
|
|||
40e7415062
|
|||
319405d2b1
|
|||
633ab88b04
|
|||
e29b42eecc
|
|||
dc69faaf1d
|
|||
442a5c5e36
|
|||
7ab0fec3bc
|
|||
bd4fb23351 | |||
ee22e9b3b6 | |||
19ae616fb4 | |||
b7657ec362 | |||
4d03d9460d | |||
3633f66a87 | |||
d43fbe7ac6 | |||
df5f9b5f1e | |||
4161248bff
|
|||
58136f3c48
|
|||
d9b4e0a9a9
|
|||
8563a8d235
|
|||
5f69232560 | |||
d3273e9ee2
|
|||
4e30f805a7 | |||
546e422e64
|
|||
9048a416df
|
|||
8578bd743c
|
|||
45a10dad00
|
|||
18a1282773
|
|||
132afc3d15
|
|||
6bf16a181a
|
|||
e20df82346
|
|||
1eb72044c2 | |||
f88eae924c
|
|||
4b6e3ba546
|
|||
bf0fe3479f | |||
45ba4f9537
|
|||
b204805ce2
|
|||
2f28e34cec
|
|||
9c8ea2cd41
|
|||
41289857b2 | |||
28a8792c9f
|
|||
58cafad032
|
|||
7848cd9cc2
|
|||
d18ccfac23
|
|||
e479e1e3a4 | |||
82b0c83b1f | |||
38ca414ef6
|
|||
fd811053c7
|
|||
9d386d1ecf
|
|||
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
|
|||
d5a9bf175f
|
|||
b597a6ac5b
|
|||
a704b92c3d | |||
53090b1a21 | |||
c49af0b83a | |||
5a05997d9d
|
|||
c109cd3ddd
|
|||
84304971d7
|
|||
b8b781f9a2 | |||
002128eed2 | |||
8d71783c42 | |||
a6f23df7d5
|
|||
d9c97628e2
|
|||
893534955d
|
|||
dfbf9972c2
|
|||
b5f3b3ffc1
|
|||
3aad4e7398
|
|||
b4a1b513cc
|
|||
c0c64f225c | |||
9d8f47115c
|
|||
f4156f1b94
|
|||
e60994e065
|
|||
801f711994
|
|||
e4568b410f
|
|||
c8f7986d5a | |||
d3a9c442a5
|
|||
016ab5a9c9
|
|||
7866ab7ec0
|
|||
f570ff3cd5
|
|||
6b2638c271
|
|||
5cb4183e9f
|
|||
3a20555663
|
|||
95be0042e9
|
|||
48880e7fd3
|
|||
e0030771e4
|
|||
d47799e6ee
|
|||
eae091625a
|
|||
aceb77ffb9
|
|||
338c94ed05
|
|||
290848f904 | |||
72dca54bbf | |||
117d9da3ba | |||
37efebe85b | |||
3af2ec71b6 | |||
0b4a95525b | |||
af664e481f | |||
0171f16311 | |||
296b94d237 | |||
4942553335 | |||
c1efb87180 | |||
72eead8595 | |||
ade7e583e5 | |||
4a8a101822 | |||
dd2cfa6327 | |||
2adf84b7fc | |||
2f54e64ea2 | |||
8434c0062c | |||
6d976f32bf | |||
b9d49d53f2 | |||
23243e09bb | |||
2682e9a610 | |||
5635598bbc | |||
b58a0c43cd | |||
e1f647bd02 | |||
39fd3a2471 | |||
1072e227b8 | |||
cbf7e6fe6c | |||
950922d041 | |||
78fe070cd3 | |||
51d5733578 | |||
7bd895c1df | |||
e5e94c52f2 | |||
051591cb7a | |||
0e7390b669 | |||
fe4363b83d | |||
6e80016b38 | |||
08e50ffc22 | |||
9cb65277f3 | |||
224a0fdd8c | |||
6dc7604e90 | |||
cb7f3c9f18 | |||
f910feca9e | |||
91f784872c | |||
b655135a42 | |||
58aa4983e3 | |||
6cc3cf4174 | |||
2097e67321 | |||
d773303d18 | |||
3cabcf40e7 | |||
bf29efda0a | |||
ceccba0d71 | |||
3eced33082 | |||
acb3fb4a91 | |||
420a24ebac |
@ -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
|
||||||
|
7
.gitignore
vendored
7
.gitignore
vendored
@ -42,8 +42,15 @@ map.json
|
|||||||
backups/
|
backups/
|
||||||
/static/
|
/static/
|
||||||
/media/
|
/media/
|
||||||
|
/tmp/
|
||||||
|
|
||||||
# Virtualenv
|
# Virtualenv
|
||||||
env/
|
env/
|
||||||
venv/
|
venv/
|
||||||
db.sqlite3
|
db.sqlite3
|
||||||
|
shell.nix
|
||||||
|
|
||||||
|
# ansibles customs host
|
||||||
|
ansible/host_vars/*.yaml
|
||||||
|
!ansible/host_vars/bde*
|
||||||
|
ansible/hosts
|
||||||
|
@ -1,25 +1,26 @@
|
|||||||
stages:
|
stages:
|
||||||
- test
|
- test
|
||||||
- quality-assurance
|
- quality-assurance
|
||||||
|
- docs
|
||||||
|
|
||||||
# Also fetch submodules
|
# Also fetch submodules
|
||||||
variables:
|
variables:
|
||||||
GIT_SUBMODULE_STRATEGY: recursive
|
GIT_SUBMODULE_STRATEGY: recursive
|
||||||
|
|
||||||
# Debian Buster
|
# Debian Buster
|
||||||
py37-django22:
|
# py37-django22:
|
||||||
stage: test
|
# stage: test
|
||||||
image: debian:buster-backports
|
# image: debian:buster-backports
|
||||||
before_script:
|
# before_script:
|
||||||
- >
|
# - >
|
||||||
apt-get update &&
|
# apt-get update &&
|
||||||
apt-get install --no-install-recommends -t buster-backports -y
|
# apt-get install --no-install-recommends -t buster-backports -y
|
||||||
python3-django python3-django-crispy-forms
|
# python3-django python3-django-crispy-forms
|
||||||
python3-django-extensions python3-django-filters python3-django-polymorphic
|
# python3-django-extensions python3-django-filters python3-django-polymorphic
|
||||||
python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil
|
# python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil
|
||||||
python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache
|
# python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache
|
||||||
python3-bs4 python3-setuptools tox texlive-xetex
|
# python3-bs4 python3-setuptools tox texlive-xetex
|
||||||
script: tox -e py37-django22
|
# script: tox -e py37-django22
|
||||||
|
|
||||||
# Ubuntu 20.04
|
# Ubuntu 20.04
|
||||||
py38-django22:
|
py38-django22:
|
||||||
@ -38,12 +39,41 @@ py38-django22:
|
|||||||
python3-bs4 python3-setuptools tox texlive-xetex
|
python3-bs4 python3-setuptools tox texlive-xetex
|
||||||
script: tox -e py38-django22
|
script: tox -e py38-django22
|
||||||
|
|
||||||
|
# Debian Bullseye
|
||||||
|
py39-django22:
|
||||||
|
stage: test
|
||||||
|
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
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
# Be nice to new contributors, but please use `tox`
|
# Be nice to new contributors, but please use `tox`
|
||||||
allow_failure: true
|
allow_failure: true
|
||||||
|
|
||||||
|
# Compile documentation
|
||||||
|
documentation:
|
||||||
|
stage: docs
|
||||||
|
image: sphinxdoc/sphinx
|
||||||
|
before_script:
|
||||||
|
- pip install sphinx-rtd-theme
|
||||||
|
- cd docs
|
||||||
|
script:
|
||||||
|
- make dirhtml
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- docs/_build
|
||||||
|
expire_in: 1 day
|
||||||
|
2
.gitmodules
vendored
2
.gitmodules
vendored
@ -1,3 +1,3 @@
|
|||||||
[submodule "apps/scripts"]
|
[submodule "apps/scripts"]
|
||||||
path = apps/scripts
|
path = apps/scripts
|
||||||
url = https://gitlab.crans.org/bde/nk20-scripts.git
|
url = https://gitlab.crans.org/bde/nk20-scripts
|
||||||
|
39
README.md
39
README.md
@ -1,8 +1,8 @@
|
|||||||
# NoteKfet 2020
|
# NoteKfet 2020
|
||||||
|
|
||||||
[](https://www.gnu.org/licenses/gpl-3.0.txt)
|
[](https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
[](https://gitlab.crans.org/bde/nk20/commits/master)
|
[](https://gitlab.crans.org/bde/nk20/commits/main)
|
||||||
[](https://gitlab.crans.org/bde/nk20/commits/master)
|
[](https://gitlab.crans.org/bde/nk20/commits/main)
|
||||||
|
|
||||||
## Table des matières
|
## Table des matières
|
||||||
|
|
||||||
@ -55,7 +55,7 @@ Bien que cela permette de créer une instance sur toutes les distributions,
|
|||||||
(env)$ ./manage.py makemigrations
|
(env)$ ./manage.py makemigrations
|
||||||
(env)$ ./manage.py migrate
|
(env)$ ./manage.py migrate
|
||||||
(env)$ ./manage.py loaddata initial
|
(env)$ ./manage.py loaddata initial
|
||||||
(env)$ ./manage.py createsuperuser # Création d'un utilisateur initial
|
(env)$ ./manage.py createsuperuser # Création d'un⋅e utilisateur⋅rice initial
|
||||||
```
|
```
|
||||||
|
|
||||||
6. Enjoy :
|
6. Enjoy :
|
||||||
@ -69,13 +69,31 @@ accessible depuis l'ensemble de votre réseau, pratique pour tester le rendu
|
|||||||
de la note sur un téléphone !
|
de la note sur un téléphone !
|
||||||
|
|
||||||
## Installation d'une instance de production
|
## Installation d'une instance de production
|
||||||
|
Pour déployer facilement la note il est possible d'utiliser le playbook Ansible (sinon vous pouvez toujours le faire a la main, voir plus bas).
|
||||||
|
### Avec ansible
|
||||||
|
Il vous faudra un serveur sous debian ou ubuntu connecté à internet et que vous souhaiterez accéder à cette instance de la note sur `note.nomdedomaine.tld`.
|
||||||
|
|
||||||
|
0. Installer Ansible sur votre machine personnelle.
|
||||||
|
|
||||||
|
0. (bis) cloner le dépot sur votre machine personelle.
|
||||||
|
|
||||||
|
1. Copier le fichier `ansible/host_example`
|
||||||
|
``` bash
|
||||||
|
$ cp ansible/hosts_example ansible/hosts
|
||||||
|
```
|
||||||
|
et ajouter sous [dev] et/ou [prod] les serveurs sur lesquels vous souhaitez installer la note.
|
||||||
|
2. Créer un fichier `ansible/host_vars/<note.nomdedomaine.tld.yaml>` sur le modèle des fichiers existants dans `ansible/hosts` et compléter les variables nécessaires.
|
||||||
|
|
||||||
|
3. lancer `ansible/base.yaml -l <nomdedomaine.tld.yaml>`
|
||||||
|
4. Aller vous faire un café, ca peux durer un moment.
|
||||||
|
|
||||||
|
### Installation manuelle
|
||||||
|
|
||||||
**En production on souhaite absolument utiliser les modules Python packagées dans le gestionnaire de paquet.**
|
**En production on souhaite absolument utiliser les modules Python packagées dans le gestionnaire de paquet.**
|
||||||
Cela permet de mettre à jour facilement les dépendances critiques telles que Django.
|
Cela permet de mettre à jour facilement les dépendances critiques telles que Django.
|
||||||
|
|
||||||
L'installation d'une instance de production néccessite **une installation de Debian Buster ou d'Ubuntu 20.04**.
|
L'installation d'une instance de production néccessite **une installation de Debian Buster ou d'Ubuntu 20.04**.
|
||||||
|
|
||||||
Pour aller vite vous pouvez lancer le Playbook Ansible fournit dans ce dépôt en l'adaptant.
|
|
||||||
Sinon vous pouvez suivre les étapes décrites ci-dessous.
|
Sinon vous pouvez suivre les étapes décrites ci-dessous.
|
||||||
|
|
||||||
0. Sous Debian Buster, **activer Debian Backports.** En effet Django 2.2 LTS n'est que disponible dans les backports.
|
0. Sous Debian Buster, **activer Debian Backports.** En effet Django 2.2 LTS n'est que disponible dans les backports.
|
||||||
@ -261,20 +279,25 @@ 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
|
||||||
|
|
||||||
### Regénérer les fichiers de traduction
|
### Regénérer les fichiers de traduction
|
||||||
|
|
||||||
Pour regénérer les traductions vous pouvez vous placer à la racine du projet et lancer le script `makemessages`. Il faut penser à ignorer les dossiers ne contenant pas notre code, dont le virtualenv.
|
Pour regénérer les traductions vous pouvez vous placer à la racine du projet et lancer le script `makemessages`.
|
||||||
|
Il faut penser à ignorer les dossiers ne contenant pas notre code, dont le virtualenv.
|
||||||
|
De plus, il faut aussi extraire les variables des fichiers JavaScript.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
django-admin makemessages -i env
|
python3 manage.py makemessages -i env
|
||||||
|
python3 manage.py makemessages -i env -e js -d djangojs
|
||||||
```
|
```
|
||||||
|
|
||||||
Une fois les fichiers édités, vous pouvez compiler les nouvelles traductions avec
|
Une fois les fichiers édités, vous pouvez compiler les nouvelles traductions avec
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
django-admin compilemessages
|
python3 manage.py compilemessages
|
||||||
|
python3 manage.py compilejsmessages
|
||||||
```
|
```
|
||||||
|
@ -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: deb.debian.org
|
mirror: eclats.crans.org
|
||||||
roles:
|
roles:
|
||||||
- 1-apt-basic
|
- 1-apt-basic
|
||||||
- 2-nk20
|
- 2-nk20
|
||||||
@ -16,3 +16,4 @@
|
|||||||
- 5-nginx
|
- 5-nginx
|
||||||
- 6-psql
|
- 6-psql
|
||||||
- 7-postinstall
|
- 7-postinstall
|
||||||
|
- 8-docs
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
note:
|
|
||||||
server_name: note-beta.crans.org
|
|
||||||
git_branch: beta
|
|
||||||
cron_enabled: false
|
|
@ -2,4 +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
|
@ -1,5 +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
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
[dev]
|
[dev]
|
||||||
bde3-virt.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,13 +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_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
|
|
||||||
install_recommends: false
|
install_recommends: false
|
||||||
name:
|
name:
|
||||||
# Common tools
|
# Common tools
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
- name: Use default env vars (should be updated!)
|
- name: Use default env vars (should be updated!)
|
||||||
template:
|
template:
|
||||||
src: "env_example"
|
src: "env.j2"
|
||||||
dest: "/var/www/note_kfet/.env"
|
dest: "/var/www/note_kfet/.env"
|
||||||
mode: 0644
|
mode: 0644
|
||||||
force: false
|
force: false
|
||||||
@ -36,3 +36,13 @@
|
|||||||
dest: /etc/cron.d/note
|
dest: /etc/cron.d/note
|
||||||
owner: root
|
owner: root
|
||||||
group: root
|
group: root
|
||||||
|
|
||||||
|
- name: Set default directory to /var/www/note_kfet
|
||||||
|
lineinfile:
|
||||||
|
path: /etc/skel/.bashrc
|
||||||
|
line: 'cd /var/www/note_kfet'
|
||||||
|
|
||||||
|
- name: Automatically source Python virtual environment
|
||||||
|
lineinfile:
|
||||||
|
path: /etc/skel/.bashrc
|
||||||
|
line: 'source /var/www/note_kfet/env/bin/activate'
|
||||||
|
23
ansible/roles/2-nk20/templates/env.j2
Normal file
23
ansible/roles/2-nk20/templates/env.j2
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
DJANGO_APP_STAGE=prod
|
||||||
|
# Only used in dev mode, change to "postgresql" if you want to use PostgreSQL in dev
|
||||||
|
DJANGO_DEV_STORE_METHOD=sqlite
|
||||||
|
DJANGO_DB_HOST=localhost
|
||||||
|
DJANGO_DB_NAME=note_db
|
||||||
|
DJANGO_DB_USER=note
|
||||||
|
DJANGO_DB_PASSWORD={{ DB_PASSWORD }}
|
||||||
|
DJANGO_DB_PORT=
|
||||||
|
DJANGO_SECRET_KEY=CHANGE_ME
|
||||||
|
DJANGO_SETTINGS_MODULE=note_kfet.settings
|
||||||
|
CONTACT_EMAIL=tresorerie.bde@localhost
|
||||||
|
NOTE_URL= {{note.server_name}}
|
||||||
|
|
||||||
|
# Config for mails. Only used in production
|
||||||
|
NOTE_MAIL=notekfet@localhost
|
||||||
|
EMAIL_HOST=smtp.localhost
|
||||||
|
EMAIL_PORT=25
|
||||||
|
EMAIL_USER=notekfet@localhost
|
||||||
|
EMAIL_PASSWORD=CHANGE_ME
|
||||||
|
|
||||||
|
# Wiki configuration
|
||||||
|
WIKI_USER=NoteKfet2020
|
||||||
|
WIKI_PASSWORD=
|
@ -9,6 +9,11 @@
|
|||||||
retries: 3
|
retries: 3
|
||||||
until: pkg_result is succeeded
|
until: pkg_result is succeeded
|
||||||
|
|
||||||
|
- name: Check if certificate already exists.
|
||||||
|
stat:
|
||||||
|
path: /etc/letsencrypt/live/{{note.server_name}}/cert.pem
|
||||||
|
register: letsencrypt_cert
|
||||||
|
|
||||||
- name: Create /etc/letsencrypt/conf.d
|
- name: Create /etc/letsencrypt/conf.d
|
||||||
file:
|
file:
|
||||||
path: /etc/letsencrypt/conf.d
|
path: /etc/letsencrypt/conf.d
|
||||||
@ -19,3 +24,17 @@
|
|||||||
src: "letsencrypt/conf.d/nk20.ini.j2"
|
src: "letsencrypt/conf.d/nk20.ini.j2"
|
||||||
dest: "/etc/letsencrypt/conf.d/nk20.ini"
|
dest: "/etc/letsencrypt/conf.d/nk20.ini"
|
||||||
mode: 0644
|
mode: 0644
|
||||||
|
|
||||||
|
- name: Stop services to allow certbot to generate a cert.
|
||||||
|
service:
|
||||||
|
name: nginx
|
||||||
|
state: stopped
|
||||||
|
|
||||||
|
- name: Generate new certificate if one doesn't exist.
|
||||||
|
shell: "certbot certonly --non-interactive --agree-tos --config /etc/letsencrypt/conf.d/nk20.ini -d {{note.server_name}}"
|
||||||
|
when: letsencrypt_cert.stat.exists == False
|
||||||
|
|
||||||
|
- name: Restart services to allow certbot to generate a cert.
|
||||||
|
service:
|
||||||
|
name: nginx
|
||||||
|
state: started
|
||||||
|
@ -10,7 +10,7 @@ rsa-key-size = 4096
|
|||||||
# server = https://acme-staging.api.letsencrypt.org/directory
|
# server = https://acme-staging.api.letsencrypt.org/directory
|
||||||
|
|
||||||
# Uncomment and update to register with the specified e-mail address
|
# Uncomment and update to register with the specified e-mail address
|
||||||
email = notekfet2020@lists.crans.org
|
email = {{ note.email }}
|
||||||
|
|
||||||
# Uncomment to use a text interface instead of ncurses
|
# Uncomment to use a text interface instead of ncurses
|
||||||
text = True
|
text = True
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# the upstream component nginx needs to connect to
|
# the upstream component nginx needs to connect to
|
||||||
upstream note{
|
upstream note {
|
||||||
server unix:///var/www/note_kfet/note_kfet.sock; # file socket
|
server unix:///var/www/note_kfet/note_kfet.sock; # file socket
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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,11 @@ 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 {
|
||||||
|
alias /var/www/documentation; # The documentation of the project
|
||||||
|
}
|
||||||
|
|
||||||
# Finally, send all non-media requests to the Django server.
|
# Finally, send all non-media requests to the Django server.
|
||||||
location / {
|
location / {
|
||||||
uwsgi_pass note;
|
uwsgi_pass note;
|
||||||
|
@ -11,14 +11,14 @@
|
|||||||
until: pkg_result is succeeded
|
until: pkg_result is succeeded
|
||||||
|
|
||||||
- name: Create role note
|
- name: Create role note
|
||||||
when: "DB_PASSWORD|bool" # If the password is not defined, skip the installation
|
when: DB_PASSWORD|length > 0 # If the password is not defined, skip the installation
|
||||||
postgresql_user:
|
postgresql_user:
|
||||||
name: note
|
name: note
|
||||||
password: "{{ DB_PASSWORD }}"
|
password: "{{ DB_PASSWORD }}"
|
||||||
become_user: postgres
|
become_user: postgres
|
||||||
|
|
||||||
- name: Create NK20 database
|
- name: Create NK20 database
|
||||||
when: "DB_PASSWORD|bool"
|
when: DB_PASSWORD|length >0
|
||||||
postgresql_db:
|
postgresql_db:
|
||||||
name: note_db
|
name: note_db
|
||||||
owner: note
|
owner: note
|
||||||
|
@ -1,4 +1,10 @@
|
|||||||
---
|
---
|
||||||
|
- name: Collect static files
|
||||||
|
command: /var/www/note_kfet/env/bin/python manage.py collectstatic --noinput
|
||||||
|
args:
|
||||||
|
chdir: /var/www/note_kfet
|
||||||
|
become_user: www-data
|
||||||
|
|
||||||
- name: Migrate Django database
|
- name: Migrate Django database
|
||||||
command: /var/www/note_kfet/env/bin/python manage.py migrate
|
command: /var/www/note_kfet/env/bin/python manage.py migrate
|
||||||
args:
|
args:
|
||||||
@ -11,14 +17,14 @@
|
|||||||
chdir: /var/www/note_kfet
|
chdir: /var/www/note_kfet
|
||||||
become_user: www-data
|
become_user: www-data
|
||||||
|
|
||||||
|
- name: Compile JavaScript messages
|
||||||
|
command: /var/www/note_kfet/env/bin/python manage.py compilejsmessages
|
||||||
|
args:
|
||||||
|
chdir: /var/www/note_kfet
|
||||||
|
become_user: www-data
|
||||||
|
|
||||||
- name: Install initial fixtures
|
- name: Install initial fixtures
|
||||||
command: /var/www/note_kfet/env/bin/python manage.py loaddata initial
|
command: /var/www/note_kfet/env/bin/python manage.py loaddata initial
|
||||||
args:
|
args:
|
||||||
chdir: /var/www/note_kfet
|
chdir: /var/www/note_kfet
|
||||||
become_user: postgres
|
become_user: postgres
|
||||||
|
|
||||||
- name: Collect static files
|
|
||||||
command: /var/www/note_kfet/env/bin/python manage.py collectstatic --noinput
|
|
||||||
args:
|
|
||||||
chdir: /var/www/note_kfet
|
|
||||||
become_user: www-data
|
|
||||||
|
20
ansible/roles/8-docs/tasks/main.yml
Normal file
20
ansible/roles/8-docs/tasks/main.yml
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
- name: Install Sphinx and RTD theme
|
||||||
|
pip:
|
||||||
|
requirements: /var/www/note_kfet/docs/requirements.txt
|
||||||
|
virtualenv: /var/www/note_kfet/env
|
||||||
|
virtualenv_command: /usr/bin/python3 -m venv
|
||||||
|
virtualenv_site_packages: true
|
||||||
|
become_user: www-data
|
||||||
|
|
||||||
|
- name: Create documentation directory with good permissions
|
||||||
|
file:
|
||||||
|
path: /var/www/documentation
|
||||||
|
state: directory
|
||||||
|
owner: www-data
|
||||||
|
group: www-data
|
||||||
|
mode: u=rwx,g=rwxs,o=rx
|
||||||
|
|
||||||
|
- name: Build HTML documentation
|
||||||
|
command: /var/www/note_kfet/env/bin/sphinx-build -b dirhtml /var/www/note_kfet/docs/ /var/www/documentation/
|
||||||
|
become_user: www-data
|
@ -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,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 import admin
|
from django.contrib import admin
|
||||||
from note_kfet.admin import admin_site
|
from note_kfet.admin import admin_site
|
||||||
|
|
||||||
from .forms import GuestForm
|
from .forms import GuestForm
|
||||||
from .models import Activity, ActivityType, Entry, Guest
|
from .models import Activity, ActivityType, Entry, Guest, Opener
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Activity, site=admin_site)
|
@admin.register(Activity, site=admin_site)
|
||||||
@ -45,3 +45,11 @@ class EntryAdmin(admin.ModelAdmin):
|
|||||||
Admin customisation for Entry
|
Admin customisation for Entry
|
||||||
"""
|
"""
|
||||||
list_display = ('note', 'activity', 'time', 'guest')
|
list_display = ('note', 'activity', 'time', 'guest')
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Opener, site=admin_site)
|
||||||
|
class OpenerAdmin(admin.ModelAdmin):
|
||||||
|
"""
|
||||||
|
Admin customisation for Opener
|
||||||
|
"""
|
||||||
|
list_display = ('activity', 'opener')
|
||||||
|
@ -1,9 +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.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
from rest_framework.validators import UniqueTogetherValidator
|
||||||
|
|
||||||
from ..models import Activity, ActivityType, Entry, Guest, GuestTransaction
|
from ..models import Activity, ActivityType, Entry, Guest, GuestTransaction, Opener
|
||||||
|
|
||||||
|
|
||||||
class ActivityTypeSerializer(serializers.ModelSerializer):
|
class ActivityTypeSerializer(serializers.ModelSerializer):
|
||||||
@ -59,3 +61,17 @@ class GuestTransactionSerializer(serializers.ModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = GuestTransaction
|
model = GuestTransaction
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
|
|
||||||
|
|
||||||
|
class OpenerSerializer(serializers.ModelSerializer):
|
||||||
|
"""
|
||||||
|
REST API Serializer for Openers.
|
||||||
|
The djangorestframework plugin will analyse the model `Opener` and parse all fields in the API.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Opener
|
||||||
|
fields = '__all__'
|
||||||
|
validators = [UniqueTogetherValidator(
|
||||||
|
queryset=Opener.objects.all(), fields=("opener", "activity"),
|
||||||
|
message=_("This opener already exists"))]
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# Copyright (C) 2018-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, OpenerViewSet
|
||||||
|
|
||||||
|
|
||||||
def register_activity_urls(router, path):
|
def register_activity_urls(router, path):
|
||||||
@ -12,3 +12,4 @@ def register_activity_urls(router, path):
|
|||||||
router.register(path + '/type', ActivityTypeViewSet)
|
router.register(path + '/type', ActivityTypeViewSet)
|
||||||
router.register(path + '/guest', GuestViewSet)
|
router.register(path + '/guest', GuestViewSet)
|
||||||
router.register(path + '/entry', EntryViewSet)
|
router.register(path + '/entry', EntryViewSet)
|
||||||
|
router.register(path + '/opener', OpenerViewSet)
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
# Copyright (C) 2018-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.filters import RegexSafeSearchFilter
|
||||||
from api.viewsets import ReadProtectedModelViewSet
|
from api.viewsets import ReadProtectedModelViewSet
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
from rest_framework.filters import SearchFilter
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status
|
||||||
|
|
||||||
from .serializers import ActivitySerializer, ActivityTypeSerializer, EntrySerializer, GuestSerializer
|
from .serializers import ActivitySerializer, ActivityTypeSerializer, EntrySerializer, GuestSerializer, OpenerSerializer
|
||||||
from ..models import Activity, ActivityType, Entry, Guest
|
from ..models import Activity, ActivityType, Entry, Guest, Opener
|
||||||
|
|
||||||
|
|
||||||
class ActivityTypeViewSet(ReadProtectedModelViewSet):
|
class ActivityTypeViewSet(ReadProtectedModelViewSet):
|
||||||
@ -15,10 +18,10 @@ class ActivityTypeViewSet(ReadProtectedModelViewSet):
|
|||||||
The djangorestframework plugin will get all `ActivityType` objects, serialize it to JSON with the given serializer,
|
The djangorestframework plugin will get all `ActivityType` objects, serialize it to JSON with the given serializer,
|
||||||
then render it on /api/activity/type/
|
then render it on /api/activity/type/
|
||||||
"""
|
"""
|
||||||
queryset = ActivityType.objects.all()
|
queryset = ActivityType.objects.order_by('id')
|
||||||
serializer_class = ActivityTypeSerializer
|
serializer_class = ActivityTypeSerializer
|
||||||
filter_backends = [DjangoFilterBackend]
|
filter_backends = [DjangoFilterBackend]
|
||||||
filterset_fields = ['name', 'can_invite', ]
|
filterset_fields = ['name', 'manage_entries', 'can_invite', 'guest_entry_fee', ]
|
||||||
|
|
||||||
|
|
||||||
class ActivityViewSet(ReadProtectedModelViewSet):
|
class ActivityViewSet(ReadProtectedModelViewSet):
|
||||||
@ -27,10 +30,16 @@ class ActivityViewSet(ReadProtectedModelViewSet):
|
|||||||
The djangorestframework plugin will get all `Activity` objects, serialize it to JSON with the given serializer,
|
The djangorestframework plugin will get all `Activity` objects, serialize it to JSON with the given serializer,
|
||||||
then render it on /api/activity/activity/
|
then render it on /api/activity/activity/
|
||||||
"""
|
"""
|
||||||
queryset = Activity.objects.all()
|
queryset = Activity.objects.order_by('id')
|
||||||
serializer_class = ActivitySerializer
|
serializer_class = ActivitySerializer
|
||||||
filter_backends = [DjangoFilterBackend]
|
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||||
filterset_fields = ['name', 'description', 'activity_type', ]
|
filterset_fields = ['name', 'description', 'activity_type', 'location', 'creater', 'organizer', 'attendees_club',
|
||||||
|
'date_start', 'date_end', 'valid', 'open', ]
|
||||||
|
search_fields = ['$name', '$description', '$location', '$creater__last_name', '$creater__first_name',
|
||||||
|
'$creater__email', '$creater__note__alias__name', '$creater__note__alias__normalized_name',
|
||||||
|
'$organizer__name', '$organizer__email', '$organizer__note__alias__name',
|
||||||
|
'$organizer__note__alias__normalized_name', '$attendees_club__name', '$attendees_club__email',
|
||||||
|
'$attendees_club__note__alias__name', '$attendees_club__note__alias__normalized_name', ]
|
||||||
|
|
||||||
|
|
||||||
class GuestViewSet(ReadProtectedModelViewSet):
|
class GuestViewSet(ReadProtectedModelViewSet):
|
||||||
@ -39,10 +48,13 @@ class GuestViewSet(ReadProtectedModelViewSet):
|
|||||||
The djangorestframework plugin will get all `Guest` objects, serialize it to JSON with the given serializer,
|
The djangorestframework plugin will get all `Guest` objects, serialize it to JSON with the given serializer,
|
||||||
then render it on /api/activity/guest/
|
then render it on /api/activity/guest/
|
||||||
"""
|
"""
|
||||||
queryset = Guest.objects.all()
|
queryset = Guest.objects.order_by('id')
|
||||||
serializer_class = GuestSerializer
|
serializer_class = GuestSerializer
|
||||||
filter_backends = [SearchFilter]
|
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||||
search_fields = ['$last_name', '$first_name', '$inviter__alias__name', '$inviter__alias__normalized_name', ]
|
filterset_fields = ['activity', 'activity__name', 'last_name', 'first_name', 'inviter', 'inviter__alias__name',
|
||||||
|
'inviter__alias__normalized_name', ]
|
||||||
|
search_fields = ['$activity__name', '$last_name', '$first_name', '$inviter__user__email', '$inviter__alias__name',
|
||||||
|
'$inviter__alias__normalized_name', ]
|
||||||
|
|
||||||
|
|
||||||
class EntryViewSet(ReadProtectedModelViewSet):
|
class EntryViewSet(ReadProtectedModelViewSet):
|
||||||
@ -51,7 +63,38 @@ class EntryViewSet(ReadProtectedModelViewSet):
|
|||||||
The djangorestframework plugin will get all `Entry` objects, serialize it to JSON with the given serializer,
|
The djangorestframework plugin will get all `Entry` objects, serialize it to JSON with the given serializer,
|
||||||
then render it on /api/activity/entry/
|
then render it on /api/activity/entry/
|
||||||
"""
|
"""
|
||||||
queryset = Entry.objects.all()
|
queryset = Entry.objects.order_by('id')
|
||||||
serializer_class = EntrySerializer
|
serializer_class = EntrySerializer
|
||||||
filter_backends = [SearchFilter]
|
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||||
search_fields = ['$last_name', '$first_name', '$inviter__alias__name', '$inviter__alias__normalized_name', ]
|
filterset_fields = ['activity', 'time', 'note', 'guest', ]
|
||||||
|
search_fields = ['$activity__name', '$note__user__email', '$note__alias__name', '$note__alias__normalized_name',
|
||||||
|
'$guest__last_name', '$guest__first_name', ]
|
||||||
|
|
||||||
|
|
||||||
|
class OpenerViewSet(ReadProtectedModelViewSet):
|
||||||
|
"""
|
||||||
|
REST Opener View set.
|
||||||
|
The djangorestframework plugin will get all `Opener` objects, serialize it to JSON with the given serializer,
|
||||||
|
then render it on /api/activity/opener/
|
||||||
|
"""
|
||||||
|
queryset = Opener.objects
|
||||||
|
serializer_class = OpenerSerializer
|
||||||
|
filter_backends = [RegexSafeSearchFilter, DjangoFilterBackend]
|
||||||
|
search_fields = ['$opener__alias__name', '$opener__alias__normalized_name',
|
||||||
|
'$activity__name']
|
||||||
|
filterset_fields = ['opener', 'opener__noteuser__user', 'activity']
|
||||||
|
|
||||||
|
def get_serializer_class(self):
|
||||||
|
serializer_class = self.serializer_class
|
||||||
|
if self.request.method in ['PUT', 'PATCH']:
|
||||||
|
# opener-activity can't change
|
||||||
|
serializer_class.Meta.read_only_fields = ('opener', 'acitivity',)
|
||||||
|
return serializer_class
|
||||||
|
|
||||||
|
def destroy(self, request, *args, **kwargs):
|
||||||
|
instance = self.get_object()
|
||||||
|
try:
|
||||||
|
self.perform_destroy(instance)
|
||||||
|
except ValidationError as e:
|
||||||
|
return Response({e.code: str(e)}, status.HTTP_400_BAD_REQUEST)
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-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"]
|
||||||
@ -37,7 +43,7 @@ class ActivityForm(forms.ModelForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Activity
|
model = Activity
|
||||||
exclude = ('creater', 'valid', 'open', )
|
exclude = ('creater', 'valid', 'open', 'opener', )
|
||||||
widgets = {
|
widgets = {
|
||||||
"organizer": Autocomplete(
|
"organizer": Autocomplete(
|
||||||
model=Club,
|
model=Club,
|
||||||
|
18
apps/activity/migrations/0003_auto_20240323_1422.py
Normal file
18
apps/activity/migrations/0003_auto_20240323_1422.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 2.2.28 on 2024-03-23 13:22
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('activity', '0002_auto_20200904_2341'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='activity',
|
||||||
|
name='description',
|
||||||
|
field=models.TextField(blank=True, default='', verbose_name='description'),
|
||||||
|
),
|
||||||
|
]
|
28
apps/activity/migrations/0004_opener.py
Normal file
28
apps/activity/migrations/0004_opener.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# Generated by Django 2.2.28 on 2024-08-01 12:36
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('note', '0006_trust'),
|
||||||
|
('activity', '0003_auto_20240323_1422'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Opener',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('activity', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='opener', to='activity.Activity', verbose_name='activity')),
|
||||||
|
('opener', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='activity_responsible', to='note.Note', verbose_name='opener')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'opener',
|
||||||
|
'verbose_name_plural': 'openers',
|
||||||
|
'unique_together': {('opener', 'activity')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-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
|
||||||
@ -11,7 +11,7 @@ from django.db import models, transaction
|
|||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from note.models import NoteUser, Transaction
|
from note.models import NoteUser, Transaction, Note
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
@ -66,6 +66,8 @@ class Activity(models.Model):
|
|||||||
|
|
||||||
description = models.TextField(
|
description = models.TextField(
|
||||||
verbose_name=_('description'),
|
verbose_name=_('description'),
|
||||||
|
blank=True,
|
||||||
|
default="",
|
||||||
)
|
)
|
||||||
|
|
||||||
location = models.CharField(
|
location = models.CharField(
|
||||||
@ -123,6 +125,14 @@ class Activity(models.Model):
|
|||||||
verbose_name=_('open'),
|
verbose_name=_('open'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("activity")
|
||||||
|
verbose_name_plural = _("activities")
|
||||||
|
unique_together = ("name", "date_start", "date_end",)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
@ -144,14 +154,6 @@ class Activity(models.Model):
|
|||||||
if settings.DATABASES["default"]["ENGINE"] == 'django.db.backends.postgresql' else refresh_activities()
|
if settings.DATABASES["default"]["ENGINE"] == 'django.db.backends.postgresql' else refresh_activities()
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _("activity")
|
|
||||||
verbose_name_plural = _("activities")
|
|
||||||
unique_together = ("name", "date_start", "date_end",)
|
|
||||||
|
|
||||||
|
|
||||||
class Entry(models.Model):
|
class Entry(models.Model):
|
||||||
"""
|
"""
|
||||||
@ -252,14 +254,13 @@ class Guest(models.Model):
|
|||||||
verbose_name=_("inviter"),
|
verbose_name=_("inviter"),
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
class Meta:
|
||||||
def has_entry(self):
|
verbose_name = _("guest")
|
||||||
try:
|
verbose_name_plural = _("guests")
|
||||||
if self.entry:
|
unique_together = ("activity", "last_name", "first_name", )
|
||||||
return True
|
|
||||||
return False
|
def __str__(self):
|
||||||
except AttributeError:
|
return self.first_name + " " + self.last_name
|
||||||
return False
|
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
|
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
|
||||||
@ -290,13 +291,14 @@ class Guest(models.Model):
|
|||||||
|
|
||||||
return super().save(force_insert, force_update, using, update_fields)
|
return super().save(force_insert, force_update, using, update_fields)
|
||||||
|
|
||||||
def __str__(self):
|
@property
|
||||||
return self.first_name + " " + self.last_name
|
def has_entry(self):
|
||||||
|
try:
|
||||||
class Meta:
|
if self.entry:
|
||||||
verbose_name = _("guest")
|
return True
|
||||||
verbose_name_plural = _("guests")
|
return False
|
||||||
unique_together = ("activity", "last_name", "first_name", )
|
except AttributeError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class GuestTransaction(Transaction):
|
class GuestTransaction(Transaction):
|
||||||
@ -308,3 +310,31 @@ class GuestTransaction(Transaction):
|
|||||||
@property
|
@property
|
||||||
def type(self):
|
def type(self):
|
||||||
return _('Invitation')
|
return _('Invitation')
|
||||||
|
|
||||||
|
|
||||||
|
class Opener(models.Model):
|
||||||
|
"""
|
||||||
|
Allow the user to make activity entries without more rights
|
||||||
|
"""
|
||||||
|
activity = models.ForeignKey(
|
||||||
|
Activity,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='opener',
|
||||||
|
verbose_name=_('activity')
|
||||||
|
)
|
||||||
|
|
||||||
|
opener = models.ForeignKey(
|
||||||
|
Note,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='activity_responsible',
|
||||||
|
verbose_name=_('Opener')
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Opener")
|
||||||
|
verbose_name_plural = _("Openers")
|
||||||
|
unique_together = ("opener", "activity")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return _("{opener} is opener of activity {acivity}").format(
|
||||||
|
opener=str(self.opener), acivity=str(self.activity))
|
||||||
|
57
apps/activity/static/activity/js/opener.js
Normal file
57
apps/activity/static/activity/js/opener.js
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
/**
|
||||||
|
* On form submit, add a new opener
|
||||||
|
*/
|
||||||
|
function form_create_opener (e) {
|
||||||
|
// Do not submit HTML form
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
// Get data and send to API
|
||||||
|
const formData = new FormData(e.target)
|
||||||
|
$.getJSON('/api/note/alias/'+formData.get('opener') + '/',
|
||||||
|
function (opener_alias) {
|
||||||
|
create_opener(formData.get('activity'), opener_alias.note)
|
||||||
|
}).fail(function (xhr, _textStatus, _error) {
|
||||||
|
errMsg(xhr.responseJSON)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an opener between an activity and a user
|
||||||
|
* @param activity:Integer activity id
|
||||||
|
* @param opener:Integer user note id
|
||||||
|
*/
|
||||||
|
function create_opener(activity, opener) {
|
||||||
|
$.post('/api/activity/opener/', {
|
||||||
|
activity: activity,
|
||||||
|
opener: opener,
|
||||||
|
csrfmiddlewaretoken: CSRF_TOKEN
|
||||||
|
}).done(function () {
|
||||||
|
// Reload tables
|
||||||
|
$('#opener_table').load(location.pathname + ' #opener_table')
|
||||||
|
addMsg(gettext('Opener successfully added'), 'success')
|
||||||
|
}).fail(function (xhr, _textStatus, _error) {
|
||||||
|
errMsg(xhr.responseJSON)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On click of "delete", delete the opener
|
||||||
|
* @param button_id:Integer Opener id to remove
|
||||||
|
*/
|
||||||
|
function delete_button (button_id) {
|
||||||
|
$.ajax({
|
||||||
|
url: '/api/activity/opener/' + button_id + '/',
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: { 'X-CSRFTOKEN': CSRF_TOKEN }
|
||||||
|
}).done(function () {
|
||||||
|
addMsg(gettext('Opener successfully deleted'), 'success')
|
||||||
|
$('#opener_table').load(location.pathname + ' #opener_table')
|
||||||
|
}).fail(function (xhr, _textStatus, _error) {
|
||||||
|
errMsg(xhr.responseJSON)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).ready(function () {
|
||||||
|
// Attach event
|
||||||
|
document.getElementById('form_opener').addEventListener('submit', form_create_opener)
|
||||||
|
})
|
@ -1,13 +1,17 @@
|
|||||||
# 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 _
|
||||||
|
from note_kfet.middlewares import get_current_request
|
||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
from django_tables2 import A
|
from django_tables2 import A
|
||||||
|
from permission.backends import PermissionBackend
|
||||||
from note.templatetags.pretty_money import pretty_money
|
from note.templatetags.pretty_money import pretty_money
|
||||||
|
|
||||||
from .models import Activity, Entry, Guest
|
from .models import Activity, Entry, Guest, Opener
|
||||||
|
|
||||||
|
|
||||||
class ActivityTable(tables.Table):
|
class ActivityTable(tables.Table):
|
||||||
@ -52,8 +56,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 +95,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):
|
||||||
@ -111,3 +115,34 @@ class EntryTable(tables.Table):
|
|||||||
'data-last-name': lambda record: record.last_name,
|
'data-last-name': lambda record: record.last_name,
|
||||||
'data-first-name': lambda record: record.first_name,
|
'data-first-name': lambda record: record.first_name,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# function delete_button(id) provided in template file
|
||||||
|
DELETE_TEMPLATE = """
|
||||||
|
<button id="{{ record.pk }}" class="btn btn-danger btn-sm" onclick="delete_button(this.id)"> {{ delete_trans }}</button>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class OpenerTable(tables.Table):
|
||||||
|
class Meta:
|
||||||
|
attrs = {
|
||||||
|
'class': 'table table condensed table-striped',
|
||||||
|
'id': "opener_table"
|
||||||
|
}
|
||||||
|
model = Opener
|
||||||
|
fields = ("opener",)
|
||||||
|
template_name = 'django_tables2/bootstrap4.html'
|
||||||
|
|
||||||
|
show_header = False
|
||||||
|
opener = tables.Column(attrs={'td': {'class': 'text-center'}})
|
||||||
|
|
||||||
|
delete_col = tables.TemplateColumn(
|
||||||
|
template_code=DELETE_TEMPLATE,
|
||||||
|
extra_context={"delete_trans": _('Delete')},
|
||||||
|
attrs={
|
||||||
|
'td': {
|
||||||
|
'class': lambda record: 'col-sm-1'
|
||||||
|
+ (' d-none' if not PermissionBackend.check_perm(
|
||||||
|
get_current_request(), "activity.delete_opener", record)
|
||||||
|
else '')}},
|
||||||
|
verbose_name=_("Delete"),)
|
||||||
|
@ -4,11 +4,31 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
{% load i18n perms %}
|
{% load i18n perms %}
|
||||||
{% load render_table from django_tables2 %}
|
{% load render_table from django_tables2 %}
|
||||||
|
{% load static django_tables2 i18n %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1 class="text-white">{{ title }}</h1>
|
<h1 class="text-white">{{ title }}</h1>
|
||||||
{% include "activity/includes/activity_info.html" %}
|
{% include "activity/includes/activity_info.html" %}
|
||||||
|
|
||||||
|
{% if activity.activity_type.manage_entries and ".change__opener"|has_perm:activity %}
|
||||||
|
<div class="card bg-white mb-3">
|
||||||
|
<h3 class="card-header text-center">
|
||||||
|
{% trans "Openers" %}
|
||||||
|
</h3>
|
||||||
|
<div class="card-body">
|
||||||
|
<form class="input-group" method="POST" id="form_opener">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="activity" value="{{ object.pk }}">
|
||||||
|
{%include "autocomplete_model.html" %}
|
||||||
|
<div class="input-group-append">
|
||||||
|
<input type="submit" class="btn btn-success" value="{% trans "Add" %}">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% render_table opener %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if guests.data %}
|
{% if guests.data %}
|
||||||
<div class="card bg-white mb-3">
|
<div class="card bg-white mb-3">
|
||||||
<h3 class="card-header text-center">
|
<h3 class="card-header text-center">
|
||||||
@ -22,6 +42,8 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extrajavascript %}
|
{% block extrajavascript %}
|
||||||
|
<script src="{% static "activity/js/opener.js" %}"></script>
|
||||||
|
<script src="{% static "js/autocomplete_model.js" %}"></script>
|
||||||
<script>
|
<script>
|
||||||
function remove_guest(guest_id) {
|
function remove_guest(guest_id) {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
@ -30,7 +52,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
headers: {"X-CSRFTOKEN": CSRF_TOKEN}
|
headers: {"X-CSRFTOKEN": CSRF_TOKEN}
|
||||||
})
|
})
|
||||||
.done(function() {
|
.done(function() {
|
||||||
addMsg('Invité supprimé','success');
|
addMsg('{% trans "Guest deleted" %}', 'success');
|
||||||
$("#guests_table").load(location.pathname + " #guests_table");
|
$("#guests_table").load(location.pathname + " #guests_table");
|
||||||
})
|
})
|
||||||
.fail(function(xhr, textStatus, error) {
|
.fail(function(xhr, textStatus, error) {
|
||||||
|
@ -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);
|
||||||
|
|
||||||
@ -86,10 +91,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
}).done(function () {
|
}).done(function () {
|
||||||
if (target.hasClass("table-info"))
|
if (target.hasClass("table-info"))
|
||||||
addMsg(
|
addMsg(
|
||||||
"Entrée effectuée, mais attention : la personne n'est plus adhérente Kfet.",
|
"{% trans "Entry done, but caution: the user is not a Kfet member." %}",
|
||||||
"warning", 10000);
|
"warning", 10000);
|
||||||
else
|
else
|
||||||
addMsg("Entrée effectuée !", "success", 4000);
|
addMsg("Entry made!", "success", 4000);
|
||||||
reloadTable(true);
|
reloadTable(true);
|
||||||
}).fail(function (xhr) {
|
}).fail(function (xhr) {
|
||||||
errMsg(xhr.responseJSON, 4000);
|
errMsg(xhr.responseJSON, 4000);
|
||||||
@ -121,10 +126,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
}).done(function () {
|
}).done(function () {
|
||||||
if (target.hasClass("table-info"))
|
if (target.hasClass("table-info"))
|
||||||
addMsg(
|
addMsg(
|
||||||
"Entrée effectuée, mais attention : la personne n'est plus adhérente Kfet.",
|
"{% trans "Entry done, but caution: the user is not a Kfet member." %}",
|
||||||
"warning", 10000);
|
"warning", 10000);
|
||||||
else
|
else
|
||||||
addMsg("Entrée effectuée !", "success", 4000);
|
addMsg("{% trans "Entry done!" %}", "success", 4000);
|
||||||
reloadTable(true);
|
reloadTable(true);
|
||||||
}).fail(function (xhr) {
|
}).fail(function (xhr) {
|
||||||
errMsg(xhr.responseJSON, 4000);
|
errMsg(xhr.responseJSON, 4000);
|
||||||
|
@ -17,4 +17,27 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extrajavascript %}
|
||||||
|
<script>
|
||||||
|
var date_end = document.getElementById("id_date_end");
|
||||||
|
var date_start = document.getElementById("id_date_start");
|
||||||
|
|
||||||
|
function update_date_end (){
|
||||||
|
if(date_end.value=="" || date_end.value<date_start.value){
|
||||||
|
date_end.value = date_start.value;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function update_date_start (){
|
||||||
|
if(date_start.value=="" || date_end.value<date_start.value){
|
||||||
|
date_start.value = date_end.value;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
date_start.addEventListener('focusout', update_date_end);
|
||||||
|
date_end.addEventListener('focusout', update_date_start);
|
||||||
|
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
@ -46,4 +46,4 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
</h3>
|
</h3>
|
||||||
{% render_table table %}
|
{% render_table table %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -1,15 +1,18 @@
|
|||||||
# 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
|
||||||
|
|
||||||
|
from api.tests import TestAPI
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from activity.models import Activity, ActivityType, Guest, Entry
|
|
||||||
from member.models import Club
|
from member.models import Club
|
||||||
|
|
||||||
|
from ..api.views import ActivityTypeViewSet, ActivityViewSet, EntryViewSet, GuestViewSet
|
||||||
|
from ..models import Activity, ActivityType, Guest, Entry
|
||||||
|
|
||||||
|
|
||||||
class TestActivities(TestCase):
|
class TestActivities(TestCase):
|
||||||
"""
|
"""
|
||||||
@ -173,3 +176,58 @@ class TestActivities(TestCase):
|
|||||||
"""
|
"""
|
||||||
response = self.client.get(reverse("activity:calendar_ics"))
|
response = self.client.get(reverse("activity:calendar_ics"))
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
|
class TestActivityAPI(TestAPI):
|
||||||
|
def setUp(self) -> None:
|
||||||
|
super().setUp()
|
||||||
|
|
||||||
|
self.activity = Activity.objects.create(
|
||||||
|
name="Activity",
|
||||||
|
description="This is a test activity\non two very very long lines\nbecause this is very important.",
|
||||||
|
location="Earth",
|
||||||
|
activity_type=ActivityType.objects.get(name="Pot"),
|
||||||
|
creater=self.user,
|
||||||
|
organizer=Club.objects.get(name="Kfet"),
|
||||||
|
attendees_club=Club.objects.get(name="Kfet"),
|
||||||
|
date_start=timezone.now(),
|
||||||
|
date_end=timezone.now() + timedelta(days=2),
|
||||||
|
valid=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.guest = Guest.objects.create(
|
||||||
|
activity=self.activity,
|
||||||
|
inviter=self.user.note,
|
||||||
|
last_name="GUEST",
|
||||||
|
first_name="Guest",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.entry = Entry.objects.create(
|
||||||
|
activity=self.activity,
|
||||||
|
note=self.user.note,
|
||||||
|
guest=self.guest,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_activity_api(self):
|
||||||
|
"""
|
||||||
|
Load Activity API page and test all filters and permissions
|
||||||
|
"""
|
||||||
|
self.check_viewset(ActivityViewSet, "/api/activity/activity/")
|
||||||
|
|
||||||
|
def test_activity_type_api(self):
|
||||||
|
"""
|
||||||
|
Load ActivityType API page and test all filters and permissions
|
||||||
|
"""
|
||||||
|
self.check_viewset(ActivityTypeViewSet, "/api/activity/type/")
|
||||||
|
|
||||||
|
def test_entry_api(self):
|
||||||
|
"""
|
||||||
|
Load Entry API page and test all filters and permissions
|
||||||
|
"""
|
||||||
|
self.check_viewset(EntryViewSet, "/api/activity/entry/")
|
||||||
|
|
||||||
|
def test_guest_api(self):
|
||||||
|
"""
|
||||||
|
Load Guest API page and test all filters and permissions
|
||||||
|
"""
|
||||||
|
self.check_viewset(GuestViewSet, "/api/activity/guest/")
|
||||||
|
@ -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
|
||||||
@ -17,14 +17,16 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from django.views import View
|
from django.views import View
|
||||||
from django.views.decorators.cache import cache_page
|
from django.views.decorators.cache import cache_page
|
||||||
from django.views.generic import DetailView, TemplateView, UpdateView
|
from django.views.generic import DetailView, TemplateView, UpdateView
|
||||||
from django_tables2.views import SingleTableView
|
from django.views.generic.list import ListView
|
||||||
|
from django_tables2.views import MultiTableMixin, SingleTableMixin
|
||||||
|
from api.viewsets import is_regex
|
||||||
from note.models import Alias, NoteSpecial, NoteUser
|
from note.models import Alias, NoteSpecial, NoteUser
|
||||||
from permission.backends import PermissionBackend
|
from permission.backends import PermissionBackend
|
||||||
from permission.views import ProtectQuerysetMixin, ProtectedCreateView
|
from permission.views import ProtectQuerysetMixin, ProtectedCreateView
|
||||||
|
|
||||||
from .forms import ActivityForm, GuestForm
|
from .forms import ActivityForm, GuestForm
|
||||||
from .models import Activity, Entry, Guest
|
from .models import Activity, Entry, Guest, Opener
|
||||||
from .tables import ActivityTable, EntryTable, GuestTable
|
from .tables import ActivityTable, EntryTable, GuestTable, OpenerTable
|
||||||
|
|
||||||
|
|
||||||
class ActivityCreateView(ProtectQuerysetMixin, ProtectedCreateView):
|
class ActivityCreateView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||||
@ -57,36 +59,44 @@ class ActivityCreateView(ProtectQuerysetMixin, ProtectedCreateView):
|
|||||||
return reverse_lazy('activity:activity_detail', kwargs={"pk": self.object.pk})
|
return reverse_lazy('activity:activity_detail', kwargs={"pk": self.object.pk})
|
||||||
|
|
||||||
|
|
||||||
class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, ListView):
|
||||||
"""
|
"""
|
||||||
Displays all Activities, and classify if they are on-going or upcoming ones.
|
Displays all Activities, and classify if they are on-going or upcoming ones.
|
||||||
"""
|
"""
|
||||||
model = Activity
|
model = Activity
|
||||||
table_class = ActivityTable
|
tables = [
|
||||||
ordering = ('-date_start',)
|
lambda data: ActivityTable(data, prefix="all-"),
|
||||||
|
lambda data: ActivityTable(data, prefix="upcoming-"),
|
||||||
|
]
|
||||||
extra_context = {"title": _("Activities")}
|
extra_context = {"title": _("Activities")}
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self, **kwargs):
|
||||||
return super().get_queryset().distinct()
|
return super().get_queryset(**kwargs).distinct()
|
||||||
|
|
||||||
|
def get_tables_data(self):
|
||||||
|
# first table = all activities, second table = upcoming
|
||||||
|
return [
|
||||||
|
self.get_queryset().order_by("-date_start"),
|
||||||
|
Activity.objects.filter(date_end__gt=timezone.now())
|
||||||
|
.filter(PermissionBackend.filter_queryset(self.request, Activity, "view"))
|
||||||
|
.distinct()
|
||||||
|
.order_by("date_start")
|
||||||
|
]
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
upcoming_activities = Activity.objects.filter(date_end__gt=timezone.now())
|
tables = context["tables"]
|
||||||
context['upcoming'] = ActivityTable(
|
for name, table in zip(["table", "upcoming"], tables):
|
||||||
data=upcoming_activities.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view")),
|
context[name] = table
|
||||||
prefix='upcoming-',
|
|
||||||
)
|
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, DetailView):
|
||||||
"""
|
"""
|
||||||
Shows details about one activity. Add guest to context
|
Shows details about one activity. Add guest to context
|
||||||
"""
|
"""
|
||||||
@ -94,15 +104,40 @@ class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
|||||||
context_object_name = "activity"
|
context_object_name = "activity"
|
||||||
extra_context = {"title": _("Activity detail")}
|
extra_context = {"title": _("Activity detail")}
|
||||||
|
|
||||||
|
tables = [
|
||||||
|
lambda data: GuestTable(data, prefix="guests-"),
|
||||||
|
lambda data: OpenerTable(data, prefix="opener-"),
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_tables_data(self):
|
||||||
|
return [
|
||||||
|
Guest.objects.filter(activity=self.object)
|
||||||
|
.filter(PermissionBackend.filter_queryset(self.request, Guest, "view")),
|
||||||
|
self.object.opener.filter(activity=self.object)
|
||||||
|
.filter(PermissionBackend.filter_queryset(self.request, Opener, "view")),
|
||||||
|
]
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data()
|
context = super().get_context_data()
|
||||||
|
|
||||||
table = GuestTable(data=Guest.objects.filter(activity=self.object)
|
tables = context["tables"]
|
||||||
.filter(PermissionBackend.filter_queryset(self.request.user, Guest, "view")))
|
for name, table in zip(["guests", "opener"], tables):
|
||||||
context["guests"] = table
|
context[name] = table
|
||||||
|
|
||||||
context["activity_started"] = timezone.now() > timezone.localtime(self.object.date_start)
|
context["activity_started"] = timezone.now() > timezone.localtime(self.object.date_start)
|
||||||
|
|
||||||
|
context["widget"] = {
|
||||||
|
"name": "opener",
|
||||||
|
"resetable": True,
|
||||||
|
"attrs": {
|
||||||
|
"class": "autocomplete form-control",
|
||||||
|
"id": "opener",
|
||||||
|
"api_url": "/api/note/alias/?note__polymorphic_ctype__model=noteuser",
|
||||||
|
"name_field": "name",
|
||||||
|
"placeholder": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
@ -144,36 +179,41 @@ 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):
|
||||||
return reverse_lazy('activity:activity_detail', kwargs={"pk": self.kwargs["pk"]})
|
return reverse_lazy('activity:activity_detail', kwargs={"pk": self.kwargs["pk"]})
|
||||||
|
|
||||||
|
|
||||||
class ActivityEntryView(LoginRequiredMixin, TemplateView):
|
class ActivityEntryView(LoginRequiredMixin, SingleTableMixin, TemplateView):
|
||||||
"""
|
"""
|
||||||
Manages entry to an activity
|
Manages entry to an activity
|
||||||
"""
|
"""
|
||||||
template_name = "activity/activity_entry.html"
|
template_name = "activity/activity_entry.html"
|
||||||
|
|
||||||
|
table_class = EntryTable
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Don't display the entry interface if the user has no right to see it (no right to add an entry for itself),
|
Don't display the entry interface if the user has no right to see it (no right to add an entry for itself),
|
||||||
it is closed or doesn't manage entries.
|
it is closed or doesn't manage entries.
|
||||||
"""
|
"""
|
||||||
|
if not self.request.user.is_authenticated:
|
||||||
|
return self.handle_no_permission()
|
||||||
|
|
||||||
activity = Activity.objects.get(pk=self.kwargs["pk"])
|
activity = Activity.objects.get(pk=self.kwargs["pk"])
|
||||||
|
|
||||||
sample_entry = Entry(activity=activity, note=self.request.user.note)
|
sample_entry = Entry(activity=activity, note=self.request.user.note)
|
||||||
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,22 +231,25 @@ 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"]
|
||||||
if pattern[0] != "^":
|
|
||||||
pattern = "^" + pattern
|
# Check if this is a valid regex. If not, we won't check regex
|
||||||
|
valid_regex = is_regex(pattern)
|
||||||
|
suffix = "__iregex" if valid_regex else "__istartswith"
|
||||||
|
pattern = "^" + pattern if valid_regex and pattern[0] != "^" else pattern
|
||||||
guest_qs = guest_qs.filter(
|
guest_qs = guest_qs.filter(
|
||||||
Q(first_name__iregex=pattern)
|
Q(**{f"first_name{suffix}": pattern})
|
||||||
| Q(last_name__iregex=pattern)
|
| Q(**{f"last_name{suffix}": pattern})
|
||||||
| Q(inviter__alias__name__iregex=pattern)
|
| Q(**{f"inviter__alias__name{suffix}": pattern})
|
||||||
| Q(inviter__alias__normalized_name__iregex=Alias.normalize(pattern))
|
| Q(**{f"inviter__alias__normalized_name{suffix}": Alias.normalize(pattern)})
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
guest_qs = guest_qs.none()
|
guest_qs = guest_qs.none()
|
||||||
return guest_qs
|
return guest_qs.distinct()
|
||||||
|
|
||||||
def get_invited_note(self, activity):
|
def get_invited_note(self, activity):
|
||||||
"""
|
"""
|
||||||
@ -230,15 +273,19 @@ 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"]
|
||||||
|
|
||||||
|
# Check if this is a valid regex. If not, we won't check regex
|
||||||
|
valid_regex = is_regex(pattern)
|
||||||
|
suffix = "__iregex" if valid_regex else "__icontains"
|
||||||
note_qs = note_qs.filter(
|
note_qs = note_qs.filter(
|
||||||
Q(note__noteuser__user__first_name__iregex=pattern)
|
Q(**{f"note__noteuser__user__first_name{suffix}": pattern})
|
||||||
| Q(note__noteuser__user__last_name__iregex=pattern)
|
| Q(**{f"note__noteuser__user__last_name{suffix}": pattern})
|
||||||
| Q(name__iregex=pattern)
|
| Q(**{f"name{suffix}": pattern})
|
||||||
| Q(normalized_name__iregex=Alias.normalize(pattern))
|
| Q(**{f"normalized_name{suffix}": Alias.normalize(pattern)})
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
note_qs = note_qs.none()
|
note_qs = note_qs.none()
|
||||||
@ -250,15 +297,9 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
|
|||||||
if settings.DATABASES[note_qs.db]["ENGINE"] == 'django.db.backends.postgresql' else note_qs.distinct()[:20]
|
if settings.DATABASES[note_qs.db]["ENGINE"] == 'django.db.backends.postgresql' else note_qs.distinct()[:20]
|
||||||
return note_qs
|
return note_qs
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_table_data(self):
|
||||||
"""
|
activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request, Activity, "view"))\
|
||||||
Query the list of Guest and Note to the activity and add information to makes entry with JS.
|
|
||||||
"""
|
|
||||||
context = super().get_context_data(**kwargs)
|
|
||||||
|
|
||||||
activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view"))\
|
|
||||||
.distinct().get(pk=self.kwargs["pk"])
|
.distinct().get(pk=self.kwargs["pk"])
|
||||||
context["activity"] = activity
|
|
||||||
|
|
||||||
matched = []
|
matched = []
|
||||||
|
|
||||||
@ -271,8 +312,17 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
|
|||||||
note.activity = activity
|
note.activity = activity
|
||||||
matched.append(note)
|
matched.append(note)
|
||||||
|
|
||||||
table = EntryTable(data=matched)
|
return matched
|
||||||
context["table"] = table
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
"""
|
||||||
|
Query the list of Guest and Note to the activity and add information to makes entry with JS.
|
||||||
|
"""
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request, Activity, "view"))\
|
||||||
|
.distinct().get(pk=self.kwargs["pk"])
|
||||||
|
context["activity"] = activity
|
||||||
|
|
||||||
context["entries"] = Entry.objects.filter(activity=activity)
|
context["entries"] = Entry.objects.filter(activity=activity)
|
||||||
|
|
||||||
@ -281,9 +331,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,))]
|
||||||
|
|
||||||
@ -314,8 +364,8 @@ X-WR-CALNAME:Kfet Calendar
|
|||||||
NAME:Kfet Calendar
|
NAME:Kfet Calendar
|
||||||
CALSCALE:GREGORIAN
|
CALSCALE:GREGORIAN
|
||||||
BEGIN:VTIMEZONE
|
BEGIN:VTIMEZONE
|
||||||
TZID:Europe/Berlin
|
TZID:Europe/Paris
|
||||||
X-LIC-LOCATION:Europe/Berlin
|
X-LIC-LOCATION:Europe/Paris
|
||||||
BEGIN:DAYLIGHT
|
BEGIN:DAYLIGHT
|
||||||
TZOFFSETFROM:+0100
|
TZOFFSETFROM:+0100
|
||||||
TZOFFSETTO:+0200
|
TZOFFSETTO:+0200
|
||||||
@ -337,10 +387,10 @@ END:VTIMEZONE
|
|||||||
DTSTAMP:{"{:%Y%m%dT%H%M%S}".format(activity.date_start)}Z
|
DTSTAMP:{"{:%Y%m%dT%H%M%S}".format(activity.date_start)}Z
|
||||||
UID:{md5((activity.name + "$" + str(activity.id) + str(activity.date_start)).encode("UTF-8")).hexdigest()}
|
UID:{md5((activity.name + "$" + str(activity.id) + str(activity.date_start)).encode("UTF-8")).hexdigest()}
|
||||||
SUMMARY;CHARSET=UTF-8:{self.multilines(activity.name, 75, 22)}
|
SUMMARY;CHARSET=UTF-8:{self.multilines(activity.name, 75, 22)}
|
||||||
DTSTART;TZID=Europe/Berlin:{"{:%Y%m%dT%H%M%S}".format(activity.date_start)}
|
DTSTART:{"{:%Y%m%dT%H%M%S}Z".format(activity.date_start)}
|
||||||
DTEND;TZID=Europe/Berlin:{"{:%Y%m%dT%H%M%S}".format(activity.date_end)}
|
DTEND:{"{:%Y%m%dT%H%M%S}Z".format(activity.date_end)}
|
||||||
LOCATION:{self.multilines(activity.location, 75, 9) if activity.location else "Kfet"}
|
LOCATION:{self.multilines(activity.location, 75, 9) if activity.location else "Kfet"}
|
||||||
DESCRIPTION;CHARSET=UTF-8:""" + self.multilines(activity.description.replace("\n", "\\n"), 75, 26) + """
|
DESCRIPTION;CHARSET=UTF-8:""" + self.multilines(activity.description.replace("\n", "\\n"), 75, 26) + f"""
|
||||||
-- {activity.organizer.name}
|
-- {activity.organizer.name}
|
||||||
END:VEVENT
|
END:VEVENT
|
||||||
"""
|
"""
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-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
|
||||||
|
42
apps/api/filters.py
Normal file
42
apps/api/filters.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import re
|
||||||
|
from functools import lru_cache
|
||||||
|
|
||||||
|
from rest_framework.filters import SearchFilter
|
||||||
|
|
||||||
|
|
||||||
|
class RegexSafeSearchFilter(SearchFilter):
|
||||||
|
@lru_cache
|
||||||
|
def validate_regex(self, search_term) -> bool:
|
||||||
|
try:
|
||||||
|
re.compile(search_term)
|
||||||
|
return True
|
||||||
|
except re.error:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_search_fields(self, view, request):
|
||||||
|
"""
|
||||||
|
Ensure that given regex are valid.
|
||||||
|
If not, we consider that the user is trying to search by substring.
|
||||||
|
"""
|
||||||
|
search_fields = super().get_search_fields(view, request)
|
||||||
|
search_terms = self.get_search_terms(request)
|
||||||
|
|
||||||
|
for search_term in search_terms:
|
||||||
|
if not self.validate_regex(search_term):
|
||||||
|
# Invalid regex. We assume we don't query by regex but by substring.
|
||||||
|
search_fields = [f.replace('$', '') for f in search_fields]
|
||||||
|
break
|
||||||
|
|
||||||
|
return search_fields
|
||||||
|
|
||||||
|
def get_search_terms(self, request):
|
||||||
|
"""
|
||||||
|
Ensure that search field is a valid regex query. If not, we remove extra characters.
|
||||||
|
"""
|
||||||
|
terms = super().get_search_terms(request)
|
||||||
|
if not all(self.validate_regex(term) for term in terms):
|
||||||
|
# Invalid regex. If a ^ is prefixed to the search term, we remove it.
|
||||||
|
terms = [term[1:] if term[0] == '^' else term for term in terms]
|
||||||
|
# Same for dollars.
|
||||||
|
terms = [term[:-1] if term[-1] == '$' else term for term in terms]
|
||||||
|
return terms
|
5
apps/api/pagination.py
Normal file
5
apps/api/pagination.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from rest_framework.pagination import PageNumberPagination
|
||||||
|
|
||||||
|
|
||||||
|
class CustomPagination(PageNumberPagination):
|
||||||
|
page_size_query_param = 'page_size'
|
@ -1,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',
|
||||||
|
)
|
||||||
|
241
apps/api/tests.py
Normal file
241
apps/api/tests.py
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
import json
|
||||||
|
from datetime import datetime, date
|
||||||
|
from decimal import Decimal
|
||||||
|
from urllib.parse import quote_plus
|
||||||
|
from warnings import warn
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.db.models.fields.files import ImageFieldFile
|
||||||
|
from django.test import TestCase
|
||||||
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
|
from phonenumbers import PhoneNumber
|
||||||
|
from rest_framework.filters import OrderingFilter
|
||||||
|
from api.filters import RegexSafeSearchFilter
|
||||||
|
from member.models import Membership, Club
|
||||||
|
from note.models import NoteClub, NoteUser, Alias, Note
|
||||||
|
from permission.models import PermissionMask, Permission, Role
|
||||||
|
|
||||||
|
from .viewsets import ContentTypeViewSet, UserViewSet
|
||||||
|
|
||||||
|
|
||||||
|
class TestAPI(TestCase):
|
||||||
|
"""
|
||||||
|
Load API pages and check that filters are working.
|
||||||
|
"""
|
||||||
|
fixtures = ('initial', )
|
||||||
|
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.user = User.objects.create_superuser(
|
||||||
|
username="adminapi",
|
||||||
|
password="adminapi",
|
||||||
|
email="adminapi@example.com",
|
||||||
|
last_name="Admin",
|
||||||
|
first_name="Admin",
|
||||||
|
)
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
sess = self.client.session
|
||||||
|
sess["permission_mask"] = 42
|
||||||
|
sess.save()
|
||||||
|
|
||||||
|
def check_viewset(self, viewset, url):
|
||||||
|
"""
|
||||||
|
This function should be called inside a unit test.
|
||||||
|
This loads the viewset and for each filter entry, it checks that the filter is running good.
|
||||||
|
"""
|
||||||
|
resp = self.client.get(url + "?format=json")
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
model = viewset.serializer_class.Meta.model
|
||||||
|
|
||||||
|
if not model.objects.exists(): # pragma: no cover
|
||||||
|
warn(f"Warning: unable to test API filters for the model {model._meta.verbose_name} "
|
||||||
|
"since there is no instance of it.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if hasattr(viewset, "filter_backends"):
|
||||||
|
backends = viewset.filter_backends
|
||||||
|
obj = model.objects.last()
|
||||||
|
|
||||||
|
if DjangoFilterBackend in backends:
|
||||||
|
# Specific search
|
||||||
|
for field in viewset.filterset_fields:
|
||||||
|
obj = self.fix_note_object(obj, field)
|
||||||
|
|
||||||
|
value = self.get_value(obj, field)
|
||||||
|
if value is None: # pragma: no cover
|
||||||
|
warn(f"Warning: the filter {field} for the model {model._meta.verbose_name} "
|
||||||
|
"has not been tested.")
|
||||||
|
continue
|
||||||
|
resp = self.client.get(url + f"?format=json&{field}={quote_plus(str(value))}")
|
||||||
|
self.assertEqual(resp.status_code, 200, f"The filter {field} for the model "
|
||||||
|
f"{model._meta.verbose_name} does not work. "
|
||||||
|
f"Given parameter: {value}")
|
||||||
|
content = json.loads(resp.content)
|
||||||
|
self.assertGreater(content["count"], 0, f"The filter {field} for the model "
|
||||||
|
f"{model._meta.verbose_name} does not work. "
|
||||||
|
f"Given parameter: {value}")
|
||||||
|
|
||||||
|
if OrderingFilter in backends:
|
||||||
|
# Ensure that ordering is working well
|
||||||
|
for field in viewset.ordering_fields:
|
||||||
|
resp = self.client.get(url + f"?ordering={field}")
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
resp = self.client.get(url + f"?ordering=-{field}")
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
if RegexSafeSearchFilter in backends:
|
||||||
|
# Basic search
|
||||||
|
for field in viewset.search_fields:
|
||||||
|
obj = self.fix_note_object(obj, field)
|
||||||
|
|
||||||
|
if field[0] == '$' or field[0] == '=':
|
||||||
|
field = field[1:]
|
||||||
|
value = self.get_value(obj, field)
|
||||||
|
if value is None: # pragma: no cover
|
||||||
|
warn(f"Warning: the filter {field} for the model {model._meta.verbose_name} "
|
||||||
|
"has not been tested.")
|
||||||
|
continue
|
||||||
|
resp = self.client.get(url + f"?format=json&search={quote_plus(str(value))}")
|
||||||
|
self.assertEqual(resp.status_code, 200, f"The filter {field} for the model "
|
||||||
|
f"{model._meta.verbose_name} does not work. "
|
||||||
|
f"Given parameter: {value}")
|
||||||
|
content = json.loads(resp.content)
|
||||||
|
self.assertGreater(content["count"], 0, f"The filter {field} for the model "
|
||||||
|
f"{model._meta.verbose_name} does not work. "
|
||||||
|
f"Given parameter: {value}")
|
||||||
|
|
||||||
|
self.check_permissions(url, obj)
|
||||||
|
|
||||||
|
def check_permissions(self, url, obj):
|
||||||
|
"""
|
||||||
|
Check that permissions are working
|
||||||
|
"""
|
||||||
|
# Drop rights
|
||||||
|
self.user.is_superuser = False
|
||||||
|
self.user.save()
|
||||||
|
sess = self.client.session
|
||||||
|
sess["permission_mask"] = 0
|
||||||
|
sess.save()
|
||||||
|
|
||||||
|
# Delete user permissions
|
||||||
|
for m in Membership.objects.filter(user=self.user).all():
|
||||||
|
m.roles.clear()
|
||||||
|
m.save()
|
||||||
|
|
||||||
|
# Create a new role, which will have the checking permission
|
||||||
|
role = Role.objects.get_or_create(name="β-tester")[0]
|
||||||
|
role.permissions.clear()
|
||||||
|
role.save()
|
||||||
|
membership = Membership.objects.get_or_create(user=self.user, club=Club.objects.get(name="BDE"))[0]
|
||||||
|
membership.roles.set([role])
|
||||||
|
membership.save()
|
||||||
|
|
||||||
|
# Ensure that the access to the object is forbidden without permission
|
||||||
|
resp = self.client.get(url + f"{obj.pk}/")
|
||||||
|
self.assertEqual(resp.status_code, 404, f"Mysterious access to {url}{obj.pk}/ for {obj}")
|
||||||
|
|
||||||
|
obj.refresh_from_db()
|
||||||
|
|
||||||
|
# There are problems with polymorphism
|
||||||
|
if isinstance(obj, Note) and hasattr(obj, "note_ptr"):
|
||||||
|
obj = obj.note_ptr
|
||||||
|
|
||||||
|
mask = PermissionMask.objects.get(rank=0)
|
||||||
|
|
||||||
|
for field in obj._meta.fields:
|
||||||
|
# Build permission query
|
||||||
|
value = self.get_value(obj, field.name)
|
||||||
|
if isinstance(value, date) or isinstance(value, datetime):
|
||||||
|
value = value.isoformat()
|
||||||
|
elif isinstance(value, ImageFieldFile):
|
||||||
|
value = value.name
|
||||||
|
elif isinstance(value, Decimal):
|
||||||
|
value = str(value)
|
||||||
|
query = json.dumps({field.name: value})
|
||||||
|
|
||||||
|
# Create sample permission
|
||||||
|
permission = Permission.objects.get_or_create(
|
||||||
|
model=ContentType.objects.get_for_model(obj._meta.model),
|
||||||
|
query=query,
|
||||||
|
mask=mask,
|
||||||
|
type="view",
|
||||||
|
permanent=False,
|
||||||
|
description=f"Can view {obj._meta.verbose_name}",
|
||||||
|
)[0]
|
||||||
|
role.permissions.set([permission])
|
||||||
|
role.save()
|
||||||
|
|
||||||
|
# Check that the access is possible
|
||||||
|
resp = self.client.get(url + f"{obj.pk}/")
|
||||||
|
self.assertEqual(resp.status_code, 200, f"Permission {permission.query} is not working "
|
||||||
|
f"for the model {obj._meta.verbose_name}")
|
||||||
|
|
||||||
|
# Restore rights
|
||||||
|
self.user.is_superuser = True
|
||||||
|
self.user.save()
|
||||||
|
sess = self.client.session
|
||||||
|
sess["permission_mask"] = 42
|
||||||
|
sess.save()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_value(obj, key: str):
|
||||||
|
"""
|
||||||
|
Resolve the queryset filter to get the Python value of an object.
|
||||||
|
"""
|
||||||
|
if hasattr(obj, "all"):
|
||||||
|
# obj is a RelatedManager
|
||||||
|
obj = obj.last()
|
||||||
|
|
||||||
|
if obj is None: # pragma: no cover
|
||||||
|
return None
|
||||||
|
|
||||||
|
if '__' not in key:
|
||||||
|
obj = getattr(obj, key)
|
||||||
|
if hasattr(obj, "pk"):
|
||||||
|
return obj.pk
|
||||||
|
elif hasattr(obj, "all"):
|
||||||
|
if not obj.exists(): # pragma: no cover
|
||||||
|
return None
|
||||||
|
return obj.last().pk
|
||||||
|
elif isinstance(obj, bool):
|
||||||
|
return int(obj)
|
||||||
|
elif isinstance(obj, datetime):
|
||||||
|
return obj.isoformat()
|
||||||
|
elif isinstance(obj, PhoneNumber):
|
||||||
|
return obj.raw_input
|
||||||
|
return obj
|
||||||
|
|
||||||
|
key, remaining = key.split('__', 1)
|
||||||
|
return TestAPI.get_value(getattr(obj, key), remaining)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def fix_note_object(obj, field):
|
||||||
|
"""
|
||||||
|
When querying an object that has a noteclub or a noteuser field,
|
||||||
|
ensure that the object has a good value.
|
||||||
|
"""
|
||||||
|
if isinstance(obj, Alias):
|
||||||
|
if "noteuser" in field:
|
||||||
|
return NoteUser.objects.last().alias.last()
|
||||||
|
elif "noteclub" in field:
|
||||||
|
return NoteClub.objects.last().alias.last()
|
||||||
|
elif isinstance(obj, Note):
|
||||||
|
if "noteuser" in field:
|
||||||
|
return NoteUser.objects.last()
|
||||||
|
elif "noteclub" in field:
|
||||||
|
return NoteClub.objects.last()
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
class TestBasicAPI(TestAPI):
|
||||||
|
def test_user_api(self):
|
||||||
|
"""
|
||||||
|
Load the user page.
|
||||||
|
"""
|
||||||
|
self.check_viewset(ContentTypeViewSet, "/api/models/")
|
||||||
|
self.check_viewset(UserViewSet, "/api/user/")
|
@ -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,6 +1,8 @@
|
|||||||
# 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.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
@ -8,12 +10,20 @@ from django.conf import settings
|
|||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
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 .filters import RegexSafeSearchFilter
|
||||||
from .serializers import UserSerializer, ContentTypeSerializer
|
from .serializers import UserSerializer, ContentTypeSerializer
|
||||||
|
|
||||||
|
|
||||||
|
def is_regex(pattern):
|
||||||
|
try:
|
||||||
|
re.compile(pattern)
|
||||||
|
return True
|
||||||
|
except (re.error, TypeError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class ReadProtectedModelViewSet(ModelViewSet):
|
class ReadProtectedModelViewSet(ModelViewSet):
|
||||||
"""
|
"""
|
||||||
Protect a ModelViewSet by filtering the objects that the user cannot see.
|
Protect a ModelViewSet by filtering the objects that the user cannot see.
|
||||||
@ -24,9 +34,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):
|
||||||
@ -39,21 +47,20 @@ 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):
|
||||||
"""
|
"""
|
||||||
REST API View set.
|
REST API View set.
|
||||||
The djangorestframework plugin will get all `User` objects, serialize it to JSON with the given serializer,
|
The djangorestframework plugin will get all `User` objects, serialize it to JSON with the given serializer,
|
||||||
then render it on /api/users/
|
then render it on /api/user/
|
||||||
"""
|
"""
|
||||||
queryset = User.objects.all()
|
queryset = User.objects
|
||||||
serializer_class = UserSerializer
|
serializer_class = UserSerializer
|
||||||
filter_backends = [DjangoFilterBackend]
|
filter_backends = [DjangoFilterBackend]
|
||||||
filterset_fields = ['id', 'username', 'first_name', 'last_name', 'email', 'is_superuser', 'is_staff', 'is_active', ]
|
filterset_fields = ['id', 'username', 'first_name', 'last_name', 'email', 'is_superuser', 'is_staff', 'is_active',
|
||||||
|
'note__alias__name', 'note__alias__normalized_name', ]
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
queryset = super().get_queryset()
|
queryset = super().get_queryset()
|
||||||
@ -63,34 +70,38 @@ class UserViewSet(ReadProtectedModelViewSet):
|
|||||||
|
|
||||||
if "search" in self.request.GET:
|
if "search" in self.request.GET:
|
||||||
pattern = self.request.GET["search"]
|
pattern = self.request.GET["search"]
|
||||||
|
# Check if this is a valid regex. If not, we won't check regex
|
||||||
|
valid_regex = is_regex(pattern)
|
||||||
|
suffix = "__iregex" if valid_regex else "__istartswith"
|
||||||
|
prefix = "^" if valid_regex else ""
|
||||||
|
|
||||||
# Filter with different rules
|
# Filter with different rules
|
||||||
# We use union-all to keep each filter rule sorted in result
|
# We use union-all to keep each filter rule sorted in result
|
||||||
queryset = queryset.filter(
|
queryset = queryset.filter(
|
||||||
# Match without normalization
|
# Match without normalization
|
||||||
note__alias__name__iregex="^" + pattern
|
Q(**{f"note__alias__name{suffix}": prefix + pattern})
|
||||||
).union(
|
).union(
|
||||||
queryset.filter(
|
queryset.filter(
|
||||||
# Match with normalization
|
# Match with normalization
|
||||||
Q(note__alias__normalized_name__iregex="^" + Alias.normalize(pattern))
|
Q(**{f"note__alias__normalized_name{suffix}": prefix + Alias.normalize(pattern)})
|
||||||
& ~Q(note__alias__name__iregex="^" + pattern)
|
& ~Q(**{f"note__alias__name{suffix}": prefix + pattern})
|
||||||
),
|
),
|
||||||
all=True,
|
all=True,
|
||||||
).union(
|
).union(
|
||||||
queryset.filter(
|
queryset.filter(
|
||||||
# Match on lower pattern
|
# Match on lower pattern
|
||||||
Q(note__alias__normalized_name__iregex="^" + pattern.lower())
|
Q(**{f"note__alias__normalized_name{suffix}": prefix + pattern.lower()})
|
||||||
& ~Q(note__alias__normalized_name__iregex="^" + Alias.normalize(pattern))
|
& ~Q(**{f"note__alias__normalized_name{suffix}": prefix + Alias.normalize(pattern)})
|
||||||
& ~Q(note__alias__name__iregex="^" + pattern)
|
& ~Q(**{f"note__alias__name{suffix}": prefix + pattern})
|
||||||
),
|
),
|
||||||
all=True,
|
all=True,
|
||||||
).union(
|
).union(
|
||||||
queryset.filter(
|
queryset.filter(
|
||||||
# Match on firstname or lastname
|
# Match on firstname or lastname
|
||||||
(Q(last_name__iregex="^" + pattern) | Q(first_name__iregex="^" + pattern))
|
(Q(**{f"last_name{suffix}": prefix + pattern}) | Q(**{f"first_name{suffix}": prefix + pattern}))
|
||||||
& ~Q(note__alias__normalized_name__iregex="^" + pattern.lower())
|
& ~Q(**{f"note__alias__normalized_name{suffix}": prefix + pattern.lower()})
|
||||||
& ~Q(note__alias__normalized_name__iregex="^" + Alias.normalize(pattern))
|
& ~Q(**{f"note__alias__normalized_name{suffix}": prefix + Alias.normalize(pattern)})
|
||||||
& ~Q(note__alias__name__iregex="^" + pattern)
|
& ~Q(**{f"note__alias__name{suffix}": prefix + pattern})
|
||||||
),
|
),
|
||||||
all=True,
|
all=True,
|
||||||
)
|
)
|
||||||
@ -106,7 +117,10 @@ class ContentTypeViewSet(ReadOnlyModelViewSet):
|
|||||||
"""
|
"""
|
||||||
REST API View set.
|
REST API View set.
|
||||||
The djangorestframework plugin will get all `User` objects, serialize it to JSON with the given serializer,
|
The djangorestframework plugin will get all `User` objects, serialize it to JSON with the given serializer,
|
||||||
then render it on /api/users/
|
then render it on /api/models/
|
||||||
"""
|
"""
|
||||||
queryset = ContentType.objects.all()
|
queryset = ContentType.objects.order_by('id')
|
||||||
serializer_class = ContentTypeSerializer
|
serializer_class = ContentTypeSerializer
|
||||||
|
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||||
|
filterset_fields = ['id', 'app_label', 'model', ]
|
||||||
|
search_fields = ['$app_label', '$model', ]
|
||||||
|
@ -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
|
||||||
@ -15,7 +15,7 @@ class ChangelogViewSet(ReadOnlyProtectedModelViewSet):
|
|||||||
The djangorestframework plugin will get all `Changelog` objects, serialize it to JSON with the given serializer,
|
The djangorestframework plugin will get all `Changelog` objects, serialize it to JSON with the given serializer,
|
||||||
then render it on /api/logs/
|
then render it on /api/logs/
|
||||||
"""
|
"""
|
||||||
queryset = Changelog.objects.all()
|
queryset = Changelog.objects.order_by('id')
|
||||||
serializer_class = ChangelogSerializer
|
serializer_class = ChangelogSerializer
|
||||||
filter_backends = [DjangoFilterBackend, OrderingFilter]
|
filter_backends = [DjangoFilterBackend, OrderingFilter]
|
||||||
filterset_fields = ['model', 'action', "instance_pk", 'user', 'ip', ]
|
filterset_fields = ['model', 'action', "instance_pk", 'user', 'ip', ]
|
||||||
|
@ -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
|
||||||
|
|
||||||
@ -56,13 +56,13 @@ def save_object(sender, instance, **kwargs):
|
|||||||
# noinspection PyProtectedMember
|
# noinspection PyProtectedMember
|
||||||
previous = instance._previous
|
previous = instance._previous
|
||||||
|
|
||||||
# Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP
|
# Si un⋅e utilisateur⋅rice est connecté⋅e, on récupère l'utilisateur⋅rice courant⋅e ainsi que son adresse IP
|
||||||
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⋅rice dans la VM doit être un des alias note du respo info
|
||||||
ip = "127.0.0.1"
|
ip = "127.0.0.1"
|
||||||
username = Alias.normalize(getpass.getuser())
|
username = Alias.normalize(getpass.getuser())
|
||||||
note = NoteUser.objects.filter(alias__normalized_name=username)
|
note = NoteUser.objects.filter(alias__normalized_name=username)
|
||||||
@ -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
|
||||||
@ -120,13 +134,13 @@ def delete_object(sender, instance, **kwargs):
|
|||||||
if instance._meta.label_lower in EXCLUDED or hasattr(instance, "_no_signal"):
|
if instance._meta.label_lower in EXCLUDED or hasattr(instance, "_no_signal"):
|
||||||
return
|
return
|
||||||
|
|
||||||
# Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP
|
# Si un⋅e utilisateur⋅rice est connecté⋅e, on récupère l'utilisateur⋅rice courant⋅e ainsi que son adresse IP
|
||||||
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⋅rice dans la VM doit être un des alias note du respo info
|
||||||
ip = "127.0.0.1"
|
ip = "127.0.0.1"
|
||||||
username = Alias.normalize(getpass.getuser())
|
username = Alias.normalize(getpass.getuser())
|
||||||
note = NoteUser.objects.filter(alias__normalized_name=username)
|
note = NoteUser.objects.filter(alias__normalized_name=username)
|
||||||
@ -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,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 rest_framework.filters import SearchFilter
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
|
from rest_framework.filters import OrderingFilter
|
||||||
|
from api.filters import RegexSafeSearchFilter
|
||||||
from api.viewsets import ReadProtectedModelViewSet
|
from api.viewsets import ReadProtectedModelViewSet
|
||||||
|
|
||||||
from .serializers import ProfileSerializer, ClubSerializer, MembershipSerializer
|
from .serializers import ProfileSerializer, ClubSerializer, MembershipSerializer
|
||||||
@ -14,8 +16,15 @@ class ProfileViewSet(ReadProtectedModelViewSet):
|
|||||||
The djangorestframework plugin will get all `Profile` objects, serialize it to JSON with the given serializer,
|
The djangorestframework plugin will get all `Profile` objects, serialize it to JSON with the given serializer,
|
||||||
then render it on /api/members/profile/
|
then render it on /api/members/profile/
|
||||||
"""
|
"""
|
||||||
queryset = Profile.objects.all()
|
queryset = Profile.objects.order_by('id')
|
||||||
serializer_class = ProfileSerializer
|
serializer_class = ProfileSerializer
|
||||||
|
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||||
|
filterset_fields = ['user', 'user__first_name', 'user__last_name', 'user__username', 'user__email',
|
||||||
|
'user__note__alias__name', 'user__note__alias__normalized_name', 'phone_number', "section",
|
||||||
|
'department', 'promotion', 'address', 'paid', 'ml_events_registration', 'ml_sport_registration',
|
||||||
|
'ml_art_registration', 'report_frequency', 'email_confirmed', 'registration_valid', ]
|
||||||
|
search_fields = ['$user__first_name', '$user__last_name', '$user__username', '$user__email',
|
||||||
|
'$user__note__alias__name', '$user__note__alias__normalized_name', ]
|
||||||
|
|
||||||
|
|
||||||
class ClubViewSet(ReadProtectedModelViewSet):
|
class ClubViewSet(ReadProtectedModelViewSet):
|
||||||
@ -24,10 +33,13 @@ class ClubViewSet(ReadProtectedModelViewSet):
|
|||||||
The djangorestframework plugin will get all `Club` objects, serialize it to JSON with the given serializer,
|
The djangorestframework plugin will get all `Club` objects, serialize it to JSON with the given serializer,
|
||||||
then render it on /api/members/club/
|
then render it on /api/members/club/
|
||||||
"""
|
"""
|
||||||
queryset = Club.objects.all()
|
queryset = Club.objects.order_by('id')
|
||||||
serializer_class = ClubSerializer
|
serializer_class = ClubSerializer
|
||||||
filter_backends = [SearchFilter]
|
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||||
search_fields = ['$name', ]
|
filterset_fields = ['name', 'email', 'note__alias__name', 'note__alias__normalized_name', 'parent_club',
|
||||||
|
'parent_club__name', 'require_memberships', 'membership_fee_paid', 'membership_fee_unpaid',
|
||||||
|
'membership_duration', 'membership_start', 'membership_end', ]
|
||||||
|
search_fields = ['$name', '$email', '$note__alias__name', '$note__alias__normalized_name', ]
|
||||||
|
|
||||||
|
|
||||||
class MembershipViewSet(ReadProtectedModelViewSet):
|
class MembershipViewSet(ReadProtectedModelViewSet):
|
||||||
@ -36,5 +48,14 @@ class MembershipViewSet(ReadProtectedModelViewSet):
|
|||||||
The djangorestframework plugin will get all `Membership` objects, serialize it to JSON with the given serializer,
|
The djangorestframework plugin will get all `Membership` objects, serialize it to JSON with the given serializer,
|
||||||
then render it on /api/members/membership/
|
then render it on /api/members/membership/
|
||||||
"""
|
"""
|
||||||
queryset = Membership.objects.all()
|
queryset = Membership.objects.order_by('id')
|
||||||
serializer_class = MembershipSerializer
|
serializer_class = MembershipSerializer
|
||||||
|
filter_backends = [DjangoFilterBackend, OrderingFilter, RegexSafeSearchFilter]
|
||||||
|
filterset_fields = ['club__name', 'club__email', 'club__note__alias__name', 'club__note__alias__normalized_name',
|
||||||
|
'user__username', 'user__last_name', 'user__first_name', 'user__email',
|
||||||
|
'user__note__alias__name', 'user__note__alias__normalized_name',
|
||||||
|
'date_start', 'date_end', 'fee', 'roles', ]
|
||||||
|
ordering_fields = ['id', 'date_start', 'date_end', ]
|
||||||
|
search_fields = ['$club__name', '$club__email', '$club__note__alias__name', '$club__note__alias__normalized_name',
|
||||||
|
'$user__username', '$user__last_name', '$user__first_name', '$user__email',
|
||||||
|
'$user__note__alias__name', '$user__note__alias__normalized_name', '$roles__name', ]
|
||||||
|
@ -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(),
|
||||||
@ -200,9 +210,9 @@ class MembershipForm(forms.ModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Membership
|
model = Membership
|
||||||
fields = ('user', 'date_start')
|
fields = ('user', 'date_start')
|
||||||
# Le champ d'utilisateur est remplacé par un champ d'auto-complétion.
|
# Le champ d'utilisateur⋅rice est remplacé par un champ d'auto-complétion.
|
||||||
# Quand des lettres sont tapées, une requête est envoyée sur l'API d'auto-complétion
|
# Quand des lettres sont tapées, une requête est envoyée sur l'API d'auto-complétion
|
||||||
# et récupère les noms d'utilisateur valides
|
# et récupère les noms d'utilisateur⋅rices valides
|
||||||
widgets = {
|
widgets = {
|
||||||
'user':
|
'user':
|
||||||
Autocomplete(
|
Autocomplete(
|
||||||
|
@ -1,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(
|
||||||
|
@ -0,0 +1,50 @@
|
|||||||
|
import sys
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def give_note_account_permissions(apps, schema_editor):
|
||||||
|
"""
|
||||||
|
Automatically manage the membership of the Note account.
|
||||||
|
"""
|
||||||
|
User = apps.get_model("auth", "user")
|
||||||
|
Membership = apps.get_model("member", "membership")
|
||||||
|
Role = apps.get_model("permission", "role")
|
||||||
|
|
||||||
|
note = User.objects.filter(username="note")
|
||||||
|
if not note.exists():
|
||||||
|
# We are in a test environment, don't log error message
|
||||||
|
if len(sys.argv) > 1 and sys.argv[1] == 'test':
|
||||||
|
return
|
||||||
|
print("Warning: Note account was not found. The note account was not imported.")
|
||||||
|
print("Make sure you have imported the NK15 database. The new import script handles correctly the permissions.")
|
||||||
|
print("This migration will be ignored, you can re-run it if you forgot the note account or ignore it if you "
|
||||||
|
"don't want this account.")
|
||||||
|
return
|
||||||
|
|
||||||
|
note = note.get()
|
||||||
|
|
||||||
|
# Set for the two clubs a large expiration date and the correct role.
|
||||||
|
for m in Membership.objects.filter(user_id=note.id).all():
|
||||||
|
m.date_end = "3142-12-12"
|
||||||
|
m.roles.set(Role.objects.filter(name="PC Kfet").all())
|
||||||
|
m.save()
|
||||||
|
# By default, the note account is only authorized to be logged from localhost.
|
||||||
|
note.password = "ipbased$127.0.0.1"
|
||||||
|
note.is_active = True
|
||||||
|
note.save()
|
||||||
|
# Ensure that the note of the account is disabled
|
||||||
|
note.note.inactivity_reason = 'forced'
|
||||||
|
note.note.is_active = False
|
||||||
|
note.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
('member', '0005_remove_null_tag_on_charfields'),
|
||||||
|
('permission', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(give_note_account_permissions),
|
||||||
|
]
|
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'),
|
||||||
|
),
|
||||||
|
]
|
18
apps/member/migrations/0013_auto_20240801_1436.py
Normal file
18
apps/member/migrations/0013_auto_20240801_1436.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 2.2.28 on 2024-08-01 12:36
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('member', '0012_club_add_registration_form'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='profile',
|
||||||
|
name='promotion',
|
||||||
|
field=models.PositiveSmallIntegerField(default=2024, help_text='Year of entry to the school (None if not ENS student)', null=True, verbose_name='promotion'),
|
||||||
|
),
|
||||||
|
]
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-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,36 @@ class Club(models.Model):
|
|||||||
self.membership_end = None
|
self.membership_end = None
|
||||||
super().save(force_insert, force_update, update_fields)
|
super().save(force_insert, force_update, update_fields)
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _("club")
|
|
||||||
verbose_name_plural = _("clubs")
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse_lazy('member:club_detail', args=(self.pk,))
|
return reverse_lazy('member:club_detail', args=(self.pk,))
|
||||||
|
|
||||||
|
def update_membership_dates(self):
|
||||||
|
"""
|
||||||
|
This function is called each time the club detail view is displayed.
|
||||||
|
Update the year of the membership dates.
|
||||||
|
"""
|
||||||
|
if not self.membership_start or not self.membership_end:
|
||||||
|
return
|
||||||
|
|
||||||
|
today = datetime.date.today()
|
||||||
|
|
||||||
|
# Avoid any problems on February 29
|
||||||
|
if self.membership_start.month == 2 and self.membership_start.day == 29:
|
||||||
|
self.membership_start -= datetime.timedelta(days=1)
|
||||||
|
if self.membership_end.month == 2 and self.membership_end.day == 29:
|
||||||
|
self.membership_end += datetime.timedelta(days=1)
|
||||||
|
|
||||||
|
while today >= datetime.date(self.membership_start.year + 1,
|
||||||
|
self.membership_start.month, self.membership_start.day):
|
||||||
|
if self.membership_start:
|
||||||
|
self.membership_start = datetime.date(self.membership_start.year + 1,
|
||||||
|
self.membership_start.month, self.membership_start.day)
|
||||||
|
if self.membership_end:
|
||||||
|
self.membership_end = datetime.date(self.membership_end.year + 1,
|
||||||
|
self.membership_end.month, self.membership_end.day)
|
||||||
|
self._force_save = True
|
||||||
|
self.save(force_update=True)
|
||||||
|
|
||||||
|
|
||||||
class Membership(models.Model):
|
class Membership(models.Model):
|
||||||
"""
|
"""
|
||||||
@ -313,6 +333,7 @@ class Membership(models.Model):
|
|||||||
|
|
||||||
roles = models.ManyToManyField(
|
roles = models.ManyToManyField(
|
||||||
"permission.Role",
|
"permission.Role",
|
||||||
|
related_name="memberships",
|
||||||
verbose_name=_("roles"),
|
verbose_name=_("roles"),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -330,6 +351,66 @@ class Membership(models.Model):
|
|||||||
verbose_name=_('fee'),
|
verbose_name=_('fee'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('membership')
|
||||||
|
verbose_name_plural = _('memberships')
|
||||||
|
indexes = [models.Index(fields=['user'])]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return _("Membership of {user} for the club {club}").format(user=self.user.username, club=self.club.name, )
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Calculate fee and end date before saving the membership and creating the transaction if needed.
|
||||||
|
"""
|
||||||
|
# Ensure that club membership dates are valid
|
||||||
|
old_membership_start = self.club.membership_start
|
||||||
|
self.club.update_membership_dates()
|
||||||
|
if self.club.membership_start != old_membership_start:
|
||||||
|
self.club.save()
|
||||||
|
|
||||||
|
created = not self.pk
|
||||||
|
if not created:
|
||||||
|
for role in self.roles.all():
|
||||||
|
club = role.for_club
|
||||||
|
if club is not None:
|
||||||
|
if club.pk != self.club_id:
|
||||||
|
raise ValidationError(_('The role {role} does not apply to the club {club}.')
|
||||||
|
.format(role=role.name, club=club.name))
|
||||||
|
else:
|
||||||
|
if Membership.objects.filter(
|
||||||
|
user=self.user,
|
||||||
|
club=self.club,
|
||||||
|
date_start__lte=self.date_start,
|
||||||
|
date_end__gte=self.date_start,
|
||||||
|
).exists():
|
||||||
|
raise ValidationError(_('User is already a member of the club'))
|
||||||
|
|
||||||
|
if self.club.parent_club is not None:
|
||||||
|
# Check that the user is already a member of the parent club if the membership is created
|
||||||
|
if not Membership.objects.filter(
|
||||||
|
user=self.user,
|
||||||
|
club=self.club.parent_club,
|
||||||
|
date_start__gte=self.club.parent_club.membership_start,
|
||||||
|
).exists():
|
||||||
|
if hasattr(self, '_force_renew_parent') and self._force_renew_parent:
|
||||||
|
self.renew_parent()
|
||||||
|
else:
|
||||||
|
raise ValidationError(_('User is not a member of the parent club')
|
||||||
|
+ ' ' + self.club.parent_club.name)
|
||||||
|
|
||||||
|
self.fee = self.club.membership_fee_paid if self.user.profile.paid else self.club.membership_fee_unpaid
|
||||||
|
|
||||||
|
self.date_end = self.date_start + datetime.timedelta(days=self.club.membership_duration) \
|
||||||
|
if self.club.membership_duration is not None else self.date_start + datetime.timedelta(days=424242)
|
||||||
|
if self.club.membership_end is not None and self.date_end > self.club.membership_end:
|
||||||
|
self.date_end = self.club.membership_end
|
||||||
|
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
self.make_transaction()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def valid(self):
|
def valid(self):
|
||||||
"""
|
"""
|
||||||
@ -399,60 +480,14 @@ class Membership(models.Model):
|
|||||||
|
|
||||||
if self.club.parent_club.name == "BDE":
|
if self.club.parent_club.name == "BDE":
|
||||||
parent_membership.roles.set(
|
parent_membership.roles.set(
|
||||||
Role.objects.filter(Q(name="Adhérent BDE") | Q(name="Membre de club")).all())
|
Role.objects.filter(Q(name="Adhérent⋅e BDE") | Q(name="Membre de club")).all())
|
||||||
elif self.club.parent_club.name == "Kfet":
|
elif self.club.parent_club.name == "Kfet":
|
||||||
parent_membership.roles.set(
|
parent_membership.roles.set(
|
||||||
Role.objects.filter(Q(name="Adhérent Kfet") | Q(name="Membre de club")).all())
|
Role.objects.filter(Q(name="Adhérent⋅e Kfet") | Q(name="Membre de club")).all())
|
||||||
else:
|
else:
|
||||||
parent_membership.roles.set(Role.objects.filter(name="Membre de club").all())
|
parent_membership.roles.set(Role.objects.filter(name="Membre de club").all())
|
||||||
parent_membership.save()
|
parent_membership.save()
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Calculate fee and end date before saving the membership and creating the transaction if needed.
|
|
||||||
"""
|
|
||||||
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.
|
||||||
@ -490,11 +525,3 @@ class Membership(models.Model):
|
|||||||
soge_credit.save()
|
soge_credit.save()
|
||||||
else:
|
else:
|
||||||
transaction.save(force_insert=True)
|
transaction.save(force_insert=True)
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return _("Membership of {user} for the club {club}").format(user=self.user.username, club=self.club.name, )
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _('membership')
|
|
||||||
verbose_name_plural = _('memberships')
|
|
||||||
indexes = [models.Index(fields=['user'])]
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-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
|
||||||
|
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ function create_alias (e) {
|
|||||||
}).done(function () {
|
}).done(function () {
|
||||||
// Reload table
|
// Reload table
|
||||||
$('#alias_table').load(location.pathname + ' #alias_table')
|
$('#alias_table').load(location.pathname + ' #alias_table')
|
||||||
addMsg('Alias ajouté', 'success')
|
addMsg(gettext('Alias successfully added'), 'success')
|
||||||
}).fail(function (xhr, _textStatus, _error) {
|
}).fail(function (xhr, _textStatus, _error) {
|
||||||
errMsg(xhr.responseJSON)
|
errMsg(xhr.responseJSON)
|
||||||
})
|
})
|
||||||
@ -22,7 +22,7 @@ function create_alias (e) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* On click of "delete", delete the alias
|
* On click of "delete", delete the alias
|
||||||
* @param Integer button_id Alias id to remove
|
* @param button_id:Integer Alias id to remove
|
||||||
*/
|
*/
|
||||||
function delete_button (button_id) {
|
function delete_button (button_id) {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
@ -30,7 +30,7 @@ function delete_button (button_id) {
|
|||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: { 'X-CSRFTOKEN': CSRF_TOKEN }
|
headers: { 'X-CSRFTOKEN': CSRF_TOKEN }
|
||||||
}).done(function () {
|
}).done(function () {
|
||||||
addMsg('Alias supprimé', 'success')
|
addMsg(gettext('Alias successfully deleted'), 'success')
|
||||||
$('#alias_table').load(location.pathname + ' #alias_table')
|
$('#alias_table').load(location.pathname + ' #alias_table')
|
||||||
}).fail(function (xhr, _textStatus, _error) {
|
}).fail(function (xhr, _textStatus, _error) {
|
||||||
errMsg(xhr.responseJSON)
|
errMsg(xhr.responseJSON)
|
||||||
|
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',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -43,11 +44,27 @@ class UserTable(tables.Table):
|
|||||||
|
|
||||||
section = tables.Column(accessor='profile__section')
|
section = tables.Column(accessor='profile__section')
|
||||||
|
|
||||||
|
# Override the column to let replace the URL
|
||||||
|
email = tables.EmailColumn(linkify=lambda record: "mailto:{}".format(record.email))
|
||||||
|
|
||||||
balance = tables.Column(accessor='note__balance', verbose_name=_("Balance"))
|
balance = tables.Column(accessor='note__balance', verbose_name=_("Balance"))
|
||||||
|
|
||||||
|
def render_email(self, record, value):
|
||||||
|
# Replace the email by a dash if the user can't see the profile detail
|
||||||
|
# Replace also the URL
|
||||||
|
if not PermissionBackend.check_perm(get_current_request(), "member.view_profile", record.profile):
|
||||||
|
value = "—"
|
||||||
|
record.email = value
|
||||||
|
return value
|
||||||
|
|
||||||
|
def render_section(self, record, value):
|
||||||
|
return value \
|
||||||
|
if PermissionBackend.check_perm(get_current_request(), "member.view_profile", record.profile) \
|
||||||
|
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 = {
|
||||||
@ -58,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',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -77,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)
|
||||||
|
|
||||||
@ -86,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)
|
||||||
|
|
||||||
@ -102,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,
|
||||||
@ -111,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})
|
||||||
@ -126,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
|
||||||
@ -149,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)
|
||||||
|
|
||||||
|
@ -48,7 +48,7 @@
|
|||||||
<dd class="col-xl-6">
|
<dd class="col-xl-6">
|
||||||
<a class="badge badge-secondary" href="{% url 'member:club_alias' club.pk %}">
|
<a class="badge badge-secondary" href="{% url 'member:club_alias' club.pk %}">
|
||||||
<i class="fa fa-edit"></i>
|
<i class="fa fa-edit"></i>
|
||||||
{% trans 'Manage aliases' %} ({{ club.note.alias_set.all|length }})
|
{% trans 'Manage aliases' %} ({{ club.note.alias.all|length }})
|
||||||
</a>
|
</a>
|
||||||
</dd>
|
</dd>
|
||||||
|
|
||||||
|
@ -21,29 +21,39 @@
|
|||||||
<dd class="col-xl-6">
|
<dd class="col-xl-6">
|
||||||
<a class="badge badge-secondary" href="{% url 'member:user_alias' user_object.pk %}">
|
<a class="badge badge-secondary" href="{% url 'member:user_alias' user_object.pk %}">
|
||||||
<i class="fa fa-edit"></i>
|
<i class="fa fa-edit"></i>
|
||||||
{% trans 'Manage aliases' %} ({{ user_object.note.alias_set.all|length }})
|
{% trans 'Manage aliases' %} ({{ user_object.note.alias.all|length }})
|
||||||
</a>
|
</a>
|
||||||
</dd>
|
</dd>
|
||||||
|
|
||||||
<dt class="col-xl-6">{% trans 'section'|capfirst %}</dt>
|
<dt class="col-xl-6">{% trans 'friendships'|capfirst %}</dt>
|
||||||
<dd class="col-xl-6">{{ user_object.profile.section }}</dd>
|
<dd class="col-xl-6">
|
||||||
|
<a class="badge badge-secondary" href="{% url 'member:user_trust' user_object.pk %}">
|
||||||
<dt class="col-xl-6">{% trans 'email'|capfirst %}</dt>
|
<i class="fa fa-edit"></i>
|
||||||
<dd class="col-xl-6"><a href="mailto:{{ user_object.email }}">{{ user_object.email }}</a></dd>
|
{% trans 'Manage friendships' %} ({{ user_object.note.trusting.all|length }})
|
||||||
|
</a>
|
||||||
<dt class="col-xl-6">{% trans 'phone number'|capfirst %}</dt>
|
|
||||||
<dd class="col-xl-6"><a href="tel:{{ user_object.profile.phone_number }}">{{ user_object.profile.phone_number }}</a>
|
|
||||||
</dd>
|
</dd>
|
||||||
|
|
||||||
<dt class="col-xl-6">{% trans 'address'|capfirst %}</dt>
|
{% if "member.view_profile"|has_perm:user_object.profile %}
|
||||||
<dd class="col-xl-6">{{ user_object.profile.address }}</dd>
|
<dt class="col-xl-6">{% trans 'section'|capfirst %}</dt>
|
||||||
|
<dd class="col-xl-6">{{ user_object.profile.section }}</dd>
|
||||||
|
|
||||||
|
<dt class="col-xl-6">{% trans 'email'|capfirst %}</dt>
|
||||||
|
<dd class="col-xl-6"><a href="mailto:{{ user_object.email }}">{{ user_object.email }}</a></dd>
|
||||||
|
|
||||||
|
<dt class="col-xl-6">{% trans 'phone number'|capfirst %}</dt>
|
||||||
|
<dd class="col-xl-6"><a href="tel:{{ user_object.profile.phone_number }}">{{ user_object.profile.phone_number }}</a>
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<dt class="col-xl-6">{% trans 'address'|capfirst %}</dt>
|
||||||
|
<dd class="col-xl-6">{{ user_object.profile.address }}</dd>
|
||||||
|
|
||||||
|
<dt class="col-xl-6">{% trans 'paid'|capfirst %}</dt>
|
||||||
|
<dd class="col-xl-6">{{ user_object.profile.paid|yesno }}</dd>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if user_object.note and "note.view_note"|has_perm:user_object.note %}
|
{% if user_object.note and "note.view_note"|has_perm:user_object.note %}
|
||||||
<dt class="col-xl-6">{% trans 'balance'|capfirst %}</dt>
|
<dt class="col-xl-6">{% trans 'balance'|capfirst %}</dt>
|
||||||
<dd class="col-xl-6">{{ user_object.note.balance | pretty_money }}</dd>
|
<dd class="col-xl-6">{{ user_object.note.balance | pretty_money }}</dd>
|
||||||
|
|
||||||
<dt class="col-xl-6">{% trans 'paid'|capfirst %}</dt>
|
|
||||||
<dd class="col-xl-6">{{ user_object.profile.paid|yesno }}</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%}
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
{% load i18n perms %}
|
{% load i18n perms %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% if "member.change_profile_registration_valid"|has_perm:user %}
|
{% if can_manage_registrations %}
|
||||||
<a class="btn btn-block btn-secondary mb-3" href="{% url 'registration:future_user_list' %}">
|
<a class="btn btn-block btn-secondary mb-3" href="{% url 'registration:future_user_list' %}">
|
||||||
<i class="fa fa-user-plus"></i> {% trans "Registrations" %}
|
<i class="fa fa-user-plus"></i> {% trans "Registrations" %}
|
||||||
</a>
|
</a>
|
||||||
|
@ -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,21 +1,24 @@
|
|||||||
# 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
|
||||||
import os
|
import os
|
||||||
from datetime import date, timedelta
|
from datetime import date, timedelta
|
||||||
|
|
||||||
|
from api.tests import TestAPI
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from member.models import Club, Membership, Profile
|
|
||||||
from note.models import Alias, NoteSpecial
|
from note.models import Alias, NoteSpecial
|
||||||
from permission.models import Role
|
from permission.models import Role
|
||||||
from treasury.models import SogeCredit
|
from treasury.models import SogeCredit
|
||||||
|
|
||||||
|
from ..api.views import ClubViewSet, MembershipViewSet, ProfileViewSet
|
||||||
|
from ..models import Club, Membership, Profile
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Create some users and clubs and test that all pages are rendering properly
|
Create some users and clubs and test that all pages are rendering properly
|
||||||
and that memberships are working.
|
and that memberships are working.
|
||||||
@ -180,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,
|
||||||
@ -288,7 +291,7 @@ class TestMemberships(TestCase):
|
|||||||
|
|
||||||
response = self.client.post(reverse("member:club_manage_roles", args=(self.membership.pk,)), data=dict(
|
response = self.client.post(reverse("member:club_manage_roles", args=(self.membership.pk,)), data=dict(
|
||||||
roles=[role.id for role in Role.objects.filter(
|
roles=[role.id for role in Role.objects.filter(
|
||||||
Q(name="Membre de club") | Q(name="Trésorier·ère de club") | Q(name="Bureau de club")).all()],
|
Q(name="Membre de club") | Q(name="Trésorièr⋅e de club") | Q(name="Bureau de club")).all()],
|
||||||
))
|
))
|
||||||
self.assertRedirects(response, self.user.profile.get_absolute_url(), 302, 200)
|
self.assertRedirects(response, self.user.profile.get_absolute_url(), 302, 200)
|
||||||
self.membership.refresh_from_db()
|
self.membership.refresh_from_db()
|
||||||
@ -332,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())
|
||||||
@ -403,3 +407,46 @@ class TestMemberships(TestCase):
|
|||||||
self.user.password = "custom_nk15$1$" + salt + "|" + hashed
|
self.user.password = "custom_nk15$1$" + salt + "|" + hashed
|
||||||
self.user.save()
|
self.user.save()
|
||||||
self.assertTrue(self.user.check_password(password))
|
self.assertTrue(self.user.check_password(password))
|
||||||
|
|
||||||
|
|
||||||
|
class TestMemberAPI(TestAPI):
|
||||||
|
def setUp(self) -> None:
|
||||||
|
super().setUp()
|
||||||
|
|
||||||
|
self.user.profile.registration_valid = True
|
||||||
|
self.user.profile.email_confirmed = True
|
||||||
|
self.user.profile.phone_number = "0600000000"
|
||||||
|
self.user.profile.section = "1A0"
|
||||||
|
self.user.profile.department = "A0"
|
||||||
|
self.user.profile.address = "Earth"
|
||||||
|
self.user.profile.save()
|
||||||
|
|
||||||
|
self.club = Club.objects.create(
|
||||||
|
name="totoclub",
|
||||||
|
parent_club=Club.objects.get(name="BDE"),
|
||||||
|
membership_start=date(year=1970, month=1, day=1),
|
||||||
|
membership_end=date(year=2040, month=1, day=1),
|
||||||
|
membership_duration=365 * 10,
|
||||||
|
)
|
||||||
|
self.bde_membership = Membership.objects.create(user=self.user, club=Club.objects.get(name="BDE"))
|
||||||
|
self.membership = Membership.objects.create(user=self.user, club=self.club)
|
||||||
|
self.membership.roles.add(Role.objects.get(name="Bureau de club"))
|
||||||
|
self.membership.save()
|
||||||
|
|
||||||
|
def test_club_api(self):
|
||||||
|
"""
|
||||||
|
Load Club API page and test all filters and permissions
|
||||||
|
"""
|
||||||
|
self.check_viewset(ClubViewSet, "/api/members/club/")
|
||||||
|
|
||||||
|
def test_profile_api(self):
|
||||||
|
"""
|
||||||
|
Load Profile API page and test all filters and permissions
|
||||||
|
"""
|
||||||
|
self.check_viewset(ProfileViewSet, "/api/members/profile/")
|
||||||
|
|
||||||
|
def test_membership_api(self):
|
||||||
|
"""
|
||||||
|
Load Membership API page and test all filters and permissions
|
||||||
|
"""
|
||||||
|
self.check_viewset(MembershipViewSet, "/api/members/membership/")
|
||||||
|
@ -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
|
||||||
@ -16,17 +16,18 @@ from django.utils import timezone
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.generic import DetailView, UpdateView, TemplateView
|
from django.views.generic import DetailView, UpdateView, TemplateView
|
||||||
from django.views.generic.edit import FormMixin
|
from django.views.generic.edit import FormMixin
|
||||||
from django_tables2.views import SingleTableView
|
from django_tables2.views import MultiTableMixin, SingleTableMixin, SingleTableView
|
||||||
from rest_framework.authtoken.models import Token
|
from rest_framework.authtoken.models import Token
|
||||||
from note.models import Alias, NoteUser
|
from api.viewsets import is_regex
|
||||||
|
from note.models import Alias, NoteClub, NoteUser, Trust
|
||||||
from note.models.transactions import Transaction, SpecialTransaction
|
from note.models.transactions import Transaction, SpecialTransaction
|
||||||
from note.tables import HistoryTable, AliasTable
|
from note.tables import HistoryTable, AliasTable, TrustTable, TrustedTable
|
||||||
from note_kfet.middlewares import _set_current_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 +42,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,10 +72,11 @@ class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
|||||||
form.fields['email'].required = True
|
form.fields['email'].required = True
|
||||||
form.fields['email'].help_text = _("This address must be valid.")
|
form.fields['email'].help_text = _("This address must be valid.")
|
||||||
|
|
||||||
context['profile_form'] = self.profile_form(instance=context['user_object'].profile,
|
if PermissionBackend.check_perm(self.request, "member.change_profile", context['user_object'].profile):
|
||||||
data=self.request.POST if self.request.POST else None)
|
context['profile_form'] = self.profile_form(instance=context['user_object'].profile,
|
||||||
if not self.object.profile.report_frequency:
|
data=self.request.POST if self.request.POST else None)
|
||||||
del context['profile_form'].fields["last_report"]
|
if not self.object.profile.report_frequency:
|
||||||
|
del context['profile_form'].fields["last_report"]
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
@ -152,13 +155,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")\
|
||||||
@ -172,24 +175,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
|
||||||
|
|
||||||
@ -218,24 +220,81 @@ class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
|||||||
if "search" in self.request.GET and self.request.GET["search"]:
|
if "search" in self.request.GET and self.request.GET["search"]:
|
||||||
pattern = self.request.GET["search"]
|
pattern = self.request.GET["search"]
|
||||||
|
|
||||||
|
# Check if this is a valid regex. If not, we won't check regex
|
||||||
|
valid_regex = is_regex(pattern)
|
||||||
|
suffix = "__iregex" if valid_regex else "__istartswith"
|
||||||
|
prefix = "^" if valid_regex else ""
|
||||||
qs = qs.filter(
|
qs = qs.filter(
|
||||||
username__iregex="^" + pattern
|
Q(**{f"username{suffix}": prefix + pattern})
|
||||||
).union(
|
).union(
|
||||||
qs.filter(
|
qs.filter(
|
||||||
(Q(alias__iregex="^" + pattern)
|
(Q(**{f"alias{suffix}": prefix + pattern})
|
||||||
| Q(normalized_alias__iregex="^" + Alias.normalize(pattern))
|
| Q(**{f"normalized_alias{suffix}": prefix + Alias.normalize(pattern)})
|
||||||
| Q(last_name__iregex="^" + pattern)
|
| Q(**{f"last_name{suffix}": prefix + pattern})
|
||||||
| Q(first_name__iregex="^" + pattern)
|
| Q(**{f"first_name{suffix}": prefix + pattern})
|
||||||
| Q(email__istartswith=pattern))
|
| Q(email__istartswith=pattern))
|
||||||
& ~Q(username__iregex="^" + pattern)
|
& ~Q(**{f"username{suffix}": prefix + pattern})
|
||||||
), all=True)
|
), all=True)
|
||||||
else:
|
else:
|
||||||
qs = qs.none()
|
qs = qs.none()
|
||||||
|
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
pre_registered_users = User.objects.filter(PermissionBackend.filter_queryset(self.request, User, "view"))\
|
||||||
|
.filter(profile__registration_valid=False)
|
||||||
|
context["can_manage_registrations"] = pre_registered_users.exists()
|
||||||
|
return context
|
||||||
|
|
||||||
class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
|
||||||
|
class ProfileTrustView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, DetailView):
|
||||||
|
"""
|
||||||
|
View and manage user trust relationships
|
||||||
|
"""
|
||||||
|
model = User
|
||||||
|
template_name = 'member/profile_trust.html'
|
||||||
|
context_object_name = 'user_object'
|
||||||
|
extra_context = {"title": _("Note friendships")}
|
||||||
|
|
||||||
|
tables = [
|
||||||
|
lambda data: TrustTable(data, prefix="trust-"),
|
||||||
|
lambda data: TrustedTable(data, prefix="trusted-"),
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_tables_data(self):
|
||||||
|
note = self.object.note
|
||||||
|
return [
|
||||||
|
note.trusting.filter(PermissionBackend.filter_queryset(self.request, Trust, "view")).distinct(),
|
||||||
|
note.trusted.filter(PermissionBackend.filter_queryset(self.request, Trust, "view")).distinct(),
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
tables = context["tables"]
|
||||||
|
for name, table in zip(["trusting", "trusted_by"], tables):
|
||||||
|
context[name] = table
|
||||||
|
|
||||||
|
context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_trust", Trust(
|
||||||
|
trusting=context["object"].note,
|
||||||
|
trusted=context["object"].note
|
||||||
|
))
|
||||||
|
context["widget"] = {
|
||||||
|
"name": "trusted",
|
||||||
|
"resetable": True,
|
||||||
|
"attrs": {
|
||||||
|
"class": "autocomplete form-control",
|
||||||
|
"id": "trusted",
|
||||||
|
"api_url": "/api/note/alias/?note__polymorphic_ctype__model=noteuser",
|
||||||
|
"name_field": "name",
|
||||||
|
"placeholder": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableMixin, DetailView):
|
||||||
"""
|
"""
|
||||||
View and manage user aliases.
|
View and manage user aliases.
|
||||||
"""
|
"""
|
||||||
@ -244,12 +303,16 @@ class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
|||||||
context_object_name = 'user_object'
|
context_object_name = 'user_object'
|
||||||
extra_context = {"title": _("Note aliases")}
|
extra_context = {"title": _("Note aliases")}
|
||||||
|
|
||||||
|
table_class = AliasTable
|
||||||
|
context_table_name = "aliases"
|
||||||
|
|
||||||
|
def get_table_data(self):
|
||||||
|
return self.object.note.alias.filter(PermissionBackend.filter_queryset(self.request, Alias, "view")).distinct() \
|
||||||
|
.order_by('normalized_name')
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
note = context['object'].note
|
context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_alias", Alias(
|
||||||
context["aliases"] = AliasTable(note.alias_set.filter(PermissionBackend
|
|
||||||
.filter_queryset(self.request.user, Alias, "view")).all())
|
|
||||||
context["can_create"] = PermissionBackend.check_perm(self.request.user, "note.add_alias", Alias(
|
|
||||||
note=context["object"].note,
|
note=context["object"].note,
|
||||||
name="",
|
name="",
|
||||||
normalized_name="",
|
normalized_name="",
|
||||||
@ -283,12 +346,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
|
||||||
@ -364,17 +430,22 @@ class ClubListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
|||||||
if "search" in self.request.GET:
|
if "search" in self.request.GET:
|
||||||
pattern = self.request.GET["search"]
|
pattern = self.request.GET["search"]
|
||||||
|
|
||||||
|
# Check if this is a valid regex. If not, we won't check regex
|
||||||
|
valid_regex = is_regex(pattern)
|
||||||
|
suffix = "__iregex" if valid_regex else "__istartswith"
|
||||||
|
prefix = "^" if valid_regex else ""
|
||||||
|
|
||||||
qs = qs.filter(
|
qs = qs.filter(
|
||||||
Q(name__iregex=pattern)
|
Q(**{f"name{suffix}": prefix + pattern})
|
||||||
| Q(note__alias__name__iregex=pattern)
|
| Q(**{f"note__alias__name{suffix}": prefix + pattern})
|
||||||
| Q(note__alias__normalized_name__iregex=Alias.normalize(pattern))
|
| Q(**{f"note__alias__normalized_name{suffix}": prefix + Alias.normalize(pattern)})
|
||||||
)
|
)
|
||||||
|
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
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",
|
||||||
))
|
))
|
||||||
@ -395,9 +466,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())\
|
||||||
@ -405,7 +479,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))
|
||||||
@ -414,7 +488,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")\
|
||||||
@ -435,10 +509,33 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
|||||||
context["can_add_members"] = PermissionBackend()\
|
context["can_add_members"] = PermissionBackend()\
|
||||||
.has_perm(self.request.user, "member.add_membership", empty_membership)
|
.has_perm(self.request.user, "member.add_membership", empty_membership)
|
||||||
|
|
||||||
|
# Check permissions to see if the authenticated user can lock/unlock the note
|
||||||
|
with transaction.atomic():
|
||||||
|
modified_note = NoteClub.objects.get(pk=club.note.pk)
|
||||||
|
# Don't log these tests
|
||||||
|
modified_note._no_signal = True
|
||||||
|
modified_note.is_active = False
|
||||||
|
modified_note.inactivity_reason = 'manual'
|
||||||
|
context["can_lock_note"] = club.note.is_active and PermissionBackend \
|
||||||
|
.check_perm(self.request, "note.change_noteclub_is_active", modified_note)
|
||||||
|
old_note = NoteClub.objects.select_for_update().get(pk=club.note.pk)
|
||||||
|
modified_note.inactivity_reason = 'forced'
|
||||||
|
modified_note._force_save = True
|
||||||
|
modified_note.save()
|
||||||
|
context["can_force_lock"] = club.note.is_active and PermissionBackend \
|
||||||
|
.check_perm(self.request, "note.change_noteclub_is_active", modified_note)
|
||||||
|
old_note._force_save = True
|
||||||
|
old_note._no_signal = True
|
||||||
|
old_note.save()
|
||||||
|
modified_note.refresh_from_db()
|
||||||
|
modified_note.is_active = True
|
||||||
|
context["can_unlock_note"] = not club.note.is_active and PermissionBackend \
|
||||||
|
.check_perm(self.request, "note.change_noteclub_is_active", modified_note)
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class ClubAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
class ClubAliasView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableMixin, DetailView):
|
||||||
"""
|
"""
|
||||||
Manage aliases of a club.
|
Manage aliases of a club.
|
||||||
"""
|
"""
|
||||||
@ -447,12 +544,17 @@ class ClubAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
|||||||
context_object_name = 'club'
|
context_object_name = 'club'
|
||||||
extra_context = {"title": _("Note aliases")}
|
extra_context = {"title": _("Note aliases")}
|
||||||
|
|
||||||
|
table_class = AliasTable
|
||||||
|
context_table_name = "aliases"
|
||||||
|
|
||||||
|
def get_table_data(self):
|
||||||
|
return self.object.note.alias.filter(
|
||||||
|
PermissionBackend.filter_queryset(self.request, Alias, "view")).distinct()
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
note = context['object'].note
|
|
||||||
context["aliases"] = AliasTable(note.alias_set.filter(PermissionBackend
|
context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_alias", Alias(
|
||||||
.filter_queryset(self.request.user, Alias, "view")).all())
|
|
||||||
context["can_create"] = PermissionBackend.check_perm(self.request.user, "note.add_alias", Alias(
|
|
||||||
note=context["object"].note,
|
note=context["object"].note,
|
||||||
name="",
|
name="",
|
||||||
normalized_name="",
|
normalized_name="",
|
||||||
@ -527,7 +629,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.
|
||||||
@ -617,9 +719,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:
|
||||||
@ -651,7 +750,6 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
|
|||||||
user=form.instance.user,
|
user=form.instance.user,
|
||||||
club=club.parent_club,
|
club=club.parent_club,
|
||||||
date_start__gte=club.parent_club.membership_start,
|
date_start__gte=club.parent_club.membership_start,
|
||||||
date_end__lte=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
|
||||||
@ -666,15 +764,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."))
|
|
||||||
if not first_name:
|
|
||||||
form.add_error('first_name', _("This field is required."))
|
|
||||||
if not bank and credit_type.special_type == "Chèque":
|
|
||||||
form.add_error('bank', _("This field is required."))
|
|
||||||
return self.form_invalid(form)
|
|
||||||
|
|
||||||
return not error
|
return not error
|
||||||
|
|
||||||
@ -685,7 +777,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
|
||||||
@ -694,6 +786,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
|
||||||
@ -736,6 +832,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.
|
||||||
@ -760,8 +857,8 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
|
|||||||
|
|
||||||
ret = super().form_valid(form)
|
ret = super().form_valid(form)
|
||||||
|
|
||||||
member_role = Role.objects.filter(Q(name="Adhérent BDE") | Q(name="Membre de club")).all() \
|
member_role = Role.objects.filter(Q(name="Adhérent⋅e BDE") | Q(name="Membre de club")).all() \
|
||||||
if club.name == "BDE" else Role.objects.filter(Q(name="Adhérent Kfet") | Q(name="Membre de club")).all() \
|
if club.name == "BDE" else Role.objects.filter(Q(name="Adhérent⋅e Kfet") | Q(name="Membre de club")).all() \
|
||||||
if club.name == "Kfet"else Role.objects.filter(name="Membre de club").all()
|
if club.name == "Kfet"else Role.objects.filter(name="Membre de club").all()
|
||||||
# Set the same roles as before
|
# Set the same roles as before
|
||||||
if old_membership:
|
if old_membership:
|
||||||
@ -797,7 +894,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
|
|||||||
membership.refresh_from_db()
|
membership.refresh_from_db()
|
||||||
if old_membership.exists():
|
if old_membership.exists():
|
||||||
membership.roles.set(old_membership.get().roles.all())
|
membership.roles.set(old_membership.get().roles.all())
|
||||||
membership.roles.set(Role.objects.filter(Q(name="Adhérent Kfet") | Q(name="Membre de club")).all())
|
membership.roles.set(Role.objects.filter(Q(name="Adhérent⋅e Kfet") | Q(name="Membre de club")).all())
|
||||||
membership.save()
|
membership.save()
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
@ -845,10 +942,15 @@ class ClubMembersListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableV
|
|||||||
|
|
||||||
if 'search' in self.request.GET:
|
if 'search' in self.request.GET:
|
||||||
pattern = self.request.GET['search']
|
pattern = self.request.GET['search']
|
||||||
|
|
||||||
|
# Check if this is a valid regex. If not, we won't check regex
|
||||||
|
valid_regex = is_regex(pattern)
|
||||||
|
suffix = "__iregex" if valid_regex else "__istartswith"
|
||||||
|
prefix = "^" if valid_regex else ""
|
||||||
qs = qs.filter(
|
qs = qs.filter(
|
||||||
Q(user__first_name__iregex='^' + pattern)
|
Q(**{f"user__first_name{suffix}": prefix + pattern})
|
||||||
| Q(user__last_name__iregex='^' + pattern)
|
| Q(**{f"user__last_name{suffix}": prefix + pattern})
|
||||||
| Q(user__note__alias__normalized_name__iregex='^' + Alias.normalize(pattern))
|
| Q(**{f"user__note__alias__normalized_name{suffix}": prefix + Alias.normalize(pattern)})
|
||||||
)
|
)
|
||||||
|
|
||||||
only_active = "only_active" not in self.request.GET or self.request.GET["only_active"] != '0'
|
only_active = "only_active" not in self.request.GET or self.request.GET["only_active"] != '0'
|
||||||
@ -868,7 +970,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,72 +1,85 @@
|
|||||||
# 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.db.models import Q
|
from django.db.models import Q
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
from rest_framework.filters import OrderingFilter, SearchFilter
|
from rest_framework.filters import OrderingFilter
|
||||||
from rest_framework import viewsets
|
from rest_framework import status, viewsets
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework import status
|
from api.filters import RegexSafeSearchFilter
|
||||||
from api.viewsets import ReadProtectedModelViewSet, ReadOnlyProtectedModelViewSet
|
from api.viewsets import ReadProtectedModelViewSet, ReadOnlyProtectedModelViewSet, \
|
||||||
from note_kfet.middlewares import get_current_session
|
is_regex
|
||||||
from permission.backends import PermissionBackend
|
from permission.backends import PermissionBackend
|
||||||
|
|
||||||
from .serializers import NotePolymorphicSerializer, AliasSerializer, ConsumerSerializer,\
|
from .serializers import NotePolymorphicSerializer, AliasSerializer, ConsumerSerializer, \
|
||||||
TemplateCategorySerializer, TransactionTemplateSerializer, TransactionPolymorphicSerializer
|
TemplateCategorySerializer, TransactionTemplateSerializer, TransactionPolymorphicSerializer, \
|
||||||
from ..models.notes import Note, Alias
|
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
|
||||||
|
|
||||||
|
|
||||||
class NotePolymorphicViewSet(ReadProtectedModelViewSet):
|
class NotePolymorphicViewSet(ReadProtectedModelViewSet):
|
||||||
"""
|
"""
|
||||||
REST API View set.
|
REST API View set.
|
||||||
The djangorestframework plugin will get all `Note` objects (with polymorhism), serialize it to JSON with the given serializer,
|
The djangorestframework plugin will get all `Note` objects (with polymorhism),
|
||||||
|
serialize it to JSON with the given serializer,
|
||||||
then render it on /api/note/note/
|
then render it on /api/note/note/
|
||||||
"""
|
"""
|
||||||
queryset = Note.objects.all()
|
queryset = Note.objects.order_by('id')
|
||||||
serializer_class = NotePolymorphicSerializer
|
serializer_class = NotePolymorphicSerializer
|
||||||
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter, OrderingFilter]
|
||||||
filterset_fields = ['polymorphic_ctype', 'is_active', ]
|
filterset_fields = ['alias__name', 'polymorphic_ctype', 'is_active', 'balance', 'last_negative', 'created_at', ]
|
||||||
search_fields = ['$alias__normalized_name', '$alias__name', '$polymorphic_ctype__model', ]
|
search_fields = ['$alias__normalized_name', '$alias__name', '$polymorphic_ctype__model',
|
||||||
ordering_fields = ['alias__name', 'alias__normalized_name']
|
'$noteuser__user__last_name', '$noteuser__user__first_name', '$noteuser__user__email',
|
||||||
|
'$noteuser__user__email', '$noteclub__club__email', ]
|
||||||
|
ordering_fields = ['alias__name', 'alias__normalized_name', 'balance', 'created_at', ]
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
"""
|
"""
|
||||||
Parse query and apply filters.
|
Parse query and apply filters.
|
||||||
:return: The filtered set of requested notes
|
:return: The filtered set of requested notes
|
||||||
"""
|
"""
|
||||||
queryset = super().get_queryset().distinct()
|
queryset = self.queryset.filter(PermissionBackend.filter_queryset(self.request, Note, "view")
|
||||||
|
| PermissionBackend.filter_queryset(self.request, NoteUser, "view")
|
||||||
|
| PermissionBackend.filter_queryset(self.request, NoteClub, "view")
|
||||||
|
| PermissionBackend.filter_queryset(self.request, NoteSpecial, "view"))\
|
||||||
|
.distinct()
|
||||||
|
|
||||||
alias = self.request.query_params.get("alias", ".*")
|
alias = self.request.query_params.get("alias", ".*")
|
||||||
|
# Check if this is a valid regex. If not, we won't check regex
|
||||||
|
valid_regex = is_regex(alias)
|
||||||
|
suffix = '__iregex' if valid_regex else '__istartswith'
|
||||||
|
alias_prefix = '^' if valid_regex else ''
|
||||||
queryset = queryset.filter(
|
queryset = queryset.filter(
|
||||||
Q(alias__name__iregex="^" + alias)
|
Q(**{f"alias__name{suffix}": alias_prefix + alias})
|
||||||
| Q(alias__normalized_name__iregex="^" + Alias.normalize(alias))
|
| Q(**{f"alias__normalized_name{suffix}": alias_prefix + Alias.normalize(alias)})
|
||||||
| Q(alias__normalized_name__iregex="^" + alias.lower())
|
| Q(**{f"alias__normalized_name{suffix}": alias_prefix + alias.lower()})
|
||||||
)
|
)
|
||||||
|
|
||||||
return queryset.order_by("id")
|
return queryset.order_by("id")
|
||||||
|
|
||||||
|
|
||||||
class 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.all()
|
queryset = Trust.objects
|
||||||
serializer_class = AliasSerializer
|
serializer_class = TrustSerializer
|
||||||
filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
|
filter_backends = [RegexSafeSearchFilter, DjangoFilterBackend, OrderingFilter]
|
||||||
search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ]
|
search_fields = ['$trusting__alias__name', '$trusting__alias__normalized_name',
|
||||||
filterset_fields = ['note']
|
'$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):
|
||||||
@ -74,7 +87,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/alias/
|
||||||
|
"""
|
||||||
|
queryset = Alias.objects
|
||||||
|
serializer_class = AliasSerializer
|
||||||
|
filter_backends = [RegexSafeSearchFilter, DjangoFilterBackend, OrderingFilter]
|
||||||
|
search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ]
|
||||||
|
filterset_fields = ['name', 'normalized_name', 'note', 'note__noteuser__user',
|
||||||
|
'note__noteclub__club', 'note__polymorphic_ctype__model', ]
|
||||||
|
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):
|
||||||
@ -87,18 +130,22 @@ class AliasViewSet(ReadProtectedModelViewSet):
|
|||||||
|
|
||||||
alias = self.request.query_params.get("alias", None)
|
alias = self.request.query_params.get("alias", None)
|
||||||
if alias:
|
if alias:
|
||||||
|
# Check if this is a valid regex. If not, we won't check regex
|
||||||
|
valid_regex = is_regex(alias)
|
||||||
|
suffix = '__iregex' if valid_regex else '__istartswith'
|
||||||
|
alias_prefix = '^' if valid_regex else ''
|
||||||
queryset = queryset.filter(
|
queryset = queryset.filter(
|
||||||
name__iregex="^" + alias
|
**{f"name{suffix}": alias_prefix + alias}
|
||||||
).union(
|
).union(
|
||||||
queryset.filter(
|
queryset.filter(
|
||||||
Q(normalized_name__iregex="^" + Alias.normalize(alias))
|
Q(**{f"normalized_name{suffix}": alias_prefix + Alias.normalize(alias)})
|
||||||
& ~Q(name__iregex="^" + alias)
|
& ~Q(**{f"name{suffix}": alias_prefix + alias})
|
||||||
),
|
),
|
||||||
all=True).union(
|
all=True).union(
|
||||||
queryset.filter(
|
queryset.filter(
|
||||||
Q(normalized_name__iregex="^" + alias.lower())
|
Q(**{f"normalized_name{suffix}": "^" + alias.lower()})
|
||||||
& ~Q(normalized_name__iregex="^" + Alias.normalize(alias))
|
& ~Q(**{f"normalized_name{suffix}": "^" + Alias.normalize(alias)})
|
||||||
& ~Q(name__iregex="^" + alias)
|
& ~Q(**{f"name{suffix}": "^" + alias})
|
||||||
),
|
),
|
||||||
all=True)
|
all=True)
|
||||||
|
|
||||||
@ -106,12 +153,13 @@ class AliasViewSet(ReadProtectedModelViewSet):
|
|||||||
|
|
||||||
|
|
||||||
class ConsumerViewSet(ReadOnlyProtectedModelViewSet):
|
class ConsumerViewSet(ReadOnlyProtectedModelViewSet):
|
||||||
queryset = Alias.objects.all()
|
queryset = Alias.objects
|
||||||
serializer_class = ConsumerSerializer
|
serializer_class = ConsumerSerializer
|
||||||
filter_backends = [SearchFilter, OrderingFilter, DjangoFilterBackend]
|
filter_backends = [RegexSafeSearchFilter, OrderingFilter, DjangoFilterBackend]
|
||||||
search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ]
|
search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ]
|
||||||
filterset_fields = ['note']
|
filterset_fields = ['name', 'normalized_name', 'note', 'note__noteuser__user',
|
||||||
ordering_fields = ['name', 'normalized_name']
|
'note__noteclub__club', 'note__polymorphic_ctype__model', ]
|
||||||
|
ordering_fields = ['name', 'normalized_name', ]
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
"""
|
"""
|
||||||
@ -125,23 +173,27 @@ 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
|
||||||
|
valid_regex = is_regex(alias)
|
||||||
|
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)
|
||||||
|
|
||||||
@ -157,10 +209,11 @@ class TemplateCategoryViewSet(ReadProtectedModelViewSet):
|
|||||||
The djangorestframework plugin will get all `TemplateCategory` objects, serialize it to JSON with the given serializer,
|
The djangorestframework plugin will get all `TemplateCategory` objects, serialize it to JSON with the given serializer,
|
||||||
then render it on /api/note/transaction/category/
|
then render it on /api/note/transaction/category/
|
||||||
"""
|
"""
|
||||||
queryset = TemplateCategory.objects.order_by("name").all()
|
queryset = TemplateCategory.objects.order_by('name')
|
||||||
serializer_class = TemplateCategorySerializer
|
serializer_class = TemplateCategorySerializer
|
||||||
filter_backends = [SearchFilter]
|
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||||
search_fields = ['$name', ]
|
filterset_fields = ['name', 'templates', 'templates__name']
|
||||||
|
search_fields = ['$name', '$templates__name', ]
|
||||||
|
|
||||||
|
|
||||||
class TransactionTemplateViewSet(viewsets.ModelViewSet):
|
class TransactionTemplateViewSet(viewsets.ModelViewSet):
|
||||||
@ -169,11 +222,12 @@ class TransactionTemplateViewSet(viewsets.ModelViewSet):
|
|||||||
The djangorestframework plugin will get all `TransactionTemplate` objects, serialize it to JSON with the given serializer,
|
The djangorestframework plugin will get all `TransactionTemplate` objects, serialize it to JSON with the given serializer,
|
||||||
then render it on /api/note/transaction/template/
|
then render it on /api/note/transaction/template/
|
||||||
"""
|
"""
|
||||||
queryset = TransactionTemplate.objects.order_by("name").all()
|
queryset = TransactionTemplate.objects.order_by('name')
|
||||||
serializer_class = TransactionTemplateSerializer
|
serializer_class = TransactionTemplateSerializer
|
||||||
filter_backends = [SearchFilter, DjangoFilterBackend]
|
filter_backends = [RegexSafeSearchFilter, DjangoFilterBackend, OrderingFilter]
|
||||||
filterset_fields = ['name', 'amount', 'display', 'category', ]
|
filterset_fields = ['name', 'amount', 'display', 'category', 'category__name', ]
|
||||||
search_fields = ['$name', ]
|
search_fields = ['$name', '$category__name', ]
|
||||||
|
ordering_fields = ['amount', ]
|
||||||
|
|
||||||
|
|
||||||
class TransactionViewSet(ReadProtectedModelViewSet):
|
class TransactionViewSet(ReadProtectedModelViewSet):
|
||||||
@ -182,16 +236,18 @@ class TransactionViewSet(ReadProtectedModelViewSet):
|
|||||||
The djangorestframework plugin will get all `Transaction` objects, serialize it to JSON with the given serializer,
|
The djangorestframework plugin will get all `Transaction` objects, serialize it to JSON with the given serializer,
|
||||||
then render it on /api/note/transaction/transaction/
|
then render it on /api/note/transaction/transaction/
|
||||||
"""
|
"""
|
||||||
queryset = Transaction.objects.order_by("-created_at").all()
|
queryset = Transaction.objects.order_by('-created_at')
|
||||||
serializer_class = TransactionPolymorphicSerializer
|
serializer_class = TransactionPolymorphicSerializer
|
||||||
filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
|
filter_backends = [RegexSafeSearchFilter, DjangoFilterBackend, OrderingFilter]
|
||||||
filterset_fields = ["source", "source_alias", "destination", "destination_alias", "quantity",
|
filterset_fields = ['source', 'source_alias', 'source__alias__name', 'source__alias__normalized_name',
|
||||||
"polymorphic_ctype", "amount", "created_at", ]
|
'destination', 'destination_alias', 'destination__alias__name',
|
||||||
search_fields = ['$reason', ]
|
'destination__alias__normalized_name', 'quantity', 'polymorphic_ctype', 'amount',
|
||||||
ordering_fields = ['created_at', 'amount']
|
'created_at', 'valid', 'invalidity_reason', ]
|
||||||
|
search_fields = ['$reason', '$source_alias', '$source__alias__name', '$source__alias__normalized_name',
|
||||||
|
'$destination_alias', '$destination__alias__name', '$destination__alias__normalized_name',
|
||||||
|
'$invalidity_reason', ]
|
||||||
|
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
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ def create_special_notes(apps, schema_editor):
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('note', '0001_initial'),
|
('note', '0001_initial'),
|
||||||
|
('logs', '0001_initial'),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
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',
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user