1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2025-04-27 17:12:38 +00:00

Compare commits

...

830 Commits
v1.0.1 ... main

Author SHA1 Message Date
quark
f0e9a7d3dc Merge branch 'food_traceability' into 'main'
Food traceability

See merge request bde/nk20!308
2025-04-27 09:36:46 +02:00
quark
a2b42c5329 permission, fixture, translation (fr), bug fixes 2025-04-24 20:50:32 +02:00
quark
6d6583bfe6 Rewrite food apps, new feature some changes to model 2025-04-22 19:52:32 +02:00
quark
485d093002 here we go again (better this time) 2025-04-16 17:26:00 +02:00
ehouarn
ff4353d344 Merge branch 'update_invoice_template' into 'main'
Update invoice template

See merge request bde/nk20!307
2025-04-15 18:11:18 +02:00
ehouarn
a90f45bd8b Replace Diolistos.png 2025-04-15 17:38:45 +02:00
ehouarn
10c22ccc53 Replace Diolistos_bg.jpg 2025-04-15 17:38:26 +02:00
quark
6969cee0f3 Merge branch 'time-display' into 'main'
Fixed some non timezone-aware displays

See merge request bde/nk20!303
2025-04-15 17:28:28 +02:00
Ehouarn
ddeada200b Changement logo factures 2025-04-15 17:26:14 +02:00
ehouarn
8e2b24b2da Merge branch 'options_order' into 'main'
Options order

See merge request bde/nk20!306
2025-04-13 23:12:05 +02:00
ehouarn
bd76c280ec Update forms.py 2025-04-13 22:59:04 +02:00
ehouarn
ca0a95ba9e Update transaction_form.html 2025-04-13 22:32:49 +02:00
alexismdr
614f76e699 Add BDA email as an option to fix NL updates
Commit [1] removed default BDA email address from extract_ml_registrations script code. We should now add it to cron.

[1] 3dd5f6e3e0

See merge request bde/nk20!305
2025-04-13 20:17:44 +02:00
alexismdr
a5815f0bc7 Add BDA email as an option to fix NL updates
Commit [1] removed default BDA email address from extract_ml_registrations script code. We should now add it to cron.

[1] 3dd5f6e3e0
2025-04-13 19:56:20 +02:00
bleizi
84e9fea15f linters 2025-04-04 14:46:43 +02:00
alexismdr
b7a660ee40 bootstrap: fix minor issues with profile picture cropping
* Add required [1] "display: block;" style property to img element
* Fix image overflow in modal. As cropper size inherits from img's parent element [2] (including padding according to my research), we need to wrap modal body into another div that has the padding we want.
* Remove ability [3] to click away to dismiss the modal as it often interfered with user interaction when cropping.

[1] https://github.com/fengyuanchen/cropperjs/tree/v1?tab=readme-ov-file#example
[2] https://github.com/fengyuanchen/cropperjs/tree/v1?tab=readme-ov-file#notes
[3] https://getbootstrap.com/docs/4.0/components/modal/#options

See merge request bde/nk20!301
2025-04-04 01:14:46 +02:00
Nicolas Margulies
b9ebb1718a Fixed some non timezone-aware displays 2025-04-04 00:29:22 +02:00
quark
7ba5c76a89 Merge branch 'guests_schools' into 'main'
add school field to guest

See merge request bde/nk20!302
2025-03-25 18:59:46 +01:00
quark
702ddb5679 add school field to guest 2025-03-25 17:39:31 +01:00
Alexis Mercier des Rochettes
93aed87265 bootstrap: fix minor issues with profile picture cropping
* Add required [1] "display: block;" style property to img element
* Fix image overflow in modal. As cropper size inherits from img's parent element [2] (including padding according to my research), we need to wrap modal body into another div that has the padding we want.
* Remove ability [3] to click away to dismiss the modal as it often interfered with user interaction when cropping.

[1] https://github.com/fengyuanchen/cropperjs/tree/v1?tab=readme-ov-file#example
[2] https://github.com/fengyuanchen/cropperjs/tree/v1?tab=readme-ov-file#notes
[3] https://getbootstrap.com/docs/4.0/components/modal/#options

Signed-off-by: Alexis Mercier des Rochettes <apernouille@gmail.com>
2025-03-24 17:36:30 +01:00
bleizi
60355196ce Merge branch 'openid-connect' into 'main'
Openid connect

See merge request bde/nk20!293
2025-03-20 18:42:51 +01:00
bleizi
9bffb32a5e
documentation 2025-03-20 17:36:38 +01:00
quark
5ef019c5c2 Merge branch 'notekfet_wrapped' into 'main'
Rewrite script and add test

See merge request bde/nk20!300
2025-03-18 16:11:33 +01:00
quark
8da62e62fb Rewrite script and add test 2025-03-18 15:53:02 +01:00
thomasl
56a43396d4 Merge branch 'Add_some_permissions' into 'main'
Add some permissions

See merge request bde/nk20!296
2025-03-17 13:16:01 +01:00
thomasl
7966d6f397 Update file initial.json 2025-03-17 13:15:07 +01:00
quark
cb61c511ce Merge branch 'notekfet_wrapped' into 'main'
Another tables and doc

See merge request bde/nk20!299
2025-03-14 00:44:57 +01:00
quark
25bfa575ed Another tables and doc 2025-03-14 00:31:25 +01:00
quark
e21d9fcfbe Merge branch 'notekfet_wrapped' into 'main'
Notekfet wrapped

See merge request bde/nk20!298
2025-03-14 00:11:04 +01:00
quark
b293904525 Another tables and doc 2025-03-13 23:56:10 +01:00
quark
bd7e6b8ad4 add table, add some translation 2025-03-13 21:08:52 +01:00
quark
a208a4fa25 Merge branch 'bde_color' into 'main'
Resize and compress image, add shiny button

See merge request bde/nk20!297
2025-03-13 00:44:45 +01:00
quark
4799b2c52d Resize and compress image, add shiny button 2025-03-12 23:42:37 +01:00
thomasl
562dcfb908 Update file initial.json 2025-03-11 19:34:56 +01:00
thomasl
12ef258ff0 Update file initial.json 2025-03-11 19:27:02 +01:00
thomasl
2ae32ee3b6 Update file initial.json 2025-03-11 19:26:49 +01:00
thomasl
ec1bd45481 Update file initial.json 2025-03-11 19:14:09 +01:00
quark
370a9a069e Merge branch 'bde_color' into 'main'
Rave Part[list] colors

See merge request bde/nk20!295
2025-03-11 10:31:02 +01:00
quark
7f0a3784e9 Rave Part[list] colors 2025-03-11 10:15:11 +01:00
quark
36f4adf2e7 Merge branch 'update_copyright' into 'main'
update copyright

See merge request bde/nk20!292
2025-03-09 18:35:44 +01:00
quark
ae7d5d5489 update copyright 2025-03-09 18:14:58 +01:00
quark
434097aba4 Merge branch 'nerf_pc_kfet' into 'main'
Nerf pc kfet

See merge request bde/nk20!291
2025-03-09 17:58:25 +01:00
quark
a0ebf8658d nerf invalidate perm 2025-03-09 17:34:14 +01:00
quark
423454ba5d nerf PC Kfet perms 2025-03-09 14:37:35 +01:00
quark
3ccb31639c Merge branch 'perm_gc_anti_vss' into 'main'
improve permissions for GC anti-VSS

See merge request bde/nk20!290
2025-03-09 13:28:55 +01:00
quark
5fb12a1388 improve permissions for GC anti-VSS 2025-03-09 13:11:46 +01:00
thomasl
fe029893b0 Merge branch 'permissions_fo_parent_clubs' into 'main'
Permissions fo parent clubs

See merge request bde/nk20!289
2025-03-08 22:29:07 +01:00
thomasl
767e98c2a3 Update file initial.json 2025-03-08 22:05:22 +01:00
thomasl
1bdad76fe9 Update file initial.json 2025-03-08 22:00:46 +01:00
thomasl
0196db7fff Update file initial.json 2025-03-08 21:54:28 +01:00
thomasl
1f53ad4407 Update file initial.json 2025-03-08 21:47:21 +01:00
thomasl
018f6e3f13 Update file initial.json 2025-03-08 21:37:53 +01:00
thomasl
9752a030d9 Update file initial.json 2025-03-08 21:30:25 +01:00
thomasl
b27bdb090d Update file initial.json 2025-03-08 21:30:16 +01:00
thomasl
55a0fbb6cb Update file initial.json 2025-03-08 21:15:56 +01:00
thomasl
c356534309 Update file initial.json 2025-03-08 20:25:33 +01:00
thomasl
51315a0555 Update file initial.json 2025-03-08 20:25:16 +01:00
thomasl
e5f9fe2cf5 Update file initial.json 2025-03-08 20:00:20 +01:00
Nicolas Margulies
6c63c6417c Typesetting 2025-03-08 16:08:40 +01:00
Nicolas Margulies
4563b2b640 Added configusation for OpenID support, along with installation information 2025-03-08 16:04:25 +01:00
quark
c630a3fbd5 Merge branch 'change_template' into 'main'
Change template

See merge request bde/nk20!288
2025-03-07 19:25:27 +01:00
quark
79b8ebeca4 Merge branch 'main' into change_template 2025-03-07 19:09:17 +01:00
quark
dc14ba0101 Suppression article TVA 2025-03-07 18:42:41 +01:00
quark
6028bfeb56 Merge branch 'notekfet_wrapped' into 'main'
Notekfet wrapped

See merge request bde/nk20!287
2025-03-05 22:03:51 +01:00
quark
bd9773a8af change icon 2025-03-05 13:28:55 +01:00
quark
cdeb76d9f8 Merge branch 'main' into notekfet_wrapped 2025-03-04 19:08:32 +01:00
quark
ac4574200d Modify font 2025-03-04 18:45:22 +01:00
quark
b17d31e8ee translation 2025-02-25 14:11:53 +01:00
quark
30d27459dd modify tox.ini to use complex script for make wrapped (bypass C901 in linters) 2025-02-25 01:52:13 +01:00
quark
333f7aa284 update font and minor change 2025-02-24 18:37:18 +01:00
quark
587314e03c linters 2025-02-24 16:10:58 +01:00
thomasl
9f888a5281 Merge branch 'patch_openers_(forgot_something)' into 'main'
Patch openers (forgot something)

See merge request bde/nk20!286
2025-02-18 21:44:21 +01:00
thomasl
88b1a25ca0 Update file initial.json 2025-02-18 21:26:55 +01:00
thomasl
8cb50f58f2 Merge branch 'Respo_jam_permission' into 'main'
Respo jam permission

See merge request bde/nk20!285
2025-02-17 14:48:21 +01:00
thomasl
041a8f20a9 A permission was missing 2025-02-17 14:28:00 +01:00
thomasl
b1ffb28532 Update file initial.json 2025-02-17 14:19:00 +01:00
thomasl
6225fb51f1 Add some permissions 2025-02-17 14:10:21 +01:00
thomasl
1dd74e8024 Merge branch 'openers' into 'main'
Patch Openers

See merge request bde/nk20!284
2025-02-17 02:13:47 +01:00
thomasl
1af9f5f23c some updates 2025-02-17 02:12:44 +01:00
thomasl
83d5a7ceff Update file initial.json 2025-02-17 01:58:13 +01:00
thomasl
a7cba0a4a3 Update file initial.json 2025-02-16 23:33:18 +01:00
thomasl
ccd9a66ab9 Update file initial.json 2025-02-16 23:24:39 +01:00
thomasl
c7a92fa4b2 Update file initial.json 2025-02-16 20:49:11 +01:00
quark
5f1b698d58 Finish script, finish view, make some progress on template 2025-02-16 18:10:53 +01:00
thomasl
0a5368d23f Merge branch 'respo_comm_permissionsV2' into 'main'
Respo comm permissions v2

See merge request bde/nk20!283
2025-02-14 18:38:39 +01:00
thomasl
26b351a51c Add another permission for model guest in activity 2025-02-14 18:14:35 +01:00
thomasl
1836677c47 Update file initial.json 2025-02-13 22:30:36 +01:00
thomasl
e7a98c86f0 Tried something with permissions 2025-02-13 21:51:26 +01:00
thomasl
eb5044490b Delete a useless permission 2025-02-13 21:37:58 +01:00
thomasl
983d7ec052 linters 2025-02-13 21:35:29 +01:00
thomasl
dc56deaf85 Final modifications 2025-02-13 21:17:57 +01:00
quark
19d1ecfc66 continue the script and few change to model 2025-02-13 02:39:33 +01:00
quark
694f54e1c4 Merge branch 'fix_activity_view' into 'main'
fix issue with activity entry view

See merge request bde/nk20!282
2025-02-12 10:18:33 +01:00
quark
b0c3eee699 start to write generate_wrapped script 2025-02-12 00:00:23 +01:00
quark
cd942779ca Wrapped apps 2025-02-11 18:19:24 +01:00
quark
0d0fdef363 fix issue with activity entry view 2025-02-09 17:58:38 +01:00
quark
7ed544b3ac fix issues with activity entry view 2025-02-09 17:50:15 +01:00
thomasl
821efbf78b Merge branch 'Automation_mailing_lists' into 'main'
Automation mailing lists

See merge request bde/nk20!280
2025-02-02 14:53:04 +01:00
thomasl
a209e0d366 Update file forms.py 2025-02-02 14:30:53 +01:00
thomasl
ef485e0628 Update file forms.py 2025-02-02 14:06:22 +01:00
thomasl
1481aa0635 Update file forms.py 2025-02-02 14:05:05 +01:00
thomasl
867bf9fd25 Update file forms.py 2025-02-02 13:33:41 +01:00
thomasl
47fda0ea36 Update file forms.py 2025-02-02 13:17:19 +01:00
thomasl
623290827a Update file forms.py 2025-01-27 16:34:45 +01:00
thomasl
a87ce625f3 Update file note.cron 2025-01-25 13:55:21 +01:00
thomasl
3559787fa7 Merge branch 'New_permission' into 'main'
New permission

See merge request bde/nk20!278
2025-01-18 15:41:15 +01:00
thomasl
bd6ed27ae5 Update 2 files
- /apps/permission/fixtures/initial.json
- /apps/permission/admin.py
2025-01-18 15:11:57 +01:00
thomasl
43dc676747 Update file initial.json 2025-01-18 12:57:42 +01:00
thomasl
caaeab6b0b Update file initial.json 2025-01-17 19:39:26 +01:00
thomasl
54ba786884 Update file initial.json 2025-01-17 19:03:59 +01:00
thomasl
80e109114f Update file initial.json 2025-01-17 18:23:28 +01:00
mcngnt
787005e60d Merge branch 'finito_sda' into 'main'
finitio le message sda

See merge request bde/nk20!279
2025-01-06 00:11:01 +01:00
mcngnt
414e103686 finitio le message sda 2025-01-05 23:17:01 +01:00
thomasl
942d887c2e Update file initial.json 2024-12-23 18:31:11 +01:00
thomasl
a63c34fe37 Update file initial.json 2024-12-22 21:38:17 +01:00
thomasl
2be6133458 Update file initial.json 2024-12-22 20:42:20 +01:00
quark
7975fe47a6 Merge branch 'sda' into 'main'
Donation goal la note kfet x les SdA

See merge request bde/nk20!277
2024-10-10 23:44:22 +02:00
quark
476fbceeea Donation goal la note kfet x les SdA 2024-10-10 01:48:23 +02:00
mcngnt
8fbaa0bdc8 Merge branch 'linters' into 'main'
fix linters for WEI 2024 survey

See merge request bde/nk20!274
2024-10-03 16:51:04 +02:00
thomasl
a0de63effd Merge branch 'beta' into 'main'
Correction translation of sport events ml

See merge request bde/nk20!276
2024-09-18 13:52:33 +02:00
korenstin
09fb1d227e Correction translation of sport events ml 2024-09-18 08:54:04 +02:00
thomasl
2e27d4f05c Merge branch 'non-BDE-members-permission-fix' into 'main'
Added some necessary rights

See merge request bde/nk20!275
2024-09-17 17:24:30 +02:00
Nicolas Margulies
5d16dc4e7d Added some necessary rights 2024-09-17 17:13:47 +02:00
bleizi
3c34033bf5
fix linters for WEI 2024 survey 2024-09-12 13:41:04 +02:00
mcngnt
131f508433 Merge branch 'survey_wei_2024' into 'main'
update hardcoded

See merge request bde/nk20!273
2024-09-12 12:03:10 +02:00
mcngnt
c1a353963a handle hardcoded corrected 2024-09-12 11:36:37 +02:00
mcngnt
178ce2b579 update hardcoded 2024-09-10 22:41:35 +02:00
quark
9162319734 Merge branch 'quark-main-patch-05186' into 'main'
Update views.py (don't display forced blocked note, it's just temporary patch,...

See merge request bde/nk20!272
2024-09-09 21:02:19 +02:00
quark
5d2a8e9b79 Update views.py (don't display forced blocked note, it's just temporary patch, we need to block these note in models too) 2024-09-09 19:05:53 +02:00
bleizi
33c94d0720 Merge branch 'non-BDE-members' into 'main'
Allow non-BDE members to use the note

See merge request bde/nk20!268
2024-09-05 23:15:04 +02:00
bleizi
5040e8e8ea Merge branch 'continuous-intergration' into 'main'
continuous-intergration

See merge request bde/nk20!271
2024-09-05 20:54:40 +02:00
Nicolas Margulies
c5697c4cb4 don't hide the transfer tab 2024-09-05 20:54:23 +02:00
nicomarg
e188c5a153 Merge branch 'mail' into 'main'
mail

Closes #119

See merge request bde/nk20!270
2024-09-05 20:29:30 +02:00
bleizi
94e1fdc93a
add ubuntu 24.4 in tox.ini and remove debian bullseye in gitlab-ci 2024-09-05 20:19:46 +02:00
Nicolas Margulies
d1ef367bab Permissions for child clubs, also changed spaces for tabs 2024-09-05 20:17:45 +02:00
bleizi
0fbb19c5fd
limite mail sending to 10 per minute and purge fail mail log 2024-09-05 19:48:54 +02:00
mcngnt
21cbf2b21a Merge branch 'survey_wei_2024' into 'main'
Survey wei 2024

See merge request bde/nk20!269
2024-08-29 23:10:57 +02:00
mcngnt
185a2cabf2 corrected emoji + linting 2024-08-29 22:47:33 +02:00
mcngnt
7552e55c8d removed diet filed 2024-08-29 22:19:11 +02:00
nicomarg
361de9f8b4 more bug fixing 2024-08-29 21:06:34 +02:00
nicomarg
e2426bd6a6 Bugfix 2024-08-29 20:03:43 +02:00
nicomarg
7fea619a9f add permission to make transfers with members of your club 2024-08-29 20:02:06 +02:00
nicomarg
7b5eefcc0a Update 2 files
- /apps/registration/views.py
- /apps/permission/fixtures/initial.json
2024-08-29 19:23:26 +02:00
mcngnt
e4aa16986f Merge branch 'survey_wei_2024' into 'main'
linting

See merge request bde/nk20!267
2024-08-29 19:12:23 +02:00
mcngnt
b92e6e4e10 linting 2024-08-29 18:36:20 +02:00
mcngnt
dd675b3676 Merge branch 'survey_wei_2024' into 'main'
Survey wei 2024

See merge request bde/nk20!266
2024-08-29 14:45:28 +02:00
mcngnt
f50849b4f8 delete print 2024-08-29 14:01:55 +02:00
mcngnt
73ff35c232 updated bus descr 2024-08-29 12:42:26 +02:00
korenstin
a5df98224f Merge branch 'migration-django-4-2' into 'main'
Migration django 4 2

See merge request bde/nk20!265
2024-08-29 10:49:44 +02:00
korenstin
2cb9ac8735 replace "…" -> "..." (#130) and disable sorting on certain columns (#129) 2024-08-29 10:19:06 +02:00
korenstin
35d4849a28 fix Oauth 2024-08-29 00:43:33 +02:00
mcngnt
96539d262f working html for survey + fixed json error + added specific diet text field 2024-08-29 00:05:44 +02:00
korenstin
946674f59b inclusif, avoids python3.10 syntax 2024-08-28 11:11:32 +02:00
mcngnt
a201d8376a updated survey 2024-08-28 11:01:33 +02:00
korenstin
a21b9275ea Add caution_check in the validation form, #96 2024-08-28 09:48:52 +02:00
korenstin
d4e85e8215 test wei 2024, linters 2024-08-28 09:48:52 +02:00
mcngnt
7af2ebba40 basic survey 2024-08-28 09:48:52 +02:00
quark
bd94400883 Merge branch 'food_traceability' into 'main'
Change deprecated function

See merge request bde/nk20!264
2024-08-27 19:15:52 +02:00
quark
5558341c8c Change deprecated function 2024-08-27 19:14:59 +02:00
quark
35ef82223c Merge branch 'food_traceability' into 'main'
Create traceability application

See merge request bde/nk20!263
2024-08-27 18:46:54 +02:00
korenstin
9ccac36831 Copy constructor 2024-08-27 18:01:13 +02:00
quark
2e71ce05a9 Merge branch 'main' into food_traceability 2024-08-27 17:11:32 +02:00
quark
f2cb10b69f Fix problem in addingredientform, change filter for container in QrcodeForm 2024-08-27 15:12:15 +02:00
bleizi
24c4edf2e3 Merge branch 'migration-django-4-2' into 'main'
nk20 v2.0.0 with django 4.2

See merge request bde/nk20!230
2024-08-27 13:43:58 +02:00
quark
213e9a8b12 Fix problem in addingredientform, change filter for container in QrcodeForm 2024-08-27 10:47:44 +02:00
korenstin
2c56178b15 Merge branch 'main' into migration-django-4-2 2024-08-25 16:14:59 +02:00
korenstin
48a5b04579 Merge branch 'beta' into migration-django-4-2 2024-08-25 16:13:01 +02:00
korenstin
2ab5c4082a Merge branch 'beta' into 'main'
revert sort tables to member views

See merge request bde/nk20!262
2024-08-25 15:17:36 +02:00
korenstin
053225c6dc revert sort tables to member views 2024-08-25 15:13:02 +02:00
korenstin
ac7b86651d Merge branch 'beta' into 'main'
api errors (fix #113), sortable tables, calendar (fix #95), opener (fix #117), colored linters, inclusif, bug july 31, 403 (fix #65)

Closes #65, #117, #95, and #113

See merge request bde/nk20!260
2024-08-25 14:45:08 +02:00
korenstin
21f5a5d566 Merge branch 'invoice_template' into 'main'
Update invoice_sample.tex, remove link toward bde.ens-cachan

See merge request bde/nk20!261
2024-08-25 14:34:37 +02:00
korenstin
ff9c78ed4e added opener in admin and fixed the guest view 2024-08-25 14:29:06 +02:00
quark
1e121297d1 Update invoice_sample.tex, remove link toward bde.ens-cachan 2024-08-23 00:32:37 +02:00
quark
549f56dc0b Translation 2024-08-17 11:58:33 +02:00
quark
debeb33d46 Improve/modify form, view, template. Add permissions 2024-08-17 02:42:29 +02:00
quark
6d7076b03e Edit forms, views, template to improve/modify view. Edit urls to remove some path. Few changes in models. 2024-08-14 01:32:55 +02:00
quark
196df1e775 Remove initial.json (food) mandatory allergen are directly created in migration. Edit tables.py and views.py transformedfoodlist.html to improve/change the view. Edit base.html, urls.py to correct little mistakes. Edit initial.json (permission) to begin permission for food apps and create a new role (Respo Bouffe). 2024-08-13 02:07:32 +02:00
korenstin
28117c8c61 Add developers, Opener comments 2024-08-10 11:50:27 +02:00
bleizi
0d9891fbd8 Merge branch 'migration-django-4-2' of gitlab.crans.org:bde/nk20 into migration-django-4-2 2024-08-09 23:20:48 +02:00
korenstin
4be4a18dd1 Merge branch 'sortable_tables' into 'beta'
Sortable tables

See merge request bde/nk20!257
2024-08-08 17:37:31 +02:00
korenstin
27b00ba4f0 Merge branch 'beta' into sortable_tables 2024-08-08 17:27:44 +02:00
korenstin
3fcbb4f310 Merge branch 'no-api-error' into 'beta'
fix #113

See merge request bde/nk20!253
2024-08-08 17:05:25 +02:00
korenstin
d1c9a2a7f1 Merge branch 'beta' into no-api-error 2024-08-08 16:54:21 +02:00
korenstin
a673fd6871 Merge branch 'ouvreureuse' into 'beta'
Ouvreureuse

See merge request bde/nk20!256
2024-08-08 16:41:06 +02:00
korenstin
a324d3a892 Merge branch 'beta' into ouvreureuse 2024-08-08 16:28:22 +02:00
korenstin
951ba74f8f Merge branch 'bug_31_july' into 'beta'
bug du jour 31 juillet (bissextile)

See merge request bde/nk20!254
2024-08-08 16:23:21 +02:00
korenstin
abc4f14bd1 Merge branch '404_or_403' into 'beta'
fix #65 Returning 403 when you don't have enough permissions

See merge request bde/nk20!259
2024-08-07 21:54:54 +02:00
korenstin
47138bafd4 Merge branch 'traduction_inclusive_fr' into 'beta'
De l'inclusif, partout

See merge request bde/nk20!258
2024-08-07 21:45:05 +02:00
korenstin
a3920fcae3 Merge branch 'Fix_time_zone_calendar.ics' into 'beta'
Update views.py - Fix calendar.ics

See merge request bde/nk20!237
2024-08-07 21:26:32 +02:00
korenstin
ae4213d087 Merge branch 'colored_linters' into 'beta'
Colored linters

See merge request bde/nk20!255
2024-08-07 21:25:22 +02:00
quark
b2b1f03b46 Edit food HTML template for translation, translations. Now the mandatory allergens are automatically created 2024-08-06 15:20:22 +02:00
quark
1c5ed2bd3f Edit base.html and few translations 2024-08-06 13:59:30 +02:00
korenstin
a7e87ea639 API Food 2024-08-04 23:38:21 +02:00
korenstin
cbf92651f0 Returning 403 when you don't have enough permissions 2024-08-04 21:58:57 +02:00
korenstin
12c93ff9da bug du jour 31 juillet (bissextile) 2024-08-04 14:45:17 +02:00
korenstin
354c79bb82 Inclusif manquant 2024-08-04 13:32:33 +02:00
korenstin
1ea7b3dda1 documentation and modification of permissions 2024-08-02 15:21:34 +02:00
korenstin
35ffbfcf55 Colored linters 2024-08-01 17:29:24 +02:00
korenstin
162371042c Creation of "Opener", Fix #117 2024-08-01 14:49:52 +02:00
korenstin
581715d804 Fix #95 (calendar) 2024-07-31 23:18:41 +02:00
korenstin
c7c6f0350f Looks unused 2024-07-31 22:19:16 +02:00
korenstin
9d1024024b Each table can be sorted (with a few exceptions) 2024-07-30 21:42:45 +02:00
d595d908c6 Fix tests
Signed-off-by: Emmy D'ANELLO <ynerant@crans.org>
2024-07-30 16:34:20 +02:00
734f5b242d C'est pas moi
Signed-off-by: Emmy D'ANELLO <ynerant@crans.org>
2024-07-30 16:32:19 +02:00
b0c7d43a50 De l'inclusif, partout
Signed-off-by: Emmy D'ANELLO <ynerant@crans.org>
2024-07-30 16:28:47 +02:00
korenstin
6f67d2c629 Documentation 2024-07-22 15:52:09 +02:00
korenstin
4b97ab2e2a linters 2024-07-22 15:52:09 +02:00
korenstin
dcfd0167e7 Security against the cycles 2024-07-22 15:52:09 +02:00
korenstin
50a680eed2 Open table and shelf life 2024-07-22 15:52:09 +02:00
korenstin
226a2a6357 Automatic allergens and expiry_date update 2024-07-22 15:52:09 +02:00
korenstin
48462f2ffc Adding ingredients to a preparation 2024-07-22 15:52:09 +02:00
korenstin
260513ae3b Migration fixes 2024-07-22 15:52:09 +02:00
korenstin
210a3cc93c Implementing QRcode creation, modifying Allergen model and creating of few views 2024-07-22 15:52:09 +02:00
quark
896095a44c Un peu de nettoyage, rajout de commentaires 2024-07-22 15:52:09 +02:00
quark
3f997f94fa few changes in models, delete default label 2024-07-22 15:52:09 +02:00
quark
0801ad64ae création de forms fonctionnel (form + views + url + html), few changes in models.py 2024-07-22 15:52:09 +02:00
quark
64bd5ed546 création d'un form pour l'ajout d'aliments basiques 2024-07-22 15:52:09 +02:00
quark
4c390dce17 nom app 2024-07-22 15:52:09 +02:00
quark
adacc293f5 First forms 2024-07-22 15:52:09 +02:00
quark
968fa64d37 Réagencement des tables et de leurs attributs 2024-07-22 15:52:09 +02:00
quark
a481adbae4 création de l'interface admin temporaire 2024-07-22 15:52:09 +02:00
quark
4de2e987ef Rajout de la pseudo-doc 2024-07-22 15:52:07 +02:00
quark
9e6342c929 Création de l'apps et de la base de donnée 2024-07-22 15:50:09 +02:00
quark
74de358953 Update README.md 2024-07-22 15:50:09 +02:00
korenstin
7322d55789 Fix #113. Fix regex in views. 2024-07-19 20:00:33 +02:00
1a258dfe9e Parse input of search filters to prevent errors based on invalid regex, fixes #113
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2024-07-19 19:59:30 +02:00
korenstin
b8f81048a5 Merge branch 'fix_ActivityList' into 'main'
Allow to order the 2 tables and to fix the bug of several activities

See merge request bde/nk20!252
2024-07-18 18:17:06 +02:00
korenstin
af819f45a1 Merge branch 'remove_picture' into 'main'
Allow you to delete the profile picture

See merge request bde/nk20!250
2024-07-18 18:02:43 +02:00
korenstin
076d065ffa Merge branch 'main' into 'remove_picture'
# Conflicts:
#   locale/fr/LC_MESSAGES/django.po
2024-07-18 17:52:22 +02:00
korenstin
2da77d9c17 Merge branch 'fix_join_bda' into 'main'
Fix #126 (join_bda)

Closes #126

See merge request bde/nk20!251
2024-07-18 17:14:23 +02:00
korenstin
01584d6330 Merge branch 'modif_perm' into 'main'
Modif perm

See merge request bde/nk20!249
2024-07-18 16:54:23 +02:00
korenstin
4c0a5922c4 Allow to order the 2 tables and to fix the bug of several activities 2024-07-15 22:06:11 +02:00
korenstin
f90b28fc7c Fix #126 (join_bda) 2024-07-15 14:30:46 +02:00
korenstin
bbbdcc7247 linters 2024-07-13 18:03:19 +02:00
korenstin
925e0f26f5 Allow you to delete the profile picture 2024-07-13 17:37:19 +02:00
korenstin
feeb99041f Fix the Alias Search API 2024-07-13 12:41:59 +02:00
quark
c912383f86 oups la virgule oublié 2024-06-24 22:36:22 +02:00
quark
32830e43fd Modify permission for negative 2024-06-24 21:21:22 +02:00
korenstin
11c6a6fa7a modifications permissions consommation pc kfet (Alcool) 2024-06-24 16:57:39 +02:00
korenstin
201d6b114a Merge branch 'new_logo' into 'main'
New logo

See merge request bde/nk20!247
2024-06-03 22:00:03 +02:00
korenstin
19e77df299 Merge branch 'main' into 'new_logo'
# Conflicts:
#   .gitlab-ci.yml
2024-06-03 21:59:44 +02:00
korenstin
5fd6ec5668 Merge branch 'charte_info' into 'main'
Charte info

See merge request bde/nk20!248
2024-06-03 21:53:01 +02:00
korenstin
10a01c5bc2 linters 2024-05-30 20:21:56 +02:00
korenstin
989905ea64 Update .gitlab-ci.yml 2024-05-26 18:41:49 +02:00
korenstin
0218d43a17 Update .gitlab-ci.yml 2024-05-26 16:00:26 +02:00
test
5d30b0e819 charte info 2024-05-26 15:46:50 +02:00
korenstin
ec759dd3c0 error py37-django22 2024-05-23 22:38:09 +02:00
korenstin
2eb965291d new_logo 2024-05-23 21:46:01 +02:00
quark
7f182ee2ee Merge branch 'traduction_inclusive_fr' into 'main'
Réécriture en inclusif de l'ensemble des textes français de la note

See merge request bde/nk20!246
2024-03-30 13:24:06 +01:00
quark
3132aa4c38 Prise en compte des commentaires de Korenstin 2024-03-30 12:44:51 +01:00
quark
c7eb774859 Prise en compte des commentaires 2024-03-30 11:20:23 +01:00
quark
32f8d285b3 Prise en compte des commentaires 2024-03-30 11:12:33 +01:00
quark
050256ea13 Réécriture en inclusif de l'ensemble des textes français de la note 2024-03-29 17:59:43 +01:00
quark
7afd15b1cc Merge branch 'invoice_modification' into 'main'
changement template facture

Closes #128

See merge request bde/nk20!243
2024-03-27 19:10:40 +01:00
korenstin
258361f116 Update forms.py 2024-03-27 10:25:38 +01:00
korenstin
a307530579 Merge branch 'change_date' into 'main'
change date

See merge request bde/nk20!245
2024-03-27 10:19:37 +01:00
quark
5de930bf40 Update forms.py 2024-03-27 10:04:14 +01:00
quark
f7ebe0e99b Update forms.py 2024-03-27 09:43:49 +01:00
quark
73de6e2176 Update forms.py 2024-03-27 09:20:32 +01:00
test
201611b105 change date 2024-03-26 08:33:34 +01:00
quark
40c239e9da Update models.py 2024-03-24 16:41:18 +01:00
quark
2aaab2b454 Update test_treasury.py 2024-03-24 15:55:46 +01:00
quark
fc088dec86 Update test_treasury.py 2024-03-24 15:20:46 +01:00
korenstin
2d60f1fd7b Merge branch 'patch_sort' into 'main'
patch sort and optional description

See merge request bde/nk20!244
2024-03-23 21:07:03 +01:00
test
7b48b09329 patch sort and optional description 2024-03-23 14:32:31 +01:00
quark
ffac940511 changement template facture 2024-03-22 18:22:08 +01:00
mcngnt
50f98fd5ad Merge branch 'prez-perm' into 'main'
changed permission for club president

See merge request bde/nk20!242
2024-03-22 12:56:09 +01:00
mcngnt
402e19d1ce changed permission for club president 2024-03-22 12:27:08 +01:00
korenstin
0b0394b61f Merge branch 'image_fix' into 'main'
réparation photo de profil

See merge request bde/nk20!241
2024-03-21 20:57:56 +01:00
test
98422d8259 réparation photo de profil 2024-03-21 18:37:47 +01:00
quark
29509b5b26 Merge branch 'quark-main-patch-96792' into 'main'
Changement couleur de la note

See merge request bde/nk20!240
2024-03-14 17:38:29 +01:00
quark
0d64ad31e0 Update custom.css 2024-03-14 17:22:41 +01:00
quark
5781cbd6a5 Merge branch 'quark-main-patch-83351' into 'main'
Changement couleur de la note

See merge request bde/nk20!239
2024-03-14 16:16:37 +01:00
quark
5295e61a00 Changement couleur de la note 2024-03-14 15:59:53 +01:00
quark
e79ed6226a Merge branch 'quark-main-patch-51348' into 'main'
Upload New Migration (change bde)

See merge request bde/nk20!238
2024-03-11 16:28:41 +01:00
quark
68152e6354 Upload New Migration (change bde) 2024-03-11 16:11:54 +01:00
charliep
6c61daf1c5 Update views.py
Passage à la time zone Europe/Paris
2024-03-11 10:25:48 +01:00
quark
b8cc297baf Merge branch 'quark-main-patch-c661' into 'main'
Update facture template

See merge request bde/nk20!236
2024-03-09 16:25:45 +01:00
quark
cd8224f2e0 Upload New File 2024-03-09 16:06:39 +01:00
quark
3c882a7854 Delete RavePartlist_bg.png 2024-03-09 16:06:01 +01:00
quark
357e1bbaa2 Replace RavePartlist_bg.png 2024-03-09 16:05:29 +01:00
quark
f5c4c58525 Replace RavePartlist_bg.png 2024-03-09 14:03:17 +01:00
quark
dafb602b08 Update models.py 2024-03-09 13:40:45 +01:00
quark
5b377e6a75 Update facture template 2024-03-09 13:04:33 +01:00
bleizi
28bd62531e Merge branch 'docs-append' into 'main'
Add : Documentation years flag for Extract ML Registrations

See merge request bde/nk20!235
2024-03-08 19:51:12 +01:00
rlali
b3a31c27a5 Add : Documentation years flag for Extract ML Registrations 2024-03-08 19:34:48 +01:00
bleizi
c7a8e6a1a5 Merge branch 'fin_de_campagne' into 'main'
Remove BDE compaign banner

See merge request bde/nk20!234
2024-02-16 16:58:42 +01:00
bleizi
546a3a72b1
Remove BDE compaign banner 2024-02-15 10:32:39 +01:00
charliep
2e5664f79d Merge branch 'Compromis' into 'main'
Update base.html compromis

See merge request bde/nk20!233
2024-02-13 23:27:32 +01:00
charliep
e367666fe9 Update base.html compromis 2024-02-13 23:27:11 +01:00
charliep
04a9b3daf0 Merge branch 'Revanche' into 'main'
Update base.html

See merge request bde/nk20!232
2024-02-13 21:25:16 +01:00
charliep
d1df8f3eac Update base.html
📢Pour la meilleure liste BDE
2024-02-13 21:24:23 +01:00
nicomarg
a5221f66ef Merge branch 'main' into 'main'
Compaign banner

See merge request bde/nk20!231
2024-02-13 14:58:31 +01:00
mcngnt
7d59cd6cd2 Compaign banner 2024-02-13 14:26:28 +01:00
bleizi
96215cc1ff oidc_claim_scope in Class instead of method 2024-02-13 13:43:14 +01:00
bleizi
b7a71d911d _get_validtion_exclusions() now return a set, PIL.Image.ANTIALIAS was renamed LANCZOS and typo in .gitlab-ci.yml 2024-02-12 22:56:43 +01:00
bleizi
2ee7f41dfe tests with ubuntu 22.04, django-bootstrap-datepicker-plus is a standalone package and fix encoding in tests 2024-02-12 21:25:07 +01:00
bleizi
fb3337966e
bootstrap4 is now a standalone package from crispy-forms 2024-02-11 22:24:37 +01:00
bleizi
0db0474217 Merge branch 'Update_2024_Copyright' into 'main'
Update 131 files

See merge request bde/nk20!229
2024-02-11 17:29:46 +01:00
bleizi
2b3eb15f59
fix one copyright and a string before merge 2024-02-11 16:58:53 +01:00
bleizi
399a32bece
default auto field 2024-02-11 16:51:48 +01:00
bleizi
82fea65b5e
django_htcpcp_tea in middleware only if in apps 2024-02-07 20:03:57 +01:00
bleizi
abc88d0118
replace url from django.conf.urls by re_path from django.urls 2024-02-07 18:21:08 +01:00
bleizi
b6b81a8b8f
typo 2024-02-07 18:05:32 +01:00
bleizi
d228dbf225
fix some breaking changes and linters 2024-02-07 18:02:56 +01:00
charliep
a6b479db19 Update 131 files
- /apps/activity/api/serializers.py
- /apps/activity/api/urls.py
- /apps/activity/api/views.py
- /apps/activity/tests/test_activities.py
- /apps/activity/__init__.py
- /apps/activity/admin.py
- /apps/activity/apps.py
- /apps/activity/forms.py
- /apps/activity/tables.py
- /apps/activity/urls.py
- /apps/activity/views.py
- /apps/api/__init__.py
- /apps/api/apps.py
- /apps/api/serializers.py
- /apps/api/tests.py
- /apps/api/urls.py
- /apps/api/views.py
- /apps/api/viewsets.py
- /apps/logs/signals.py
- /apps/logs/apps.py
- /apps/logs/__init__.py
- /apps/logs/api/serializers.py
- /apps/logs/api/urls.py
- /apps/logs/api/views.py
- /apps/member/api/serializers.py
- /apps/member/api/urls.py
- /apps/member/api/views.py
- /apps/member/templatetags/memberinfo.py
- /apps/member/__init__.py
- /apps/member/admin.py
- /apps/member/apps.py
- /apps/member/auth.py
- /apps/member/forms.py
- /apps/member/hashers.py
- /apps/member/signals.py
- /apps/member/tables.py
- /apps/member/urls.py
- /apps/member/views.py
- /apps/note/api/serializers.py
- /apps/note/api/urls.py
- /apps/note/api/views.py
- /apps/note/models/__init__.py
- /apps/note/static/note/js/consos.js
- /apps/note/templates/note/mails/negative_balance.txt
- /apps/note/templatetags/getenv.py
- /apps/note/templatetags/pretty_money.py
- /apps/note/tests/test_transactions.py
- /apps/note/__init__.py
- /apps/note/admin.py
- /apps/note/apps.py
- /apps/note/forms.py
- /apps/note/signals.py
- /apps/note/tables.py
- /apps/note/urls.py
- /apps/note/views.py
- /apps/permission/api/serializers.py
- /apps/permission/api/urls.py
- /apps/permission/api/views.py
- /apps/permission/templatetags/perms.py
- /apps/permission/tests/test_oauth2.py
- /apps/permission/tests/test_permission_denied.py
- /apps/permission/tests/test_permission_queries.py
- /apps/permission/tests/test_rights_page.py
- /apps/permission/__init__.py
- /apps/permission/admin.py
- /apps/permission/backends.py
- /apps/permission/apps.py
- /apps/permission/decorators.py
- /apps/permission/permissions.py
- /apps/permission/scopes.py
- /apps/permission/signals.py
- /apps/permission/tables.py
- /apps/permission/urls.py
- /apps/permission/views.py
- /apps/registration/tests/test_registration.py
- /apps/registration/__init__.py
- /apps/registration/apps.py
- /apps/registration/forms.py
- /apps/registration/tables.py
- /apps/registration/tokens.py
- /apps/registration/urls.py
- /apps/registration/views.py
- /apps/treasury/api/serializers.py
- /apps/treasury/api/urls.py
- /apps/treasury/api/views.py
- /apps/treasury/templatetags/escape_tex.py
- /apps/treasury/tests/test_treasury.py
- /apps/treasury/__init__.py
- /apps/treasury/admin.py
- /apps/treasury/apps.py
- /apps/treasury/forms.py
- /apps/treasury/signals.py
- /apps/treasury/tables.py
- /apps/treasury/urls.py
- /apps/treasury/views.py
- /apps/wei/api/serializers.py
- /apps/wei/api/urls.py
- /apps/wei/api/views.py
- /apps/wei/forms/surveys/__init__.py
- /apps/wei/forms/surveys/base.py
- /apps/wei/forms/surveys/wei2021.py
- /apps/wei/forms/surveys/wei2022.py
- /apps/wei/forms/surveys/wei2023.py
- /apps/wei/forms/__init__.py
- /apps/wei/forms/registration.py
- /apps/wei/management/commands/export_wei_registrations.py
- /apps/wei/management/commands/import_scores.py
- /apps/wei/management/commands/wei_algorithm.py
- /apps/wei/templates/wei/weilist_sample.tex
- /apps/wei/tests/test_wei_algorithm_2021.py
- /apps/wei/tests/test_wei_algorithm_2022.py
- /apps/wei/tests/test_wei_algorithm_2023.py
- /apps/wei/tests/test_wei_registration.py
- /apps/wei/__init__.py
- /apps/wei/admin.py
- /apps/wei/apps.py
- /apps/wei/tables.py
- /apps/wei/urls.py
- /apps/wei/views.py
- /note_kfet/settings/__init__.py
- /note_kfet/settings/base.py
- /note_kfet/settings/development.py
- /note_kfet/settings/secrets_example.py
- /note_kfet/static/js/base.js
- /note_kfet/admin.py
- /note_kfet/inputs.py
- /note_kfet/middlewares.py
- /note_kfet/urls.py
- /note_kfet/views.py
- /note_kfet/wsgi.py
- /entrypoint.sh
2024-02-07 02:26:49 +01:00
charliep
048d251f75 Merge branch 'charliep-main-patch-40779' into 'main'
update Copyright 2024

See merge request bde/nk20!228
2024-02-07 02:05:59 +01:00
charliep
7b11cb0797 update Copyright 2024 2024-02-07 01:37:43 +01:00
bleizi
516a7f4be5
Remove importation of django-htcpcp-tea which is not compatible with django 4.2 2024-01-24 20:14:32 +01:00
bleizi
2f8c9b54e7
Remove importation of django-cas-server which is not compatible with django 4.2 2024-01-24 19:58:55 +01:00
bleizi
e9f18c3ed9
migrate to django 4.2 (LTS), change requirement and tests. remove depreciated ifnotequal 2024-01-24 19:18:02 +01:00
bleizi
ff3c30517e Merge branch 'happy-new-year' into 'main'
happy new year

See merge request bde/nk20!226
2024-01-11 16:48:06 +01:00
bleizi
f481ea6acb
happy new year (contain annually WEI change and update to follow Django Style Guide) 2024-01-11 16:32:37 +01:00
nicomarg
802fd8c2d7 Merge branch 'search_conso_bugfix' into 'main'
Bugfix

See merge request bde/nk20!225
2023-11-13 14:29:29 +01:00
Nicolas Margulies
5209a586a9 Fixed const being redeclared when script is reevaluated 2023-11-08 17:10:05 +01:00
nicomarg
24f54ac876 Merge branch 'search-conso' into 'main'
Added a search tab for the conso page, fixes #58

Closes #58

See merge request bde/nk20!224
2023-10-27 16:45:41 +02:00
Nicolas Margulies
988b4c9e88 Linting 2023-10-26 21:03:48 +02:00
Nicolas Margulies
e32c267995 Moved js code to the external conso file 2023-10-26 19:10:43 +02:00
Nicolas Margulies
5e39209ab1 Made searchbar completely client-based 2023-10-26 19:01:09 +02:00
Nicolas Margulies
08b2fabe07 Removing jquery means changing the event API... 2023-10-26 00:22:51 +02:00
Nicolas Margulies
405479e5ad Execute script to add behavior to searched buttons 2023-10-26 00:10:56 +02:00
Nicolas Margulies
0cc130092f Added a search tab for the conso page 2023-10-25 20:01:48 +02:00
charliep
ff6e207512 Merge branch 'beta' into 'main'
check for a model in permission and use that in treasury

See merge request bde/nk20!222
2023-09-29 12:08:00 +02:00
bleizi
0f1e4d2e60
check for a model in permission and use that in treasury 2023-09-28 18:48:57 +02:00
nicomarg
6255bcbbb1 Merge branch 'beta' into 'main'
Merge beta

See merge request bde/nk20!221
2023-09-27 17:14:49 +02:00
Nicolas Margulies
d82a1001c4 Moved transaction through frienships right to basic rights 2023-09-27 16:55:00 +02:00
Nicolas Margulies
31a54482f0 Updated doc to tell maintainers to create psql superusers 2023-09-27 16:53:30 +02:00
nicomarg
4ee02345d4 Merge branch 'better-friendship-view' into 'main'
Rework of the friendships page

See merge request bde/nk20!220
2023-09-21 15:48:00 +02:00
bleizi
422c087d17
fix wei test 2023-09-20 07:04:13 +02:00
Nicolas Margulies
30d6e2c95e Added trusts to note admin site 2023-09-19 15:07:30 +02:00
Nicolas Margulies
f3a3f07e38 Tweaked message and did missing french translations 2023-09-18 17:29:52 +02:00
Nicolas Margulies
a5e802f370 Improved the error message when trying to duplicate a Trust 2023-09-18 17:12:31 +02:00
Nicolas Margulies
540f3bc354 regenerated messages so locations are consistent with codebase 2023-09-02 00:04:54 +02:00
elkmaennchen
2d19457506 Add spanish translation for friendship 2023-09-01 17:35:52 +02:00
Nicolas Margulies
72786d0d2b Translated js strings, unified some case 2023-09-01 17:34:52 +02:00
Nicolas Margulies
f099cbc879 Linting 2023-09-01 17:32:29 +02:00
Nicolas Margulies
977eb7c0d4 Generated translation files, did french 2023-09-01 17:30:38 +02:00
Nicolas Margulies
d81b1f2710 Tweaked trust back display 2023-09-01 17:15:24 +02:00
Nicolas Margulies
6a69590a82 Added a 'trust back' button, front can be improved 2023-09-01 17:15:24 +02:00
Nicolas Margulies
7afc583282 Made trust adding widget resetable, corrected the unexpected empty field behavior and improved autocomplete's responsiveness 2023-09-01 17:15:24 +02:00
Nicolas Margulies
4fb0b7d736 First pass on a display of users trusting you, added a corresponding right 2023-09-01 17:15:13 +02:00
bleizi
18a5b65a1c Merge branch 'VSS' into 'main'
anti VSS

See merge request bde/nk20!219
2023-08-31 15:58:52 +02:00
bleizi
f545af4977
typo 2023-08-31 15:40:49 +02:00
bleizi
103e2d0635
add GC anti-VSS 2023-08-31 15:25:44 +02:00
bleizi
aedf0e87ba
prez BDE can block note 2023-08-31 13:46:27 +02:00
bleizi
dab45b5fd4
translation 2023-08-31 13:40:53 +02:00
bleizi
b3353b563c
add VSS checkbox on registration 2023-08-31 12:21:38 +02:00
bleizi
6bc52be707 Merge branch 'WEI_with_questions' into 'main'
Wei with questions

See merge request bde/nk20!218
2023-08-31 12:01:39 +02:00
charliep
834d68fe35 typo 2023-08-31 11:45:17 +02:00
bleizi
c6a2849d35
test 2023-08-30 16:16:29 +02:00
bleizi
4ab22c92b3
After WEI registration validation, come back to unvalidate registration page 2023-08-30 09:52:17 +02:00
bleizi
c328c1457c
add register button at the end of WEI registration 2023-08-28 22:27:45 +02:00
bleizi
96da7d01ae
change on a field that everyone have (1A don't have bus) 2023-08-28 19:26:51 +02:00
bleizi
d27f942339
typo 2023-08-28 10:13:28 +02:00
bleizi
738d6c932d
questions ! 2023-08-28 00:42:33 +02:00
bleizi
1760196578
more tests 2023-08-27 23:11:40 +02:00
bleizi
13b9b6edea
tests 2023-08-27 18:09:46 +02:00
bleizi
e06e3b2972
one question by page 2023-08-26 23:47:10 +02:00
bleizi
9596aa7b8c
base for questions instead of words 2023-08-26 17:52:48 +02:00
bleizi
ba0d64f0d4 Merge branch 'new_default_year' into 'main'
new default year

See merge request bde/nk20!217
2023-08-23 23:53:45 +02:00
bleizi
8d17801e28
new default year 2023-08-23 23:32:01 +02:00
bleizi
609362c4f8 Merge branch 'update_permission' into 'main'
Update permission

See merge request bde/nk20!216
2023-08-23 22:50:24 +02:00
bleizi
03d2d5f03e
change -50€ to -20€ and doc 2023-08-22 21:51:02 +02:00
bleizi
d2057a9f45
remove respo-info perm and change Prez BDE prem 2023-08-22 21:19:05 +02:00
charliep
b6e68eeebe Merge branch 'charliep-main-patch-47507' into 'main'
Update forms.py - Homogénéisation des cases

See merge request bde/nk20!215
2023-08-08 15:39:44 +02:00
charliep
6410542027 Update forms.py - Homogénéisation des cases 2023-08-08 15:38:29 +02:00
bleizi
6b1cd3ba7a
manage self aliases for BDE member instead of kfet 2023-07-24 12:42:44 +02:00
bleizi
9f114b8ca2
fixtures activities 2023-07-24 12:26:34 +02:00
bleizi
e0132b6dc8
migration permission 2023-07-24 12:20:16 +02:00
bleizi
f1cc82fab3 Merge branch 'linters' into 'main'
Linters

See merge request bde/nk20!214
2023-07-17 09:27:22 +02:00
bleizi
644cf14c4b missing brackets 2023-07-17 09:11:25 +02:00
bleizi
f19a489313
linters (removing B019) 2023-07-17 08:50:10 +02:00
bleizi
dedd6c69cc
new commits in nk20-scripts 2023-07-17 06:58:01 +02:00
charliep
b42f5afeab Merge branch 'registration2023' into 'main'
Registration2023

See merge request bde/nk20!213
2023-07-16 17:12:33 +02:00
bleizi
31e67ae3f6
typo 2023-07-09 16:06:30 +02:00
bleizi
b08da7a727
help text on WEI emergency contact 2023-07-09 14:57:48 +02:00
bleizi
451aa64f33
Unisexe clothing cut 2023-07-09 12:30:23 +02:00
bleizi
3c99b0f3e9
do not change transactions date when validating/deleting credit-soge (and typo) 2023-07-09 11:23:33 +02:00
bleizi
201a179947
linters 2023-07-09 10:36:36 +02:00
bleizi
96784aee3b
remove (comment) soge from registration 2023-07-07 21:44:18 +02:00
bleizi
981c4d0300
fix update of club membership start/end date 2023-07-07 20:39:19 +02:00
bleizi
11223430fd Merge branch 'WEI2023' into 'main'
Préparation WEI 2023

See merge request bde/nk20!212
2023-07-04 19:17:17 +02:00
charliep
7aeb977e72 Oubli dans le fichier test_wei_registration_.py d'un 2022 en 2023 2023-07-04 18:33:54 +02:00
charliep
52fef1df42 Préparation WEI 2023 2023-07-04 18:23:43 +02:00
bleizi
16f8a60a3f
possibilité de l'adhésion au BDA lors de l'inscription 2023-07-04 17:32:48 +02:00
bleizi
2839d3de1e
club facultatif pour un role lors du changement dans l'interface admin 2023-06-22 14:52:11 +02:00
bleizi
30afa6da0a
création d'une permission pour faire les crédits uniquement 2023-06-12 18:29:23 +02:00
bleizi
84fc77696f
see activities: BDE members instead of kfet 2023-06-05 19:04:19 +02:00
bleizi
19fc620d1f
see kfet members' note for respot 2023-06-05 17:26:49 +02:00
charliep
d5819ac562 Merge branch 'FAQ' into 'main'
Ajout d'un lien vers la FAQ de la note.

See merge request bde/nk20!209
2023-04-18 15:51:38 +02:00
bleizi
a79df8f1f6 Merge branch 'invoice_bg_storlist' into 'main'
changement du fond des factures

See merge request bde/nk20!211
2023-04-14 19:29:26 +02:00
Théo Le Moigne
364b18e188
migrations 2023-04-14 16:52:46 +02:00
Hugo
10a883b2e5 new treasury phone number 2023-04-14 16:00:48 +02:00
misterkrafts
1410ab6c4f Almost on time, the SIRET number is now changed 2023-04-14 15:35:18 +02:00
misterkrafts
623dd61be6 Remove phone number 2023-04-14 14:56:34 +02:00
Hugo
48a0a87e7c changement du fond des factures 2023-04-14 00:25:26 +02:00
bleizi
563f525b11 Merge branch 'cron' into 'main'
fréquence des mails de négatif aux trez : 1 mois -> 1 semaine, et les notes liées au BDE n'apparaissent plus

See merge request bde/nk20!210
2023-04-08 13:04:59 +02:00
misterkrafts
63c1d74f1a Ignore notes containing '- BDE-' in the list of negative balances 2023-04-07 15:47:06 +02:00
Théo Le Moigne
c42fb380a6
frequence des mails de négatif aux trez : 1 mois -> 1 semiane 2023-04-06 09:04:27 +02:00
Théo Le Moigne
c636d52a73
traduction (allemand et espagnol probablement pas optimal) 2023-03-31 17:21:58 +02:00
Otthorn
6a9021ec14 Merge branch 'couleur_totalist_spies' into 'main'
Couleur totalist spies

See merge request bde/nk20!208
2023-03-31 12:37:24 +02:00
charliep
9c9149b53a Ajout d'un lien vers la FAQ de la note. 2023-03-31 12:34:14 +02:00
misterkrafts
cb74311e7b Commit migration, j'étais triggered 2023-03-30 19:14:52 +02:00
misterkrafts
9d7dd566c9 Ignore /tmp/ 2023-03-30 17:26:06 +02:00
Théo Le Moigne
6bceb394c5
prez BDE sould see invoice list 2023-03-29 20:43:54 +02:00
Théo Le Moigne
62cf8f9d84
forgetted coma 2023-03-28 20:41:53 +02:00
parpaing
9944ebcaad changement des couleurs de la note vers les couleurs totalist spies 2023-03-25 02:13:16 +01:00
parpaing
8537f043f7 changement des couleurs de la note vers les couleurs totalist spies 2023-03-25 00:57:19 +01:00
Théo Le Moigne
2dd1c3fb89
change mask for some perm 2023-03-20 22:35:51 +01:00
Théo Le Moigne
c8665c5798
change permissions for role 2023-03-20 22:21:18 +01:00
Théo Le Moigne
e9f1b6f52d
change permanent permissions 2023-03-20 17:19:14 +01:00
Théo Le Moigne
1d95ae4810
sort perm by number 2023-03-20 16:16:32 +01:00
bleizi
c89a95f8d2 Merge branch 'invoice-logo-totalist' into 'main'
changement du fond des factures

See merge request bde/nk20!207
2023-01-30 13:06:39 +01:00
parpaing
73640b1dfa changement du fond des factures 2023-01-30 00:06:45 +01:00
bleizi
84b16ab603 Merge branch 'SogeCreditDate' into 'main'
link SogeCredit to WEI by creation date instead of civil year

See merge request bde/nk20!206
2023-01-17 15:58:52 +01:00
bleizi
6a1b51dbbf Merge branch 'api_pagination' into 'main'
Add custom pagination size as an API parameter

See merge request bde/nk20!205
2023-01-11 22:46:13 +01:00
Théo Le Moigne
c441a43a8b
link SogeCredit to WEI by creation date instead of civil year 2023-01-10 21:40:03 +01:00
Otthorn
87f3b51b04 Add custom pagination size as an API parameter 2022-12-14 18:37:13 +01:00
bleizi
0a853fd3e6 Merge branch 'permission_trez' into 'main'
fix trez perm

See merge request bde/nk20!204
2022-12-10 14:41:57 +01:00
Théo Le Moigne
c429734810
fix bug 2022-11-12 14:51:22 +01:00
bleizi
5d759111b6 Merge branch 'weiWords' into 'main'
change wei words

See merge request bde/nk20!203
2022-09-05 13:24:24 +02:00
Théo Le Moigne
70baf7566c
change wei words 2022-09-05 13:20:00 +02:00
bleizi
eb355f547c Merge branch 'SogeNotForMembership' into 'main'
Soge not for membership

See merge request bde/nk20!202
2022-09-04 22:56:07 +02:00
Yoann Beaugnon
7068170f18 fixing grammar in comments 2022-09-04 13:24:39 +02:00
Théo Le Moigne
45ee9a8941
Soge only payd WEI (not bde/kfet membership) 2022-09-04 12:52:40 +02:00
Théo Le Moigne
454ea19603
hide Soge during registration 2022-09-04 12:31:08 +02:00
5a77a66391 Merge branch 'beta' into 'main'
Friendships

See merge request bde/nk20!200
2022-04-13 12:45:06 +02:00
elkmaennchen
761fc170eb
Update Spanish translation 2022-04-13 12:30:22 +02:00
Nicolas Margulies
ac23d7eb54
Generated translation files for de/es (but didn't translate anything) 2022-04-13 12:30:22 +02:00
Nicolas Margulies
40e7415062
Added translations for friendships 2022-04-13 12:30:22 +02:00
Nicolas Margulies
319405d2b1
Added a message to explain what frendships do
Signed-off-by: Nicolas Margulies <nicomarg@crans.org>
2022-04-13 12:30:22 +02:00
Nicolas Margulies
633ab88b04
Linting 2022-04-13 12:30:22 +02:00
Nicolas Margulies
e29b42eecc
Add permissions related to trusting 2022-04-13 12:30:22 +02:00
Nicolas Margulies
dc69faaf1d
Better user search to add friendships 2022-04-13 12:30:22 +02:00
Nicolas Margulies
442a5c5e36
First proro of trusting, with models and front, but no additional permissions 2022-04-13 12:30:22 +02:00
Nicolas Margulies
7ab0fec3bc
Added trust model 2022-04-13 12:30:22 +02:00
aeltheos
bd4fb23351 Merge branch 'color_survi' into 'main'
switching to survivalist color

See merge request bde/nk20!199
2022-04-12 20:16:55 +02:00
Yoann Beaugnon
ee22e9b3b6 fixing color to follow the proper theme 2022-04-12 18:33:22 +02:00
Yoann Beaugnon
19ae616fb4 switching to survivalist color 2022-04-12 17:40:52 +02:00
Otthorn
b7657ec362 Merge branch 'color_ttlsp' into 'main'
Passage des couleur vers ttlsp

See merge request bde/nk20!197
2022-04-05 15:05:41 +02:00
parpaing
4d03d9460d Passage des couleurs ttlsp 2022-04-05 14:45:41 +02:00
3633f66a87 Merge branch 'beta' into 'main'
Corrections de bugs

See merge request bde/nk20!195
2022-03-09 15:10:37 +01:00
d43fbe7ac6 Merge branch 'harden' into 'beta'
Harden Django project configuration

See merge request bde/nk20!194
2022-03-09 12:30:23 +01:00
Alexandre Iooss
df5f9b5f1e Harden Django project configuration
Set session and CSRF cookies as secure for production.
Set HSTS header to let browser remember HTTPS for 1 year.
2022-03-09 12:12:56 +01:00
4161248bff
Add permissions to view/create/change/delete OAuth2 applications
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2022-03-09 12:06:19 +01:00
58136f3c48
Fix permission checks in the /api/me view
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2022-03-09 11:45:24 +01:00
d9b4e0a9a9
Fix membership tables for clubs without an ending membership date
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2022-02-13 17:53:05 +01:00
8563a8d235
Fix membership tables for clubs without an ending membership date
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2022-02-13 17:51:22 +01:00
5f69232560 Merge branch 'beta' into 'main'
Optional scopes + small bug fix

See merge request bde/nk20!193
2022-02-12 14:37:58 +01:00
d3273e9ee2
Prepare WEI 2022 (because tests are broken)
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2022-02-12 14:24:32 +01:00
4e30f805a7 Merge branch 'optional-scopes' into 'beta'
Implement optional scopes : clients can request scopes, but they are not guaranteed to get them

See merge request bde/nk20!192
2022-02-12 13:57:19 +01:00
546e422e64
Ensure some values exist before updating them
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2022-02-12 13:56:07 +01:00
9048a416df
In the /api/me page, display note, profile and memberships only if we have associated permissions
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-12-23 23:25:18 +01:00
8578bd743c
Add documentation about optional scopes
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-12-23 22:15:06 +01:00
45a10dad00
Refresh token expire between 14 days
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-12-23 22:00:08 +01:00
18a1282773
Implement optional scopes : clients can request scopes, but they are not guaranteed to get them
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-12-23 21:59:37 +01:00
132afc3d15
Fix scope view
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-12-15 18:59:23 +01:00
6bf16a181a
[ansible] Deploy buster-backports repository only on Debian 10
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-12-15 15:59:58 +01:00
e20df82346
Main branch is now called main
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-12-15 15:55:13 +01:00
1eb72044c2 Merge branch 'beta' into 'master'
Changements variés et mineurs

Closes #107 et #91

See merge request bde/nk20!191
2021-12-13 21:16:26 +01:00
f88eae924c
Use local version of Turbolinks instead of using Cloudfare
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-12-13 21:00:34 +01:00
4b6e3ba546
Display club transactions only with note rights, fixes #107
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-12-13 20:01:00 +01:00
bf0fe3479f Merge branch 'lock-club-notes' into 'beta'
Verrouillage de notes

See merge request bde/nk20!190
2021-12-13 18:55:03 +01:00
45ba4f9537
Linting
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-12-13 18:33:18 +01:00
b204805ce2
Add permissions to (un)lock club notes
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-12-13 18:31:36 +01:00
2f28e34cec
Fix permissions to lock our own note
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-12-13 18:27:24 +01:00
9c8ea2cd41
Club notes can now be locked through web interface
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-12-13 17:48:20 +01:00
41289857b2 Merge branch 'tirage-au-sort' into 'beta'
Boutons

See merge request bde/nk20!189
2021-12-13 17:37:13 +01:00
28a8792c9f
[activity] Add space before line breaks in Wiki export of activities
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-12-13 17:30:13 +01:00
58cafad032
Sort buttons by category name instead of id in button list
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-12-13 17:19:10 +01:00
7848cd9cc2
Don't search buttons by prefix
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-12-13 17:18:54 +01:00
d18ccfac23
Sort aliases by normalized name in profile alias view
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-12-13 17:18:54 +01:00
Nicolas Margulies
e479e1e3a4 Added messages for Hide/Show 2021-10-07 23:06:40 +02:00
Nicolas Margulies
82b0c83b1f Added a Hide/Show button for transaction templates, fixes #91 2021-10-07 22:54:01 +02:00
38ca414ef6
Res[pot] can display user information in order to get first/last name in credits
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-10-06 10:44:24 +02:00
fd811053c7
Commit missing migrations
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-10-06 10:41:58 +02:00
9d386d1ecf
Unauthenticated users can't display activity entry view
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-10-06 10:41:42 +02:00
erdnaxe
0bd447b608 Merge branch 'relax_requirements' into 'beta'
Relax requirements and ignore shell.nix

See merge request bde/nk20!187
2021-10-05 15:45:31 +02:00
Alexandre Iooss
3f3c93d928 Ignore shell.nix in Git tree
shell.nix is used in Nix to create a specific shell with custom
packages. The name is standardised and need to be in project folder to
ease development tools integrations.
2021-10-05 15:14:56 +02:00
Alexandre Iooss
340c90f5d3 Relax requirements
Relax requirements to allow the use of newer versions of dependencies
found in NixPkgs and ArchLinux. Do not limit upper version of
django-extensions as it is not mission critical.
2021-10-05 15:10:20 +02:00
ca2b9f061c Merge branch 'beta' into 'master'
Multiples fix, réparation des pots

Closes #75

See merge request bde/nk20!186
2021-10-05 12:02:03 +02:00
a05dfcbf3d
Linting
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-10-05 11:46:24 +02:00
ba3c0fb18d
Fix activity get in invite view
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-10-04 21:53:35 +02:00
ab69963ea1 Merge branch 'cest-lheure-du-pot' into 'beta'
Améliorations Pot

See merge request bde/nk20!184
2021-10-04 18:45:21 +02:00
654c01631a
BDE members can see aliases from other people now
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-10-04 18:29:34 +02:00
d94cc2a7ad
NameNAN
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-10-04 18:26:14 +02:00
69bb38297f
Fix membership dates for new memberships, fix tests
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-10-04 18:15:07 +02:00
9628560d64
Improve entry search with a debouncer
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-10-04 14:39:53 +02:00
df3bb71357
Serve static files with Nginx only in production to make JavaScript development easier
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-10-04 13:58:48 +02:00
2a216fd994
Entries are distinct
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-10-04 13:50:39 +02:00
8dd2619013
Activities are distinct
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-10-04 13:50:21 +02:00
62431a4910
Treasurers can manage activity entries
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-10-04 13:49:16 +02:00
Pierre-antoine Comby
946bc1e497 show that rows are clickable, fix #75 2021-10-01 14:35:29 +02:00
d4896bfd76
Check that club's note is active before creating an activity
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-28 17:03:32 +02:00
23f46cc598
Create transfers when pressing Enter in the amount part
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-28 16:57:23 +02:00
d1a9f21b56 Merge branch 'fix-pretty-money' into 'beta'
Pretty money function is invalid in Javascript: it mays display an additional euro

See merge request bde/nk20!183
2021-09-28 09:36:44 +00:00
d809b2595a
Pretty money function is invalid in Javascript: it mays display an additional euro
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-28 11:20:57 +02:00
97803ac983 Merge branch 'beta' into 'master'
Le [Pot] c'est demain

See merge request bde/nk20!182
2021-09-27 14:52:09 +00:00
b951c4aa05 Merge branch 'fix-pot' into 'beta'
Entrées activités

See merge request bde/nk20!181
2021-09-27 14:37:10 +00:00
69b3d2ac9c
[activity] Fix button shortcut to entries page
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-27 14:51:17 +02:00
f29054558a
Fix note render with formattable aliases
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-27 14:30:47 +02:00
11dd8adbb7 Merge branch 'wei' into 'master'
[WEI] Algo de répartition

Closes #97 et #98

See merge request bde/nk20!180
2021-09-27 12:28:03 +00:00
d437f2bdbd
Linting
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-27 13:59:43 +02:00
ac8453b04c
[WEI] Reset cache after running algorithm
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-27 13:56:10 +02:00
Pierre-antoine Comby
6b4d18f4b3 fix #97 2021-09-26 23:03:25 +02:00
Pierre-antoine Comby
668cfa71a7 fix #98 2021-09-26 23:02:31 +02:00
161db0b00b
[WEI] Fix quotas
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-16 23:48:03 +02:00
8638c16b34
[WEI] New score function that takes in account scores given by other buses
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-16 22:15:45 +02:00
9583cec3ff
[WEI] Fix quotas
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-16 21:10:23 +02:00
1ef25924a0
[WEI] Display status bar with tqdm
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-16 20:46:34 +02:00
e89383e3f4
[WEI] Start repartition by non-male people
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-16 20:06:34 +02:00
79a116d9c6
[WEI] Cache optimization
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-16 20:05:20 +02:00
aa75ce5c7a
[WEI] Don't manage hardcoded people in repartition algorithm
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-16 15:37:18 +02:00
a3a9dfc812
[Treasury] Don't add non-existing transactions to sogé-credits (eg. when membership is free)
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-16 11:00:10 +02:00
76531595ad
80 € for people that opened an account to Société générale and don't go to the WEI
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-16 10:58:23 +02:00
a0b920ac94
Don't check permission to edit credit transaction test while deleting a SogéCredit
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-15 12:40:21 +02:00
ab2e580e68
Update banner text for more precision
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-15 12:14:57 +02:00
0234f19a33
[WEI] Automatically indicate a soge credit if already created
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-14 13:45:01 +02:00
1a4b7c83e8
[WEI] Fix critical security issue
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-13 23:37:27 +02:00
4c17e2a92b
Fix wrong banner message
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-13 23:29:51 +02:00
e68afc7d0a
[WEI] Fix redirect link
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-13 21:06:44 +02:00
c6e3b54f94
Use longtable for better tables for WEI
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-13 20:27:57 +02:00
7e6a14296a Merge branch 'beta' into 'master'
Magnifique UI pour le WEI

See merge request bde/nk20!179
2021-09-13 18:06:03 +00:00
780f78b385 Merge branch 'wei' into 'beta'
[WEI] Belle UI pour attribuer les 1A dans les bus

See merge request bde/nk20!178
2021-09-13 17:50:34 +00:00
4e3c32eb5e
Linting
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-13 19:28:15 +02:00
ef118c2445
[WEI] Avoid errors if the survey is not ended
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-13 19:24:53 +02:00
600ba15faa
[WEI] Display suggested 1A number in a bus in repartition view
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-13 19:04:11 +02:00
944bb127e2
[WEI] New UI is working
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-12 22:29:57 +02:00
f6d042c998
[WEI] Attribute bus to people that paid their registration
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-12 20:10:50 +02:00
bb9a0a2593
[WEI] UI to attribute buses for 1A
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-12 19:49:22 +02:00
61feac13c7
[WEI] Add page that display information about the algorithm result
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-11 19:16:34 +02:00
81e708a7e3
[WEI] Fix registration update
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-11 14:20:38 +02:00
3532846c87
[WEI] Validate WEI memberships of first year members before the repartition algorithm to debit notes
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-10 22:09:47 +02:00
49551e88f8
Fix default promotion year
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-09 19:51:57 +02:00
db936bf75a
Avoid anonymous users to access to the WEI registration form
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-09 17:52:52 +02:00
5828a20383 Merge branch 'beta' into 'master'
Corrections de bugs

See merge request bde/nk20!177
2021-09-09 12:00:01 +00:00
cea3138daf Merge branch 'wei' into 'beta'
Corrections de bugs

See merge request bde/nk20!176
2021-09-09 11:43:34 +00:00
fb98d9cd8b
Fix one more error in alias autocompletion
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-09 10:53:40 +02:00
0dd3da5c01
Linting
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-09 10:45:36 +02:00
af4be98b5b
Fix consumer search with non-regex values (only for consumers, not for all search fields in API)
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-09 10:41:57 +02:00
be6059eba6
[WEI] Fix tests
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-09 10:20:57 +02:00
5793b83de7
[WEI] Fix error when validating sometimes a membership
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-09 09:27:15 +02:00
2c02c747f4
[WEI] Fix errors when a user go to the WEI registration form while it is already registered
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-09 09:23:12 +02:00
a78f3b7caa
[WEI] Fix broken tests
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-09 09:16:08 +02:00
1ee40cb94e
Fix chemistry department (warning: this may break the choices from members of the department)
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-09 09:10:05 +02:00
bd035744a4
Don't create WEI registrations for unvalidated users
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-09 08:56:21 +02:00
7edd622755
BDE members can now use their note balance for personal transactions
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-08 18:35:36 +02:00
8fd5b6ee01
Fix safe summary for old passwords hashes from NK15 in Django Admin
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-08 17:07:07 +02:00
03411ac9bd
Don't check permissions in a script
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-08 16:59:44 +02:00
d965732b65
Support multiple addresses for IP-based connection (useful when using IPv4/IPv6 and for ENS -> Crans transition)
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-08 14:52:39 +02:00
048266ed61
[WEI] Fix unvalidated registrations table
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-07 22:09:00 +02:00
b27341009e
[WEI] Update validation buttons for 1A
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-07 15:11:15 +02:00
da1e15c5e6
Update Sogé credit amount when a transaction is added if the credit was already validated
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-07 13:04:09 +02:00
4b03a78ad6
Fix password change form from unauthenticated users
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-07 12:57:03 +02:00
fb6e3c3de0
If connected and if we have the right, directly redirect to the validation page when registering someone
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-07 10:56:50 +02:00
391f3bde8f
Fix permission to see note balance when we can't see profile detail (e.g. for note account)
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-06 11:56:56 +02:00
ad04e45992
PC Kfet can create and update Sogé credits (but not see them)
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-06 11:43:39 +02:00
4e1ba1447a
Add option to add a posteriori a Sogé credit
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-06 00:47:11 +02:00
b646f549d6
When creating a Sogé credit, serch existing recent memberships and register them
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-05 21:24:16 +02:00
ba9ef0371a
[WEI] Run algorithm only on valid surveys
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-05 20:36:17 +02:00
881cd88f48
[WEI] Fix permission check for information json
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-05 20:10:21 +02:00
b4ed354b73 Merge branch 'wei' into 'master'
Amélirations questionnaire WEI

See merge request bde/nk20!175
2021-09-05 17:32:57 +00:00
e5051ab018
Linting
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-05 19:32:34 +02:00
bb69627ac5
Remove debug code
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-05 18:57:07 +02:00
ffaa020310
Fix WEI registration in dev mode
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-05 18:52:57 +02:00
6d2b7054e2
[WEI] Optimizations in survey load
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-05 18:49:34 +02:00
d888d5863a
[WEI] For each bus, choose a random word which score is higher than the mid score
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-05 18:39:03 +02:00
dbc7b3444b
[WEI] Add script to import bus scores
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-05 18:23:55 +02:00
f25eb1d2c5
[WEI] Fix some issues
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-05 17:30:59 +02:00
a2a749e1ca
[WEI] Fix permission check to register new accounts to users
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-05 17:15:19 +02:00
5bf6a5501d
[WEI] Fix test for 1A registration
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-04 13:03:38 +02:00
9523b5f05f
[WEI] Choose one word per bus in the survey
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-04 12:37:29 +02:00
5eb3ffca66 Merge branch 'beta' into 'master'
OAuth2, tests WEI

See merge request bde/nk20!174
2021-09-02 20:49:58 +00:00
9930c48253 Merge branch 'oauth2' into 'beta'
Implement OAuth2 scopes based on permissions

See merge request bde/nk20!170
2021-09-02 19:18:43 +00:00
d902e63a0c
Allow search aliases per exact name
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-02 20:59:46 +02:00
48b0bade51
Indicate what scopes are used
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-02 20:59:46 +02:00
f75dbc4525
OAuth2 implementation documentation
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-02 20:59:45 +02:00
fbf64db16e
Simple test to check permissions with the new OAuth2 implementation
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-02 20:59:45 +02:00
a3fd8ba063
Bad paste in comment
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-02 20:59:45 +02:00
9b26207515
Rework templates for OAuth2
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-02 20:59:43 +02:00
7ea36a5415
[oauth2] Add view to generate authorization link per application with given scopes
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-02 20:59:33 +02:00
898f6d52bf
Better templates for OAuth2 authentication
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-02 20:59:20 +02:00
8be16e7b58
Permissions support fully OAuth2 scopes
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-02 20:58:05 +02:00
ea092803d7
Check permissions per request instead of per user
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-02 20:58:05 +02:00
5e9f36ef1a
Store current request rather than user/session/ip
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-02 20:58:04 +02:00
b4d87bc6b5
Fix import
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-02 20:58:04 +02:00
dd639d829e
Implement OAuth2 scopes based on permissions
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-02 20:58:04 +02:00
7b809ff3a6 Merge branch 'wei' into 'beta'
[WEI] Correction de l'algorithme et tests unitaires

See merge request bde/nk20!173
2021-09-02 18:53:21 +00:00
d36edfc063
Linting
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-02 13:44:18 +02:00
cf87da096f
No more offer 80 € to new members since there is a WEI
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-02 13:39:17 +02:00
e452b7acbf
[WEI] Allow a tolerance of 25 %
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-02 09:53:27 +02:00
74ab4df9fe
[WEI] Extreme test with full buses and quality constraints
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-02 01:36:37 +02:00
451851c955
[WEI] Add a small test for the WEI algorithm with a few people
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-09-01 22:53:28 +02:00
789ca149af Merge branch 'beta' into 'master'
WEI, diverses améliorations

See merge request bde/nk20!172
2021-08-29 13:22:04 +00:00
7d3f1930b8 Merge branch 'wei' into 'beta'
Améliorations WEI

See merge request bde/nk20!171
2021-08-29 13:03:02 +00:00
e8f4ca1e09
Fix note account
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-08-29 14:40:55 +02:00
733f145be3
BDE members can now use they note even if they are not in the Kfet club
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-08-29 14:39:36 +02:00
48c37353ea
[WEI] Fix pipeline before the good unit tests for WEI algorithm
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-08-29 14:38:11 +02:00
8056dc096d
[WEI] Old members can create WEI registrations to renew their membership easily
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-08-29 14:33:17 +02:00
6d5b69cd26
Fix verification of parent club membership
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-08-29 14:17:09 +02:00
a7bdffd71a
[WEI] Change color of validation button of WEI registrations
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-08-29 14:10:52 +02:00
0887e4bbde
[WEI] Fix some tests, without considering WEI algorithm
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-08-27 13:15:28 +02:00
199f4ca1f2
[WEI] First implementation of algorithm
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-08-27 10:44:38 +02:00
802a6c68cb
[WEI] Update survey words
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-08-26 00:11:24 +02:00
41a0b3a1c1
[WEI] Request bus size
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-08-25 23:26:57 +02:00
aa35724be2
Better display for WEI member list
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-08-23 19:00:26 +02:00
9086d33158
[WEI] Caution check is not required to validate registrations
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-08-23 18:51:34 +02:00
43d214b982
[WEI] Store seed in WEI Survey to add determinism in RNG
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-08-02 19:30:36 +02:00
b93e4a8d11
Current WEI year is 2021
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-08-02 19:22:07 +02:00
b9a9704061
[WEI] Prepare WEI 2021
No need to save WEI 2020 data because there weren't any WEI 2020

Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-08-02 18:22:19 +02:00
fee52f326a
[WEI] Add dry mode in WEI algorithm command, output generated data
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-08-02 18:21:06 +02:00
317966d5c1 Merge branch 'l_eveil_du_nanax' into 'beta'
More linting

See merge request bde/nk20!163
2021-06-14 20:25:40 +00:00
9f0a22d3d1
There is not always an error
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-06-14 22:15:35 +02:00
a5ecdd100c Merge branch '2021' into 'beta'
Update copyright for 2021

See merge request bde/nk20!169
2021-06-14 20:04:15 +00:00
f60691846b
Don't block valid payments
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-06-14 21:54:32 +02:00
d5ecb72a71
Update copyright for 2021
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-06-14 21:45:56 +02:00
8cf9dfb9b9
Reduce complexity of the validation of a user, add verbosity in comments
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-06-14 21:43:04 +02:00
c3ab61bd04
Factorize detection of uncomplete payment forms
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-06-14 21:39:29 +02:00
0b4b6dcb3e Merge branch 'fix-mail-source' into 'beta'
Never use default constants. webmaster@localhost is never allowed to send emails.

See merge request bde/nk20!168
2021-06-14 19:25:26 +00:00
0d5f6c0332 Merge branch 'fix-amounts' into 'beta'
Round amounts to the nearest integer rather than take the floor

See merge request bde/nk20!167
2021-06-14 19:24:26 +00:00
7b28938cde
Never use default constants. webmaster@localhost is never allowed to send emails.
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-06-07 23:49:46 +02:00
35ffb36fbd
Round amounts to the nearest integer rather than take the floor
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-06-07 23:47:07 +02:00
ourspalois
08ba0b263a Merge branch 'beta' into 'master'
changement couleur final (j'espère)

See merge request bde/nk20!166
2021-05-22 14:09:51 +00:00
ourspalois
c4c4e9594f couleur 4.0 2021-05-22 12:34:31 +02:00
ourspalois
4166823d55 couleurs 3.0 2021-05-22 12:29:02 +02:00
ourspalois
dc0f3dbcef Changement de couleur 3.0 2021-05-22 12:19:29 +02:00
ourspalois
4583958f50 Merge branch 'beta' into 'master'
Changement de couleurs

See merge request bde/nk20!165
2021-05-22 09:56:55 +00:00
ourspalois
b3abe9ab18 Changement de couleur 2.0 2021-05-22 11:53:13 +02:00
ourspalois
27f23b48b6 Merge branch 'coulour_vieux' into 'beta'
Bonjour c le changement de couleur

See merge request bde/nk20!164
2021-05-22 09:41:54 +00:00
ourspalois
67e170d4a6 Bonjour c le changement de couleur 2021-05-22 11:30:11 +02:00
Alexandre Iooss
8f895dc4d7
note: use native selector rather than Query 2021-05-12 18:03:44 +02:00
Alexandre Iooss
1187577728
Do not lint scripts submodule 2021-05-12 17:33:11 +02:00
Alexandre Iooss
8a58af3b31
Reformat apps/registration/tokens.py 2021-05-05 19:48:19 +02:00
Alexandre Iooss
0c23625147
Remove newline in imports 2021-05-05 19:47:16 +02:00
Alexandre Iooss
21219b9c62
Rename join_BDE and join_Kfet to lowercase 2021-05-05 19:46:53 +02:00
Alexandre Iooss
5ab8beecef
Use _ prefix for ignored loop variable 2021-05-05 19:14:59 +02:00
Alexandre Iooss
1ca5133026
BaseException.message is removed in Python 3 2021-05-05 19:12:23 +02:00
Alexandre Iooss
93bc6bb245
Do not call setattr with a constant attribute value 2021-05-05 19:12:03 +02:00
Alexandre Iooss
952c4383e7
Add flake8-bugbear and lint all apps 2021-05-05 19:09:33 +02:00
15dd2b8f0c
PC Kfet can update profile section while renewing memberships
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-04-29 13:11:00 +02:00
c540b6334c
Fix minimum amount for the send_mail_to_negative_balances script
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-04-27 09:52:34 +02:00
ourspalois
bab394908d Merge branch 'beta' into 'master'
Bugs mineurs, documentation

See merge request bde/nk20!162
2021-04-23 19:32:54 +00:00
0b93968b9e Merge branch 'docs' into 'beta'
More and more documentation

See merge request bde/nk20!156
2021-04-23 19:10:56 +00:00
97375ef6c0
Copy production database to development website
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-04-23 21:07:57 +02:00
36cfcd533f
Documentation on scripts
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-04-23 21:07:56 +02:00
21dbc53615
Spam negative users if they have less than 0 euro
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-04-23 21:07:53 +02:00
e6f10ebdac
Install production server
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-04-23 21:07:28 +02:00
47968844ce
Install local development server
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-04-23 21:07:28 +02:00
a435460e29
Personal interface
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-04-23 21:07:28 +02:00
b7c4360108
How to NK20
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-04-23 21:07:28 +02:00
Otthorn
8d8c417c50
Fix old markdown remaining in docs 2021-04-23 21:07:28 +02:00
2b189af25b
[FAQ] How to create a button?
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-04-23 21:07:28 +02:00
5a07c8a94f
More FAQ
Signed-off-by: ynerant <ynerant@crans.org>
2021-04-23 21:07:27 +02:00
6cc1857eb6
Add FAQ page
Signed-off-by: ynerant <ynerant@crans.org>
2021-04-23 21:07:27 +02:00
601534d610
Add link to the documentatioh on the README
Signed-off-by: ynerant <ynerant@crans.org>
2021-04-23 21:07:27 +02:00
c271593839
Fix markdown typos
Signed-off-by: ynerant <ynerant@crans.org>
2021-04-23 21:07:27 +02:00
f351794aa0
Documentation on documentation
Signed-off-by: ynerant <ynerant@crans.org>
2021-04-23 21:07:27 +02:00
2793fee58c Merge branch 'less-verbosity' into 'beta'
Reduce verbosity of cron scripts

See merge request bde/nk20!161
2021-04-14 13:57:55 +00:00
7a715df121
Treasurers asked to add the list of old members in the monthly reports
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-04-14 15:48:15 +02:00
9308878054
Adapt verbosity of some scripts
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-04-14 15:18:49 +02:00
b5ccf5b800
Run cron without verbosity
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-04-14 15:05:13 +02:00
5e63254439 Merge branch 'fix-reports' into 'beta'
Update last report date only in non-debug mode

See merge request bde/nk20!160
2021-04-14 13:03:04 +00:00
da96506218
Update last report date only in non-debug mode
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-04-14 14:51:24 +02:00
b4714b896a Merge branch 'fix-reports' into 'beta'
Daily reports were broken

See merge request bde/nk20!159
2021-04-08 17:45:17 +00:00
cdb2647a4d
Fix note list when daily reports are sent
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-04-08 17:33:51 +02:00
cc12e3ec63
Send cron mails to the BDE
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-04-08 17:31:01 +02:00
be168c5ada
Decimal value is serialized as a str value
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-03-21 10:59:58 +01:00
b46ae6f856
[treasury] Product quantities are finally decimal fields 2021-03-21 10:41:15 +01:00
ec0bcbf015
PC Kfet can see all users
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-03-21 10:28:50 +01:00
81303b8ef8 Merge branch 'floating-quantities' into 'beta'
[invoices] Product quantities can be floating

See merge request bde/nk20!158
2021-03-13 12:13:50 +00:00
910b98fefc
Invoices are in french
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-03-13 13:04:00 +01:00
5a7a219ba8
[invoices] Quantities can be non-integers
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-03-13 12:35:28 +01:00
116451603c Merge branch 'cas' into 'beta'
CAS + OAuth2

See merge request bde/nk20!155
2021-03-09 16:39:03 +00:00
b2437ef9b5
Remove additional blank lines
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-03-09 17:18:43 +01:00
d8c9618772
OAuth2 documentation
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-03-09 17:04:16 +01:00
c825dee95a
CAS documentation
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-03-09 15:42:36 +01:00
73d27e820b
Provide also note information (with balance and picture)
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-03-09 12:55:19 +01:00
40e1b42078
Fix API path
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-03-09 12:54:57 +01:00
72806f0ace
Add profile and membership information to OAuth views
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-03-09 10:57:35 +01:00
b244e01231
Add simple view to give OAuth information
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-03-09 10:41:43 +01:00
76d1784aea
Add OAuth2 authentication for Django Rest Framework
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-03-09 09:44:25 +01:00
56c5fa4057
We don't need a session to have permissions
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-03-09 09:41:27 +01:00
b5ef937a03
Environment file path is absolute
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-03-09 09:39:57 +01:00
e95a8b6e18
Add normalized name to services
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-03-03 18:42:51 +01:00
635adf1360
Use cas server to use authentication in other services
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-03-03 18:13:33 +01:00
d5a9bf175f
Add script to force delete a user, in case of duplicates
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-02-22 11:54:23 +01:00
b597a6ac5b
Fix soge credit deletion when the account is not validated yet
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-02-21 23:05:27 +01:00
Rida LALI
a704b92c3d
Prez BDE : ajout transaction random + see all buttons 2021-02-20 15:12:08 +01:00
53090b1a21 Fix JS texts
Signed-off-by: ynerant <ynerant@crans.org>
2021-02-14 11:52:37 +01:00
c49af0b83a Merge branch 'beta' into 'master'
Fix memberships

See merge request bde/nk20!147
2021-02-11 20:49:30 +00:00
5a05997d9d
Fix date comparison when checking a membership from the parent club
Signed-off-by: ynerant <ynerant@crans.org>
2021-02-11 21:38:44 +01:00
Yohann D'ANELLO
c109cd3ddd
Source is not destination
Signed-off-by: Yohann D'ANELLO <yohann.danello@gmail.com>
2021-01-19 15:17:03 +01:00
Yohann D'ANELLO
84304971d7
Add sample translation file for english
Signed-off-by: Yohann D'ANELLO <yohann.danello@gmail.com>
2021-01-19 14:29:12 +01:00
b8b781f9a2 Merge branch 'beta' into 'master'
Beta

Closes #84 et #83

See merge request bde/nk20!146
2021-01-19 12:40:24 +01:00
002128eed2 Merge branch 'fix-js-strings' into 'master'
Fix js strings

Closes #85, #84 et #83

See merge request bde/nk20!144
2021-01-19 12:24:13 +01:00
8d71783c42 Merge branch 'docs' into 'beta'
Docs

See merge request bde/nk20!145
2021-01-19 12:01:45 +01:00
Yohann D'ANELLO
a6f23df7d5
Load the good translation file, fixes #85
Signed-off-by: Yohann D'ANELLO <yohann.danello@gmail.com>
2021-01-19 11:58:19 +01:00
Yohann D'ANELLO
d9c97628e2
Add Clacks Overhead header on each response. Closes #84
Signed-off-by: Yohann D'ANELLO <yohann.danello@gmail.com>
2020-12-31 15:40:18 +01:00
Yohann D'ANELLO
893534955d
Use the Debian mirror of Crans
Signed-off-by: Yohann D'ANELLO <yohann.danello@gmail.com>
2020-12-30 00:04:08 +01:00
Yohann D'ANELLO
dfbf9972c2
By default, automatically change directory to /var/www/note_kfet and source the Python virtual environment in the .bashrc file
Signed-off-by: Yohann D'ANELLO <yohann.danello@gmail.com>
2020-12-29 23:27:51 +01:00
Yohann D'ANELLO
b5f3b3ffc1
Use Nginx certbot challenge
Signed-off-by: Yohann D'ANELLO <yohann.danello@gmail.com>
2020-12-29 21:44:28 +01:00
Yohann D'ANELLO
3aad4e7398
Agree Let's Encrypt ToS
Signed-off-by: Yohann D'ANELLO <yohann.danello@gmail.com>
2020-12-29 21:41:29 +01:00
Yohann D'ANELLO
b4a1b513cc
Good bye bde3-virt, welcome bde-note-dev!
Signed-off-by: Yohann D'ANELLO <yohann.danello@gmail.com>
2020-12-29 20:05:15 +01:00
c0c64f225c Merge branch 'ansible-fix' into 'beta'
Ansible fix

See merge request bde/nk20!139
2020-12-29 20:01:43 +01:00
Yohann D'ANELLO
9d8f47115c
ConsumerViewSet is a bit tricky
Signed-off-by: Yohann D'ANELLO <yohann.danello@gmail.com>
2020-12-23 21:50:48 +01:00
Yohann D'ANELLO
f4156f1b94
Update API links, more detail on filtering
Signed-off-by: Yohann D'ANELLO <yohann.danello@gmail.com>
2020-12-23 21:42:58 +01:00
Yohann D'ANELLO
e60994e065
API Documentation
Signed-off-by: Yohann D'ANELLO <yohann.danello@gmail.com>
2020-12-23 21:06:30 +01:00
Yohann D'ANELLO
801f711994
Merge branch 'beta' into docs 2020-12-23 20:19:40 +01:00
Yohann D'ANELLO
e4568b410f
How to authenticate on the API?
Signed-off-by: Yohann D'ANELLO <yohann.danello@gmail.com>
2020-12-23 19:19:46 +01:00
c8f7986d5a Merge branch 'api' into 'beta'
API Filters

See merge request bde/nk20!143
2020-12-23 19:02:59 +01:00
Yohann D'ANELLO
d3a9c442a5
Test the note kfet with Debian Bullseye, Python 3.9 and Django 2.2
Signed-off-by: Yohann D'ANELLO <yohann.danello@gmail.com>
2020-12-23 18:48:09 +01:00
Yohann D'ANELLO
016ab5a9c9
Remove dead code, don't try to cover unnecessary things
Signed-off-by: Yohann D'ANELLO <yohann.danello@gmail.com>
2020-12-23 18:45:05 +01:00
Yohann D'ANELLO
7866ab7ec0
Ordering filters are now properly tested
Signed-off-by: Yohann D'ANELLO <yohann.danello@gmail.com>
2020-12-23 18:25:54 +01:00
Yohann D'ANELLO
f570ff3cd5
Check that permissions are working when accessing to API pages
Signed-off-by: Yohann D'ANELLO <yohann.danello@gmail.com>
2020-12-23 18:21:59 +01:00
Yohann D'ANELLO
6b2638c271
Documentation is located on /doc
Signed-off-by: Yohann D'ANELLO <yohann.danello@gmail.com>
2020-12-23 15:20:30 +01:00
Yohann D'ANELLO
5cb4183e9f
Use python Warnings instead of printing messages during tests
Signed-off-by: Yohann D'ANELLO <yohann.danello@gmail.com>
2020-12-23 15:11:33 +01:00
Yohann D'ANELLO
3a20555663
Unit tests for API pages, closes #83
Signed-off-by: Yohann D'ANELLO <yohann.danello@gmail.com>
2020-12-23 14:54:21 +01:00
Yohann D'ANELLO
95be0042e9
Fix transaction API page 2020-12-22 13:28:43 +01:00
Yohann D'ANELLO
48880e7fd3
More API filters for the wei app 2020-12-22 13:11:01 +01:00
Yohann D'ANELLO
e0030771e4
More API filters for the treasury app 2020-12-22 12:53:35 +01:00
Yohann D'ANELLO
d47799e6ee
More API filters for the permission app 2020-12-22 12:42:54 +01:00
Yohann D'ANELLO
eae091625a
More API filters for the note app 2020-12-22 12:37:21 +01:00
Yohann D'ANELLO
aceb77ffb9
More API filters for the activity app 2020-12-22 03:18:43 +01:00
Yohann D'ANELLO
338c94ed05
More API filters for the member app 2020-12-22 02:58:12 +01:00
Yohann D'ANELLO
290848f904 Non-member people can update their profile everytime 2020-12-02 14:58:14 +01:00
Yohann D'ANELLO
72dca54bbf Wrong path for artifact uploading 2020-11-26 03:13:57 +01:00
Yohann D'ANELLO
117d9da3ba Gitlab compiles the documentation 2020-11-26 02:55:36 +01:00
Yohann D'ANELLO
37efebe85b Ansible builds and deploys the documentation 2020-11-26 02:49:57 +01:00
Yohann D'ANELLO
3af2ec71b6 Extract documentation from the Gitlab wiki and convert it to reStructuredText 2020-11-26 02:29:51 +01:00
Yohann D'ANELLO
0b4a95525b Use RTD theme 2020-11-26 01:07:18 +01:00
Yohann D'ANELLO
af664e481f Initial setup for Sphinx 2020-11-26 01:01:08 +01:00
ynerant
0171f16311 Merge branch 'beta' into 'master'
Translate some words

See merge request bde/nk20!142
2020-11-21 13:55:26 +01:00
ynerant
296b94d237 Merge branch 'master' into 'beta'
Translations

See merge request bde/nk20!141
2020-11-21 13:37:24 +01:00
ynerant
4942553335 Merge branch 'JS_translations' into 'master'
Js translations

Closes #73

See merge request bde/nk20!140
2020-11-21 13:18:32 +01:00
elkmaennchen
c1efb87180 Fix spanish translations 2020-11-21 12:37:31 +01:00
elkmaennchen
72eead8595 Add spanish javascript translation 2020-11-21 12:26:31 +01:00
elkmaennchen
ade7e583e5 Complete spanish translation to 98% 2020-11-21 12:24:55 +01:00
4a8a101822 Translated using Weblate (German)
Currently translated at 100.0% (19 of 19 strings)

Translation: Note Kfet 2020/NK20 - JS
Translate-URL: http://translate.ynerant.fr/projects/nk20/nk20-js/de/
2020-11-16 20:21:46 +00:00
dd2cfa6327 Translated using Weblate (French)
Currently translated at 100.0% (668 of 668 strings)

Translation: Note Kfet 2020/NK20
Translate-URL: http://translate.ynerant.fr/projects/nk20/nk20/fr/
2020-11-16 20:02:32 +00:00
2adf84b7fc Translated using Weblate (German)
Currently translated at 98.0% (655 of 668 strings)

Translation: Note Kfet 2020/NK20
Translate-URL: http://translate.ynerant.fr/projects/nk20/nk20/de/
2020-11-16 20:02:31 +00:00
ynerant
2f54e64ea2 Merge branch 'JS_translations' into 'beta'
Js translations

See merge request bde/nk20!125
2020-11-16 20:49:46 +01:00
Yohann D'ANELLO
8434c0062c Merge branch 'beta' into JS_translations
# Conflicts:
#	apps/note/static/note/js/consos.js
#	locale/de/LC_MESSAGES/django.po
#	locale/es/LC_MESSAGES/django.po
#	locale/fr/LC_MESSAGES/django.po
2020-11-16 00:59:26 +01:00
Yohann D'ANELLO
6d976f32bf Update django oauth toolkit, fix #73 2020-11-16 00:49:53 +01:00
Yohann D'ANELLO
b9d49d53f2 Export JS translation files as static files 2020-11-16 00:29:27 +01:00
Yohann D'ANELLO
23243e09bb Fix some errors on JS string interpolation 2020-11-15 23:37:36 +01:00
Yohann D'ANELLO
2682e9a610 Add line in README on how to extract localized string in JS files 2020-11-15 23:31:10 +01:00
Yohann D'ANELLO
5635598bbc Extract strings from javascript files and translate them in french 2020-11-15 23:28:41 +01:00
Yohann D'ANELLO
b58a0c43cd Include auto-generated javascript translation file 2020-11-15 22:53:00 +01:00
Pierre-antoine Comby
e1f647bd02 lesser hardcoded 2020-10-30 21:28:25 +01:00
Pierre-antoine Comby
39fd3a2471 set DB_PASSWORD in env file 2020-10-30 20:54:41 +01:00
Pierre-antoine Comby
1072e227b8 don't copy personal config on prod 2020-10-30 17:07:03 +01:00
Pierre-antoine Comby
cbf7e6fe6c run certbot if necessary 2020-10-30 17:01:47 +01:00
Pierre-antoine Comby
950922d041 do not hardcode mail 2020-10-30 17:01:26 +01:00
Pierre-antoine Comby
78fe070cd3 use debian backport only with debian 2020-10-30 16:59:44 +01:00
Pierre-antoine Comby
51d5733578 less hardcoded ansible config 2020-10-30 16:58:49 +01:00
Yohann D'ANELLO
7bd895c1df Grant treasurers to update a note picture 2020-10-26 17:58:30 +01:00
ynerant
e5e94c52f2 Merge branch 'beta' into 'master'
Permissions PC Kfet

See merge request bde/nk20!138
2020-10-25 22:08:00 +01:00
Yohann D'ANELLO
051591cb7a Don't see user detail in update form 2020-10-25 21:49:16 +01:00
Yohann D'ANELLO
0e7390b669 PC Kfet can see limited user information and clubs. It can create memberships but not see them 2020-10-25 21:38:04 +01:00
Yohann D'ANELLO
fe4363b83d Don't display too much detail when a user has no right to see a profile 2020-10-25 21:29:44 +01:00
Yohann D'ANELLO
6e80016b38 Don't delete object when checking an add permission: this is useless since we rollback to the initial DB state 2020-10-25 21:08:36 +01:00
Yohann D'ANELLO
08e50ffc22 Credit form didn't raise an error when the data didn't validate 2020-10-23 18:19:21 +02:00
ynerant
9cb65277f3 Merge branch 'beta' into 'master'
Ajustement de permissions

See merge request bde/nk20!137
2020-10-23 17:10:15 +02:00
Yohann D'ANELLO
224a0fdd8c SpecialTransactionProxy are force-saved 2020-10-23 16:55:33 +02:00
Yohann D'ANELLO
6dc7604e90 Alias were duplicated in profile alias list view 2020-10-23 16:48:33 +02:00
Yohann D'ANELLO
cb7f3c9f18 Note account can manage BDE memberships 2020-10-23 16:42:06 +02:00
Yohann D'ANELLO
f910feca9e PC Kfet can create and renew memberships 2020-10-23 13:17:07 +02:00
Yohann D'ANELLO
91f784872c Treasurers can update any roles, not only the BDE-related 2020-10-23 09:50:18 +02:00
ynerant
b655135a42 Merge branch 'beta' into 'master'
PC Kfet

See merge request bde/nk20!136
2020-10-20 10:43:01 +02:00
Yohann D'ANELLO
58aa4983e3 The note account must be active in order to have access to the Rest Framework API 2020-10-20 10:30:41 +02:00
Yohann D'ANELLO
6cc3cf4174 A migration put the right role in the note account's memberships 2020-10-20 00:28:49 +02:00
Yohann D'ANELLO
2097e67321 Add permissions to PC Kfet 2020-10-20 00:19:49 +02:00
Yohann D'ANELLO
d773303d18 Add possibility to authenticate an account with its IP address 2020-10-19 23:44:56 +02:00
ynerant
3cabcf40e7 Merge branch 'beta' into 'master'
Display real user name in the Soge credits list/detail

See merge request bde/nk20!135
2020-10-08 10:48:36 +02:00
Yohann D'ANELLO
bf29efda0a Display real user name in the Soge credits list/detail 2020-10-08 10:36:30 +02:00
ynerant
ceccba0d71 Merge branch 'beta' into 'master'
Highlight users that created a bank account

See merge request bde/nk20!134
2020-10-07 17:55:40 +02:00
Yohann D'ANELLO
3eced33082 Well, everyone doesn't want a secondary bank account 2020-10-07 17:43:28 +02:00
Yohann D'ANELLO
acb3fb4a91 Highlight future users that declared that they opened a bank account 2020-10-07 17:42:46 +02:00
ynerant
1c5e951c2f Merge branch 'beta' into 'master'
Various fixes

See merge request bde/nk20!133
2020-10-07 12:06:48 +02:00
Yohann D'ANELLO
beb1853aef Forgot to create the aliases for BDE and Kfet in the migration that create the clubs 2020-10-07 11:54:04 +02:00
Yohann D'ANELLO
0078eb8f90 Index page is a redirection 2020-10-07 11:53:42 +02:00
Yohann D'ANELLO
e5e758f9d9 Display banners when a user is no more a BDE or Kfet member 2020-10-07 11:46:43 +02:00
Yohann D'ANELLO
4a78328717 The checkbox to tell that a Sogé account got opened is not mandatory 2020-10-07 11:31:20 +02:00
Yohann D'ANELLO
65a2e8c08c Better index page: non-Kfet members will be redirected to their profile page, the account note (when it will be managed) will see the consumption page 2020-10-07 11:29:52 +02:00
Yohann D'ANELLO
b5fa428bad Non-Kfet members can see their old aliases only, but no one else 2020-10-07 11:22:02 +02:00
Yohann D'ANELLO
fb72385773 Warn users that they have to open they Sogé account 2020-10-07 10:59:37 +02:00
Yohann D'ANELLO
2f68601e8b Delete the soge credit if the user declares that one was opened but in the validation form the checkbox was unchecked 2020-10-07 10:46:33 +02:00
Yohann D'ANELLO
0b1bed8048 Temporary give the right to treasurers to manage membership roles, but need to find a proper solution 2020-10-07 10:43:58 +02:00
Yohann D'ANELLO
8ada0e51f2 The validation filter of the soge credit list was buggy 2020-10-07 10:42:52 +02:00
Yohann D'ANELLO
c3d613947f Pre-registered users can declare that they opened a bank account in the signup form 2020-10-07 10:33:57 +02:00
Yohann D'ANELLO
36b8157372 Fix membership table order 2020-10-07 10:03:43 +02:00
Yohann D'ANELLO
992cfe8e23 Can set a parent club to None 2020-10-07 09:48:21 +02:00
Yohann D'ANELLO
18a8ff1b8a Set credit/debit reason non mandatory 2020-10-07 09:45:09 +02:00
Yohann D'ANELLO
c61bb2e90d When we credit the note of a club directly, fill the last name and the first name information with the club name instead of empty 2020-10-07 09:39:40 +02:00
Yohann D'ANELLO
4b12e3ed08 Display only the most recent membership 2020-10-07 09:29:41 +02:00
erdnaxe
af07ed9807 Merge branch 'erdnaxe-master-patch-99095' into 'beta'
Erdnaxe master patch 99095

See merge request bde/nk20!132
2020-10-05 16:37:14 +02:00
erdnaxe
bbe53b3b63 Update README.md 2020-10-05 16:25:06 +02:00
ynerant
536f0ec226 Merge branch 'beta' into 'master'
Ensure the integrity of all note balances in multiple transactions

See merge request bde/nk20!131
2020-10-04 22:12:12 +02:00
Yohann D'ANELLO
541ed59f40 When a membership is created, redirect to the user profile page rather than club detail 2020-10-04 21:08:35 +02:00
Yohann D'ANELLO
e172b4f4bb When a membership is renewed, set the same roles as the previous membership 2020-10-04 20:54:03 +02:00
Yohann D'ANELLO
d666179037 Display Renew membership button 15 days more 2020-10-04 20:50:10 +02:00
Yohann D'ANELLO
f22e92132c Select for update transaction notes, and not only the transaction 2020-10-04 20:47:15 +02:00
Yohann D'ANELLO
ca7ad05746 Use a signal to prevent a user that the note balance is negative 2020-10-04 20:26:43 +02:00
Pierre-antoine Comby
f55ca2f725 Merge branch 'beta' into 'master'
remove the display limit for pre-registred users.

See merge request bde/nk20!130
2020-10-03 18:07:39 +02:00
Pierre-antoine Comby
d4e4ed580f remove the display limit for pre-registred users. 2020-10-03 17:53:38 +02:00
ynerant
8756751344 Merge branch 'beta' into 'master'
Fix some membership issues

See merge request bde/nk20!129
2020-10-01 15:00:36 +02:00
Yohann D'ANELLO
fd83fe19bf Fix some membership date control 2020-10-01 09:17:02 +02:00
Yohann D'ANELLO
a00d95608b Add permission to treasurers to create a club, fix the permission check to renew a membership 2020-09-23 21:36:04 +02:00
ynerant
3303edd01f Merge branch 'beta' into 'master'
OAuth2, no side effect permissions

See merge request bde/nk20!128
2020-09-23 18:44:40 +02:00
Yohann D'ANELLO
e48ef92137 Revert commit that broke beta branch 2020-09-23 18:32:09 +02:00
erdnaxe
919d0b7e85 Merge branch 'ics_cache' into 'beta'
Ics cache

See merge request bde/nk20!127
2020-09-21 15:46:34 +02:00
Alexandre Iooss
439bf35b62
APT python memcache is PyPi memcached 2020-09-21 15:25:07 +02:00
Alexandre Iooss
74b26335d1
Cache ICS calendar 2020-09-21 15:13:59 +02:00
Alexandre Iooss
3d733ed6af
Use memcached cache 2020-09-21 15:13:43 +02:00
erdnaxe
d54ab94ceb Merge branch 'oauth' into 'beta'
Oauth

See merge request bde/nk20!126
2020-09-21 12:53:03 +02:00
Alexandre Iooss
4f188ca3e5 Admin is autodiscovering partially 2020-09-21 12:34:34 +02:00
Alexandre Iooss
72bac75fbd Add Django OAuth toolkit admin 2020-09-21 12:15:40 +02:00
Alexandre Iooss
6d54aae614
Fix django-oauth-toolkit version 2020-09-21 11:15:00 +02:00
Alexandre Iooss
8052152ea5
Add OAuth2 endpoints 2020-09-21 11:03:07 +02:00
Alexandre Iooss
70448db8e5
Remove Django CAS server and add oauth toolkit 2020-09-21 10:31:42 +02:00
ynerant
ac2d1e8111 Merge branch 'no_side_effect_permission_check' into 'beta'
No side effect permission check

See merge request bde/nk20!124
2020-09-20 11:24:46 +02:00
Yohann D'ANELLO
3ba61385a3 Debit is not credit 2020-09-20 11:12:44 +02:00
Yohann D'ANELLO
7353348d7a Rollback transaction when checking an add permission (experimental) 2020-09-20 09:07:51 +02:00
Yohann D'ANELLO
f63e2e088e Don't log when the permission to lock a note is checked 2020-09-20 08:56:42 +02:00
elkmaennchen
420a24ebac enable JavaScriptCatalog view 2020-09-19 22:42:35 +02:00
elkmaennchen
d566def706 Try to translate js, not working... 2020-09-19 22:03:45 +02:00
Yohann D'ANELLO
eaf6769e8b Treasurers can make transactions with people that are no longer a member 2020-09-19 16:33:52 +02:00
Yohann D'ANELLO
a61ec81cff note.crans.org is the default domain 2020-09-19 16:03:32 +02:00
Yohann D'ANELLO
60f2a73cc5 Don't check if the user is a member of the parent club if there is no parent club 2020-09-18 13:35:55 +02:00
Yohann D'ANELLO
bcd96b2ed8 The BDE membership and the club membership must now be in two parts 2020-09-18 12:35:36 +02:00
381 changed files with 31826 additions and 7716 deletions

View File

@ -10,7 +10,6 @@ DJANGO_SECRET_KEY=CHANGE_ME
DJANGO_SETTINGS_MODULE=note_kfet.settings
CONTACT_EMAIL=tresorerie.bde@localhost
NOTE_URL=localhost
DOMAIN=localhost
# Config for mails. Only used in production
NOTE_MAIL=notekfet@localhost

7
.gitignore vendored
View File

@ -42,8 +42,15 @@ map.json
backups/
/static/
/media/
/tmp/
# Virtualenv
env/
venv/
db.sqlite3
shell.nix
# ansibles customs host
ansible/host_vars/*.yaml
!ansible/host_vars/bde*
ansible/hosts

View File

@ -1,30 +1,16 @@
stages:
- test
- quality-assurance
- docs
# Also fetch submodules
variables:
GIT_SUBMODULE_STRATEGY: recursive
# Debian Buster
py37-django22:
# Ubuntu 22.04
py310-django42:
stage: test
image: debian:buster-backports
before_script:
- >
apt-get update &&
apt-get install --no-install-recommends -t buster-backports -y
python3-django python3-django-crispy-forms
python3-django-extensions python3-django-filters python3-django-polymorphic
python3-djangorestframework python3-django-cas-server python3-psycopg2 python3-pil
python3-babel python3-lockfile python3-pip python3-phonenumbers
python3-bs4 python3-setuptools tox texlive-xetex
script: tox -e py37-django22
# Ubuntu 20.04
py38-django22:
stage: test
image: ubuntu:20.04
image: ubuntu:22.04
before_script:
# Fix tzdata prompt
- ln -sf /usr/share/zoneinfo/Europe/Paris /etc/localtime && echo Europe/Paris > /etc/timezone
@ -33,17 +19,46 @@ py38-django22:
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-cas-server python3-psycopg2 python3-pil
python3-babel python3-lockfile python3-pip python3-phonenumbers
python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil
python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache
python3-bs4 python3-setuptools tox texlive-xetex
script: tox -e py38-django22
script: tox -e py310-django42
# Debian Bookworm
py311-django42:
stage: test
image: debian:bookworm
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 py311-django42
linters:
stage: quality-assurance
image: debian:buster-backports
image: debian:bookworm
before_script:
- apt-get update && apt-get install -y tox
script: tox -e linters
# Be nice to new contributors, but please use `tox`
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
View File

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

View File

@ -8,8 +8,8 @@ RUN apt-get update && \
apt-get install --no-install-recommends -t buster-backports -y \
python3-django python3-django-crispy-forms \
python3-django-extensions python3-django-filters python3-django-polymorphic \
python3-djangorestframework python3-django-cas-server python3-psycopg2 python3-pil \
python3-babel python3-lockfile python3-pip python3-phonenumbers ipython3 \
python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil \
python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache ipython3 \
python3-bs4 python3-setuptools \
uwsgi uwsgi-plugin-python3 \
texlive-xetex gettext libjs-bootstrap4 fonts-font-awesome && \

View File

@ -1,8 +1,8 @@
# NoteKfet 2020
[![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0.txt)
[![pipeline status](https://gitlab.crans.org/bde/nk20/badges/master/pipeline.svg)](https://gitlab.crans.org/bde/nk20/commits/master)
[![coverage report](https://gitlab.crans.org/bde/nk20/badges/master/coverage.svg)](https://gitlab.crans.org/bde/nk20/commits/master)
[![pipeline status](https://gitlab.crans.org/bde/nk20/badges/main/pipeline.svg)](https://gitlab.crans.org/bde/nk20/commits/main)
[![coverage report](https://gitlab.crans.org/bde/nk20/badges/main/coverage.svg)](https://gitlab.crans.org/bde/nk20/commits/main)
## Table des matières
@ -55,10 +55,16 @@ Bien que cela permette de créer une instance sur toutes les distributions,
(env)$ ./manage.py makemigrations
(env)$ ./manage.py migrate
(env)$ ./manage.py loaddata initial
(env)$ ./manage.py createsuperuser # Création d'un utilisateur initial
(env)$ ./manage.py createsuperuser # Création d'un⋅e utilisateur⋅rice initial
```
6. Enjoy :
6. (Optionnel) **Création d'une clé privée OpenID Connect**
Pour activer le support d'OpenID Connect, il faut générer une clé privée, par
exemple avec openssl (`openssl genrsa -out oidc.key 4096`), et renseigner son
emplacement dans `OIDC_RSA_PRIVATE_KEY` (par défaut `/var/secrets/oidc.key`).
7. Enjoy :
```bash
(env)$ ./manage.py runserver 0.0.0.0:8000
@ -69,13 +75,31 @@ accessible depuis l'ensemble de votre réseau, pratique pour tester le rendu
de la note sur un téléphone !
## 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.**
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**.
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.
0. Sous Debian Buster, **activer Debian Backports.** En effet Django 2.2 LTS n'est que disponible dans les backports.
@ -93,10 +117,10 @@ Sinon vous pouvez suivre les étapes décrites ci-dessous.
$ sudo apt install --no-install-recommends -t buster-backports -y \
python3-django python3-django-crispy-forms \
python3-django-extensions python3-django-filters python3-django-polymorphic \
python3-djangorestframework python3-django-cas-server python3-psycopg2 python3-pil \
python3-babel python3-lockfile python3-pip python3-phonenumbers ipython3 \
python3-bs4 python3-setuptools \
uwsgi uwsgi-plugin-python3 \
python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil \
python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache ipython3 \
python3-bs4 python3-setuptools python3-docutils \
memcached uwsgi uwsgi-plugin-python3 \
texlive-xetex gettext libjs-bootstrap4 fonts-font-awesome \
nginx python3-venv git acl
```
@ -210,7 +234,13 @@ Sinon vous pouvez suivre les étapes décrites ci-dessous.
(env)$ ./manage.py check # pas de bêtise qui traine
(env)$ ./manage.py migrate
7. *Enjoy \o/*
7. **Création d'une clé privée OpenID Connect**
Pour activer le support d'OpenID Connect, il faut générer une clé privée, par
exemple avec openssl (`openssl genrsa -out oidc.key 4096`), et renseigner son
emplacement dans `OIDC_RSA_PRIVATE_KEY` (par défaut `/var/secrets/oidc.key`).
8. *Enjoy \o/*
### Installation avec Docker
@ -261,20 +291,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.
**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
### 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
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
```bash
django-admin compilemessages
python3 manage.py compilemessages
python3 manage.py compilejsmessages
```

View File

@ -7,7 +7,7 @@
prompt: "Password of the database (leave it blank to skip database init)"
private: yes
vars:
mirror: deb.debian.org
mirror: eclats.crans.org
roles:
- 1-apt-basic
- 2-nk20
@ -16,3 +16,4 @@
- 5-nginx
- 6-psql
- 7-postinstall
- 8-docs

View File

@ -1,5 +0,0 @@
---
note:
server_name: note-beta.crans.org
git_branch: beta
cron_enabled: false

View File

@ -2,4 +2,6 @@
note:
server_name: note-dev.crans.org
git_branch: beta
serve_static: false
cron_enabled: false
email: notekfet2020@lists.crans.org

View File

@ -1,5 +1,7 @@
---
note:
server_name: note.crans.org
git_branch: master
git_branch: main
serve_static: true
cron_enabled: true
email: notekfet2020@lists.crans.org

View File

@ -1,6 +1,5 @@
[dev]
bde3-virt.adh.crans.org
bde-nk20-beta.adh.crans.org
bde-note-dev.adh.crans.org
[prod]
bde-note.adh.crans.org

View File

@ -1,13 +1,15 @@
---
- name: Add buster-backports to apt sources
- name: Add buster-backports to apt sources if needed
apt_repository:
repo: deb http://{{ mirror }}/debian buster-backports main
state: present
when:
- ansible_distribution == "Debian"
- ansible_distribution_major_version | int == 10
- name: Install note_kfet APT dependencies
apt:
update_cache: true
default_release: buster-backports
install_recommends: false
name:
# Common tools
@ -23,13 +25,14 @@
- python3-babel
- python3-bs4
- python3-django
- python3-django-cas-server
- python3-django-crispy-forms
- python3-django-extensions
- python3-django-filters
- python3-django-oauth-toolkit
- python3-django-polymorphic
- python3-djangorestframework
- python3-lockfile
- python3-memcache
- python3-phonenumbers
- python3-pil
- python3-pip
@ -40,6 +43,9 @@
# LaTeX (PDF generation)
- texlive-xetex
# Cache server
- memcached
# WSGI server
- uwsgi
- uwsgi-plugin-python3

View File

@ -16,7 +16,7 @@
- name: Use default env vars (should be updated!)
template:
src: "env_example"
src: "env.j2"
dest: "/var/www/note_kfet/.env"
mode: 0644
force: false
@ -36,3 +36,13 @@
dest: /etc/cron.d/note
owner: 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'

View 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=

View File

@ -9,6 +9,11 @@
retries: 3
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
file:
path: /etc/letsencrypt/conf.d
@ -19,3 +24,17 @@
src: "letsencrypt/conf.d/nk20.ini.j2"
dest: "/etc/letsencrypt/conf.d/nk20.ini"
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

View File

@ -10,7 +10,7 @@ rsa-key-size = 4096
# server = https://acme-staging.api.letsencrypt.org/directory
# 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
text = True

View File

@ -1,5 +1,5 @@
# the upstream component nginx needs to connect to
upstream note{
upstream note {
server unix:///var/www/note_kfet/note_kfet.sock; # file socket
}
@ -41,6 +41,7 @@ server {
# max upload size
client_max_body_size 75M; # adjust to taste
{% if note.serve_static %}
# Django media
location /media {
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
}
{% endif %}
location /doc {
alias /var/www/documentation; # The documentation of the project
}
# Finally, send all non-media requests to the Django server.
location / {
uwsgi_pass note;

View File

@ -11,14 +11,14 @@
until: pkg_result is succeeded
- 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:
name: note
password: "{{ DB_PASSWORD }}"
become_user: postgres
- name: Create NK20 database
when: "DB_PASSWORD|bool"
when: DB_PASSWORD|length >0
postgresql_db:
name: note_db
owner: note

View File

@ -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
command: /var/www/note_kfet/env/bin/python manage.py migrate
args:
@ -11,14 +17,14 @@
chdir: /var/www/note_kfet
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
command: /var/www/note_kfet/env/bin/python manage.py loaddata initial
args:
chdir: /var/www/note_kfet
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

View 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

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
default_app_config = 'activity.apps.ActivityConfig'

View File

@ -1,11 +1,11 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib import admin
from note_kfet.admin import admin_site
from .forms import GuestForm
from .models import Activity, ActivityType, Entry, Guest
from .models import Activity, ActivityType, Entry, Guest, Opener
@admin.register(Activity, site=admin_site)
@ -35,7 +35,7 @@ class GuestAdmin(admin.ModelAdmin):
"""
Admin customisation for Guest
"""
list_display = ('last_name', 'first_name', 'activity', 'inviter')
list_display = ('last_name', 'first_name', 'school', 'activity', 'inviter')
form = GuestForm
@ -45,3 +45,11 @@ class EntryAdmin(admin.ModelAdmin):
Admin customisation for Entry
"""
list_display = ('note', 'activity', 'time', 'guest')
@admin.register(Opener, site=admin_site)
class OpenerAdmin(admin.ModelAdmin):
"""
Admin customisation for Opener
"""
list_display = ('activity', 'opener')

View File

@ -1,9 +1,11 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from rest_framework.validators import UniqueTogetherValidator
from ..models import Activity, ActivityType, Entry, Guest, GuestTransaction
from ..models import Activity, ActivityType, Entry, Guest, GuestTransaction, Opener
class ActivityTypeSerializer(serializers.ModelSerializer):
@ -59,3 +61,17 @@ class GuestTransactionSerializer(serializers.ModelSerializer):
class Meta:
model = GuestTransaction
fields = '__all__'
class OpenerSerializer(serializers.ModelSerializer):
"""
REST API Serializer for Openers.
The djangorestframework plugin will analyse the model `Opener` and parse all fields in the API.
"""
class Meta:
model = Opener
fields = '__all__'
validators = [UniqueTogetherValidator(
queryset=Opener.objects.all(), fields=("opener", "activity"),
message=_("This opener already exists"))]

View File

@ -1,7 +1,7 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from .views import ActivityTypeViewSet, ActivityViewSet, EntryViewSet, GuestViewSet
from .views import ActivityTypeViewSet, ActivityViewSet, EntryViewSet, GuestViewSet, OpenerViewSet
def register_activity_urls(router, path):
@ -12,3 +12,4 @@ def register_activity_urls(router, path):
router.register(path + '/type', ActivityTypeViewSet)
router.register(path + '/guest', GuestViewSet)
router.register(path + '/entry', EntryViewSet)
router.register(path + '/opener', OpenerViewSet)

View File

@ -1,12 +1,15 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from api.filters import RegexSafeSearchFilter
from api.viewsets import ReadProtectedModelViewSet
from django.core.exceptions import ValidationError
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import SearchFilter
from rest_framework.response import Response
from rest_framework import status
from .serializers import ActivitySerializer, ActivityTypeSerializer, EntrySerializer, GuestSerializer
from ..models import Activity, ActivityType, Entry, Guest
from .serializers import ActivitySerializer, ActivityTypeSerializer, EntrySerializer, GuestSerializer, OpenerSerializer
from ..models import Activity, ActivityType, Entry, Guest, Opener
class ActivityTypeViewSet(ReadProtectedModelViewSet):
@ -15,10 +18,10 @@ class ActivityTypeViewSet(ReadProtectedModelViewSet):
The djangorestframework plugin will get all `ActivityType` objects, serialize it to JSON with the given serializer,
then render it on /api/activity/type/
"""
queryset = ActivityType.objects.all()
queryset = ActivityType.objects.order_by('id')
serializer_class = ActivityTypeSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ['name', 'can_invite', ]
filterset_fields = ['name', 'manage_entries', 'can_invite', 'guest_entry_fee', ]
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,
then render it on /api/activity/activity/
"""
queryset = Activity.objects.all()
queryset = Activity.objects.order_by('id')
serializer_class = ActivitySerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ['name', 'description', 'activity_type', ]
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
filterset_fields = ['name', 'description', 'activity_type', 'location', 'creater', 'organizer', 'attendees_club',
'date_start', 'date_end', 'valid', 'open', ]
search_fields = ['$name', '$description', '$location', '$creater__last_name', '$creater__first_name',
'$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):
@ -39,10 +48,13 @@ class GuestViewSet(ReadProtectedModelViewSet):
The djangorestframework plugin will get all `Guest` objects, serialize it to JSON with the given serializer,
then render it on /api/activity/guest/
"""
queryset = Guest.objects.all()
queryset = Guest.objects.order_by('id')
serializer_class = GuestSerializer
filter_backends = [SearchFilter]
search_fields = ['$last_name', '$first_name', '$inviter__alias__name', '$inviter__alias__normalized_name', ]
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
filterset_fields = ['activity', 'activity__name', 'last_name', 'first_name', 'school', 'inviter', 'inviter__alias__name',
'inviter__alias__normalized_name', ]
search_fields = ['$activity__name', '$last_name', '$first_name', '$school', '$inviter__user__email', '$inviter__alias__name',
'$inviter__alias__normalized_name', ]
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,
then render it on /api/activity/entry/
"""
queryset = Entry.objects.all()
queryset = Entry.objects.order_by('id')
serializer_class = EntrySerializer
filter_backends = [SearchFilter]
search_fields = ['$last_name', '$first_name', '$inviter__alias__name', '$inviter__alias__normalized_name', ]
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
filterset_fields = ['activity', 'time', 'note', 'guest', ]
search_fields = ['$activity__name', '$note__user__email', '$note__alias__name', '$note__alias__normalized_name',
'$guest__last_name', '$guest__first_name', ]
class OpenerViewSet(ReadProtectedModelViewSet):
"""
REST Opener View set.
The djangorestframework plugin will get all `Opener` objects, serialize it to JSON with the given serializer,
then render it on /api/activity/opener/
"""
queryset = Opener.objects
serializer_class = OpenerSerializer
filter_backends = [RegexSafeSearchFilter, DjangoFilterBackend]
search_fields = ['$opener__alias__name', '$opener__alias__normalized_name',
'$activity__name']
filterset_fields = ['opener', 'opener__noteuser__user', 'activity']
def get_serializer_class(self):
serializer_class = self.serializer_class
if self.request.method in ['PUT', 'PATCH']:
# opener-activity can't change
serializer_class.Meta.read_only_fields = ('opener', 'acitivity',)
return serializer_class
def destroy(self, request, *args, **kwargs):
instance = self.get_object()
try:
self.perform_destroy(instance)
except ValidationError as e:
return Response({e.code: str(e)}, status.HTTP_400_BAD_REQUEST)
return Response(status=status.HTTP_204_NO_CONTENT)

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.apps import AppConfig

View File

@ -6,7 +6,7 @@
"name": "Pot",
"manage_entries": true,
"can_invite": true,
"guest_entry_fee": 500
"guest_entry_fee": 1000
}
},
{
@ -28,5 +28,25 @@
"can_invite": false,
"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
}
}
]

View File

@ -1,17 +1,18 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from datetime import timedelta
from random import shuffle
from bootstrap_datepicker_plus.widgets import DateTimePickerInput
from django import forms
from django.contrib.contenttypes.models import ContentType
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from member.models import Club
from note.models import Note, NoteUser
from note_kfet.inputs import Autocomplete, DateTimePickerInput
from note_kfet.middlewares import get_current_authenticated_user
from note_kfet.inputs import Autocomplete
from note_kfet.middlewares import get_current_request
from permission.backends import PermissionBackend
from .models import Activity, Guest
@ -24,10 +25,16 @@ class ActivityForm(forms.ModelForm):
self.fields["attendees_club"].initial = Club.objects.get(name="Kfet")
self.fields["attendees_club"].widget.attrs["placeholder"] = "Kfet"
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)
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):
date_end = self.cleaned_data["date_end"]
date_start = self.cleaned_data["date_start"]
@ -37,7 +44,7 @@ class ActivityForm(forms.ModelForm):
class Meta:
model = Activity
exclude = ('creater', 'valid', 'open', )
exclude = ('creater', 'valid', 'open', 'opener', )
widgets = {
"organizer": Autocomplete(
model=Club,
@ -100,7 +107,7 @@ class GuestForm(forms.ModelForm):
class Meta:
model = Guest
fields = ('last_name', 'first_name', 'inviter', )
fields = ('last_name', 'first_name', 'school', 'inviter', )
widgets = {
"inviter": Autocomplete(
NoteUser,

View 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'),
),
]

View 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')},
},
),
]

View File

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

View File

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

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import os
@ -11,7 +11,7 @@ from django.db import models, transaction
from django.db.models import Q
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from note.models import NoteUser, Transaction
from note.models import NoteUser, Transaction, Note
from rest_framework.exceptions import ValidationError
@ -66,6 +66,8 @@ class Activity(models.Model):
description = models.TextField(
verbose_name=_('description'),
blank=True,
default="",
)
location = models.CharField(
@ -123,6 +125,14 @@ class Activity(models.Model):
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
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()
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):
"""
@ -199,7 +201,8 @@ class Entry(models.Model):
def save(self, *args, **kwargs):
qs = Entry.objects.filter(~Q(pk=self.pk), activity=self.activity, note=self.note, guest=self.guest)
if qs.exists():
raise ValidationError(_("Already entered on ") + _("{:%Y-%m-%d %H:%M:%S}").format(qs.get().time, ))
raise ValidationError(_("Already entered on ")
+ _("{:%Y-%m-%d %H:%M:%S}").format(timezone.localtime(qs.get().time), ))
if self.guest:
self.note = self.guest.inviter
@ -245,6 +248,11 @@ class Guest(models.Model):
verbose_name=_("first name"),
)
school = models.CharField(
max_length=255,
verbose_name=_("school"),
)
inviter = models.ForeignKey(
NoteUser,
on_delete=models.PROTECT,
@ -252,14 +260,13 @@ class Guest(models.Model):
verbose_name=_("inviter"),
)
@property
def has_entry(self):
try:
if self.entry:
return True
return False
except AttributeError:
return False
class Meta:
verbose_name = _("guest")
verbose_name_plural = _("guests")
unique_together = ("activity", "last_name", "first_name", )
def __str__(self):
return self.first_name + " " + self.last_name
@transaction.atomic
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
@ -290,13 +297,14 @@ class Guest(models.Model):
return super().save(force_insert, force_update, using, update_fields)
def __str__(self):
return self.first_name + " " + self.last_name
class Meta:
verbose_name = _("guest")
verbose_name_plural = _("guests")
unique_together = ("activity", "last_name", "first_name", )
@property
def has_entry(self):
try:
if self.entry:
return True
return False
except AttributeError:
return False
class GuestTransaction(Transaction):
@ -308,3 +316,31 @@ class GuestTransaction(Transaction):
@property
def type(self):
return _('Invitation')
class Opener(models.Model):
"""
Allow the user to make activity entries without more rights
"""
activity = models.ForeignKey(
Activity,
on_delete=models.CASCADE,
related_name='opener',
verbose_name=_('activity')
)
opener = models.ForeignKey(
Note,
on_delete=models.CASCADE,
related_name='activity_responsible',
verbose_name=_('Opener')
)
class Meta:
verbose_name = _("Opener")
verbose_name_plural = _("Openers")
unique_together = ("opener", "activity")
def __str__(self):
return _("{opener} is opener of activity {acivity}").format(
opener=str(self.opener), acivity=str(self.activity))

View 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)
})

View File

@ -1,13 +1,17 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.utils import timezone
from django.utils.html import format_html
from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
from note_kfet.middlewares import get_current_request
import django_tables2 as tables
from django_tables2 import A
from permission.backends import PermissionBackend
from note.templatetags.pretty_money import pretty_money
from .models import Activity, Entry, Guest
from .models import Activity, Entry, Guest, Opener
class ActivityTable(tables.Table):
@ -47,13 +51,13 @@ class GuestTable(tables.Table):
}
model = Guest
template_name = 'django_tables2/bootstrap4.html'
fields = ("last_name", "first_name", "inviter", )
fields = ("last_name", "first_name", "inviter", "school")
def render_entry(self, record):
if record.has_entry:
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)"> '
'{delete_trans}</button>'.format(id=record.id, delete_trans=_("remove").capitalize()))
return str(_("Entered on ") + str(_("{:%Y-%m-%d %H:%M:%S}").format(timezone.localtime(record.entry.time))))
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()))
def get_row_class(record):
@ -91,7 +95,7 @@ class EntryTable(tables.Table):
if hasattr(record, 'username'):
username = record.username
if username != value:
return format_html(value + " <em>aka.</em> " + username)
return mark_safe(escape(value) + " <em>aka.</em> " + escape(username))
return value
def render_balance(self, value):
@ -111,3 +115,34 @@ class EntryTable(tables.Table):
'data-last-name': lambda record: record.last_name,
'data-first-name': lambda record: record.first_name,
}
# function delete_button(id) provided in template file
DELETE_TEMPLATE = """
<button id="{{ record.pk }}" class="btn btn-danger btn-sm" onclick="delete_button(this.id)"> {{ delete_trans }}</button>
"""
class OpenerTable(tables.Table):
class Meta:
attrs = {
'class': 'table table condensed table-striped',
'id': "opener_table"
}
model = Opener
fields = ("opener",)
template_name = 'django_tables2/bootstrap4.html'
show_header = False
opener = tables.Column(attrs={'td': {'class': 'text-center'}})
delete_col = tables.TemplateColumn(
template_code=DELETE_TEMPLATE,
extra_context={"delete_trans": _('Delete')},
attrs={
'td': {
'class': lambda record: 'col-sm-1'
+ (' d-none' if not PermissionBackend.check_perm(
get_current_request(), "activity.delete_opener", record)
else '')}},
verbose_name=_("Delete"),)

View File

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

View File

@ -63,7 +63,12 @@ SPDX-License-Identifier: GPL-3.0-or-later
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);
@ -86,10 +91,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
}).done(function () {
if (target.hasClass("table-info"))
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);
else
addMsg("Entrée effectuée !", "success", 4000);
addMsg("Entry made!", "success", 4000);
reloadTable(true);
}).fail(function (xhr) {
errMsg(xhr.responseJSON, 4000);
@ -121,10 +126,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
}).done(function () {
if (target.hasClass("table-info"))
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);
else
addMsg("Entrée effectuée !", "success", 4000);
addMsg("{% trans "Entry done!" %}", "success", 4000);
reloadTable(true);
}).fail(function (xhr) {
errMsg(xhr.responseJSON, 4000);

View File

@ -17,4 +17,27 @@ SPDX-License-Identifier: GPL-3.0-or-later
</form>
</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 %}

View File

@ -46,4 +46,4 @@ SPDX-License-Identifier: GPL-3.0-or-later
</h3>
{% render_table table %}
</div>
{% endblock %}
{% endblock %}

View File

@ -1,15 +1,18 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from datetime import timedelta
from api.tests import TestAPI
from django.contrib.auth.models import User
from django.test import TestCase
from django.urls import reverse
from django.utils import timezone
from activity.models import Activity, ActivityType, Guest, Entry
from member.models import Club
from ..api.views import ActivityTypeViewSet, ActivityViewSet, EntryViewSet, GuestViewSet
from ..models import Activity, ActivityType, Guest, Entry
class TestActivities(TestCase):
"""
@ -47,6 +50,7 @@ class TestActivities(TestCase):
inviter=self.user.note,
last_name="GUEST",
first_name="Guest",
school="School",
)
def test_activity_list(self):
@ -153,6 +157,7 @@ class TestActivities(TestCase):
inviter=self.user.note.id,
last_name="GUEST2",
first_name="Guest",
school="School",
))
self.assertEqual(response.status_code, 200)
@ -164,6 +169,7 @@ class TestActivities(TestCase):
inviter=self.user.note.id,
last_name="GUEST2",
first_name="Guest",
school="School",
))
self.assertRedirects(response, reverse("activity:activity_detail", args=(self.activity.pk,)), 302, 200)
@ -173,3 +179,59 @@ class TestActivities(TestCase):
"""
response = self.client.get(reverse("activity:calendar_ics"))
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",
school="School",
)
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/")

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.urls import path

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from hashlib import md5
@ -12,17 +12,21 @@ from django.db.models import F, Q
from django.http import HttpResponse
from django.urls import reverse_lazy
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.utils.translation import gettext_lazy as _
from django.views import View
from django.views.decorators.cache import cache_page
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 permission.backends import PermissionBackend
from permission.views import ProtectQuerysetMixin, ProtectedCreateView
from .forms import ActivityForm, GuestForm
from .models import Activity, Entry, Guest
from .tables import ActivityTable, EntryTable, GuestTable
from .models import Activity, Entry, Guest, Opener
from .tables import ActivityTable, EntryTable, GuestTable, OpenerTable
class ActivityCreateView(ProtectQuerysetMixin, ProtectedCreateView):
@ -55,36 +59,44 @@ class ActivityCreateView(ProtectQuerysetMixin, ProtectedCreateView):
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.
"""
model = Activity
table_class = ActivityTable
ordering = ('-date_start',)
tables = [
lambda data: ActivityTable(data, prefix="all-"),
lambda data: ActivityTable(data, prefix="upcoming-"),
]
extra_context = {"title": _("Activities")}
def get_queryset(self):
return super().get_queryset().distinct()
def get_queryset(self, **kwargs):
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):
context = super().get_context_data(**kwargs)
upcoming_activities = Activity.objects.filter(date_end__gt=timezone.now())
context['upcoming'] = ActivityTable(
data=upcoming_activities.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view")),
prefix='upcoming-',
)
tables = context["tables"]
for name, table in zip(["table", "upcoming"], tables):
context[name] = table
started_activities = Activity.objects\
.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view"))\
.filter(open=True, valid=True).all()
started_activities = self.get_queryset().filter(open=True, valid=True).distinct().all()
context["started_activities"] = started_activities
return context
class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, DetailView):
"""
Shows details about one activity. Add guest to context
"""
@ -92,15 +104,40 @@ class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
context_object_name = "activity"
extra_context = {"title": _("Activity detail")}
tables = [
lambda data: GuestTable(data, prefix="guests-"),
lambda data: OpenerTable(data, prefix="opener-"),
]
def get_tables_data(self):
return [
Guest.objects.filter(activity=self.object)
.filter(PermissionBackend.filter_queryset(self.request, Guest, "view")),
self.object.opener.filter(activity=self.object)
.filter(PermissionBackend.filter_queryset(self.request, Opener, "view")),
]
def get_context_data(self, **kwargs):
context = super().get_context_data()
table = GuestTable(data=Guest.objects.filter(activity=self.object)
.filter(PermissionBackend.filter_queryset(self.request.user, Guest, "view")))
context["guests"] = table
tables = context["tables"]
for name, table in zip(["guests", "opener"], tables):
context[name] = table
context["activity_started"] = timezone.now() > timezone.localtime(self.object.date_start)
context["widget"] = {
"name": "opener",
"resetable": True,
"attrs": {
"class": "autocomplete form-control",
"id": "opener",
"api_url": "/api/note/alias/?note__polymorphic_ctype__model=noteuser",
"name_field": "name",
"placeholder": ""
}
}
return context
@ -131,6 +168,7 @@ class ActivityInviteView(ProtectQuerysetMixin, ProtectedCreateView):
activity=activity,
first_name="",
last_name="",
school="",
inviter=self.request.user.note,
)
@ -142,36 +180,41 @@ class ActivityInviteView(ProtectQuerysetMixin, ProtectedCreateView):
def get_form(self, form_class=None):
form = super().get_form(form_class)
form.activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view"))\
.get(pk=self.kwargs["pk"])
form.activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request, Activity, "view"))\
.filter(pk=self.kwargs["pk"]).first()
form.fields["inviter"].initial = self.request.user.note
return form
@transaction.atomic
def form_valid(self, form):
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)
def get_success_url(self, **kwargs):
return reverse_lazy('activity:activity_detail', kwargs={"pk": self.kwargs["pk"]})
class ActivityEntryView(LoginRequiredMixin, TemplateView):
class ActivityEntryView(LoginRequiredMixin, SingleTableMixin, TemplateView):
"""
Manages entry to an activity
"""
template_name = "activity/activity_entry.html"
table_class = EntryTable
def dispatch(self, request, *args, **kwargs):
"""
Don't display the entry interface if the user has no right to see it (no right to add an entry for itself),
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"])
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."))
if not activity.activity_type.manage_entries:
@ -189,22 +232,25 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
guest_qs = Guest.objects\
.annotate(balance=F("inviter__balance"), note_name=F("inviter__user__username"))\
.filter(activity=activity)\
.filter(PermissionBackend.filter_queryset(self.request.user, Guest, "view"))\
.order_by('last_name', 'first_name').distinct()
.filter(PermissionBackend.filter_queryset(self.request, Guest, "view"))\
.order_by('last_name', 'first_name')
if "search" in self.request.GET and self.request.GET["search"]:
pattern = self.request.GET["search"]
if pattern[0] != "^":
pattern = "^" + pattern
# Check if this is a valid regex. If not, we won't check regex
valid_regex = is_regex(pattern)
suffix = "__iregex" if valid_regex else "__istartswith"
pattern = "^" + pattern if valid_regex and pattern[0] != "^" else pattern
guest_qs = guest_qs.filter(
Q(first_name__iregex=pattern)
| Q(last_name__iregex=pattern)
| Q(inviter__alias__name__iregex=pattern)
| Q(inviter__alias__normalized_name__iregex=Alias.normalize(pattern))
Q(**{f"first_name{suffix}": pattern})
| Q(**{f"last_name{suffix}": pattern})
| Q(**{f"inviter__alias__name{suffix}": pattern})
| Q(**{f"inviter__alias__normalized_name{suffix}": Alias.normalize(pattern)})
)
else:
guest_qs = guest_qs.none()
return guest_qs
return guest_qs.distinct()
def get_invited_note(self, activity):
"""
@ -220,23 +266,26 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
# Keep only users that have a note
note_qs = note_qs.filter(note__noteuser__isnull=False)
# Keep only members
# Keep only valid members
note_qs = note_qs.filter(
note__noteuser__user__memberships__club=activity.attendees_club,
note__noteuser__user__memberships__date_start__lte=timezone.now(),
note__noteuser__user__memberships__date_end__gte=timezone.now(),
)
note__noteuser__user__memberships__date_end__gte=timezone.now()).exclude(note__inactivity_reason='forced')
# Filter with permission backend
note_qs = note_qs.filter(PermissionBackend.filter_queryset(self.request.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"]:
pattern = self.request.GET["search"]
# Check if this is a valid regex. If not, we won't check regex
valid_regex = is_regex(pattern)
suffix = "__iregex" if valid_regex else "__icontains"
note_qs = note_qs.filter(
Q(note__noteuser__user__first_name__iregex=pattern)
| Q(note__noteuser__user__last_name__iregex=pattern)
| Q(name__iregex=pattern)
| Q(normalized_name__iregex=Alias.normalize(pattern))
Q(**{f"note__noteuser__user__first_name{suffix}": pattern})
| Q(**{f"note__noteuser__user__last_name{suffix}": pattern})
| Q(**{f"name{suffix}": pattern})
| Q(**{f"normalized_name{suffix}": Alias.normalize(pattern)})
)
else:
note_qs = note_qs.none()
@ -248,15 +297,9 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
if settings.DATABASES[note_qs.db]["ENGINE"] == 'django.db.backends.postgresql' else note_qs.distinct()[:20]
return note_qs
def get_context_data(self, **kwargs):
"""
Query the list of Guest and Note to the activity and add information to makes entry with JS.
"""
context = super().get_context_data(**kwargs)
activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view"))\
def get_table_data(self):
activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request, Activity, "view"))\
.distinct().get(pk=self.kwargs["pk"])
context["activity"] = activity
matched = []
@ -269,8 +312,17 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
note.activity = activity
matched.append(note)
table = EntryTable(data=matched)
context["table"] = table
return matched
def get_context_data(self, **kwargs):
"""
Query the list of Guest and Note to the activity and add information to makes entry with JS.
"""
context = super().get_context_data(**kwargs)
activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request, Activity, "view"))\
.distinct().get(pk=self.kwargs["pk"])
context["activity"] = activity
context["entries"] = Entry.objects.filter(activity=activity)
@ -278,16 +330,18 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
context["noteuser_ctype"] = ContentType.objects.get_for_model(NoteUser).pk
context["notespecial_ctype"] = ContentType.objects.get_for_model(NoteSpecial).pk
activities_open = Activity.objects.filter(open=True).filter(
PermissionBackend.filter_queryset(self.request.user, Activity, "view")).distinct().all()
activities_open = Activity.objects.filter(open=True, activity_type__manage_entries=True).filter(
PermissionBackend.filter_queryset(self.request, Activity, "view")).distinct().all()
context["activities_open"] = [a for a in activities_open
if PermissionBackend.check_perm(self.request.user,
if PermissionBackend.check_perm(self.request,
"activity.add_entry",
Entry(activity=a, note=self.request.user.note,))]
return context
# Cache for 1 hour
@method_decorator(cache_page(60 * 60), name='dispatch')
class CalendarView(View):
"""
Render an ICS calendar with all valid activities.
@ -310,8 +364,8 @@ X-WR-CALNAME:Kfet Calendar
NAME:Kfet Calendar
CALSCALE:GREGORIAN
BEGIN:VTIMEZONE
TZID:Europe/Berlin
X-LIC-LOCATION:Europe/Berlin
TZID:Europe/Paris
X-LIC-LOCATION:Europe/Paris
BEGIN:DAYLIGHT
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
@ -333,10 +387,10 @@ END:VTIMEZONE
DTSTAMP:{"{:%Y%m%dT%H%M%S}".format(activity.date_start)}Z
UID:{md5((activity.name + "$" + str(activity.id) + str(activity.date_start)).encode("UTF-8")).hexdigest()}
SUMMARY;CHARSET=UTF-8:{self.multilines(activity.name, 75, 22)}
DTSTART;TZID=Europe/Berlin:{"{:%Y%m%dT%H%M%S}".format(activity.date_start)}
DTEND;TZID=Europe/Berlin:{"{:%Y%m%dT%H%M%S}".format(activity.date_end)}
DTSTART:{"{:%Y%m%dT%H%M%S}Z".format(activity.date_start)}
DTEND:{"{:%Y%m%dT%H%M%S}Z".format(activity.date_end)}
LOCATION:{self.multilines(activity.location, 75, 9) if activity.location else "Kfet"}
DESCRIPTION;CHARSET=UTF-8:""" + self.multilines(activity.description.replace("\n", "\\n"), 75, 26) + """
DESCRIPTION;CHARSET=UTF-8:""" + self.multilines(activity.description.replace("\n", "\\n"), 75, 26) + f"""
-- {activity.organizer.name}
END:VEVENT
"""

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
default_app_config = 'api.apps.APIConfig'

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.apps import AppConfig

42
apps/api/filters.py Normal file
View 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
View File

@ -0,0 +1,5 @@
from rest_framework.pagination import PageNumberPagination
class CustomPagination(PageNumberPagination):
page_size_query_param = 'page_size'

View File

@ -1,13 +1,20 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib.contenttypes.models import ContentType
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.
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.
The djangorestframework plugin will analyse the model `User` and parse all fields in the API.
@ -31,3 +38,54 @@ class ContentTypeSerializer(ModelSerializer):
class Meta:
model = ContentType
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
View File

@ -0,0 +1,241 @@
# Copyright (C) 2018-2025 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/")

View File

@ -1,10 +1,12 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.conf import settings
from django.conf.urls import url, include
from django.conf.urls import include
from django.urls import re_path
from rest_framework import routers
from .views import UserInformationView
from .viewsets import ContentTypeViewSet, UserViewSet
# Routers provide an easy way of automatically determining the URL conf.
@ -13,39 +15,48 @@ router = routers.DefaultRouter()
router.register('models', ContentTypeViewSet)
router.register('user', UserViewSet)
if "member" in settings.INSTALLED_APPS:
from member.api.urls import register_members_urls
register_members_urls(router, 'members')
if "member" in settings.INSTALLED_APPS:
if "activity" in settings.INSTALLED_APPS:
from activity.api.urls import register_activity_urls
register_activity_urls(router, 'activity')
if "note" in settings.INSTALLED_APPS:
from note.api.urls import register_note_urls
register_note_urls(router, 'note')
if "treasury" in settings.INSTALLED_APPS:
from treasury.api.urls import register_treasury_urls
register_treasury_urls(router, 'treasury')
if "permission" in settings.INSTALLED_APPS:
from permission.api.urls import register_permission_urls
register_permission_urls(router, 'permission')
if "food" in settings.INSTALLED_APPS:
from food.api.urls import register_food_urls
register_food_urls(router, 'food')
if "logs" in settings.INSTALLED_APPS:
from logs.api.urls import register_logs_urls
register_logs_urls(router, 'logs')
if "member" in settings.INSTALLED_APPS:
from member.api.urls import register_members_urls
register_members_urls(router, 'members')
if "note" in settings.INSTALLED_APPS:
from note.api.urls import register_note_urls
register_note_urls(router, 'note')
if "permission" in settings.INSTALLED_APPS:
from permission.api.urls import register_permission_urls
register_permission_urls(router, 'permission')
if "treasury" in settings.INSTALLED_APPS:
from treasury.api.urls import register_treasury_urls
register_treasury_urls(router, 'treasury')
if "wei" in settings.INSTALLED_APPS:
from wei.api.urls import register_wei_urls
register_wei_urls(router, 'wei')
if "wrapped" in settings.INSTALLED_APPS:
from wrapped.api.urls import register_wrapped_urls
register_wrapped_urls(router, 'wrapped')
app_name = 'api'
# Wire up our API using automatic URL routing.
# Additionally, we include login URLs for the browsable API.
urlpatterns = [
url('^', include(router.urls)),
url('^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
re_path('^', include(router.urls)),
re_path('^me/', UserInformationView.as_view()),
re_path('^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
]

20
apps/api/views.py Normal file
View File

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

View File

@ -1,6 +1,8 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import re
from django.contrib.contenttypes.models import ContentType
from django_filters.rest_framework import DjangoFilterBackend
from django.db.models import Q
@ -8,12 +10,20 @@ from django.conf import settings
from django.contrib.auth.models import User
from rest_framework.viewsets import ReadOnlyModelViewSet, ModelViewSet
from permission.backends import PermissionBackend
from note_kfet.middlewares import get_current_session
from note.models import Alias
from .filters import RegexSafeSearchFilter
from .serializers import UserSerializer, ContentTypeSerializer
def is_regex(pattern):
try:
re.compile(pattern)
return True
except (re.error, TypeError):
return False
class ReadProtectedModelViewSet(ModelViewSet):
"""
Protect a ModelViewSet by filtering the objects that the user cannot see.
@ -24,9 +34,7 @@ class ReadProtectedModelViewSet(ModelViewSet):
self.model = ContentType.objects.get_for_model(self.serializer_class.Meta.model).model_class()
def get_queryset(self):
user = self.request.user
get_current_session().setdefault("permission_mask", 42)
return self.queryset.filter(PermissionBackend.filter_queryset(user, self.model, "view")).distinct()
return self.queryset.filter(PermissionBackend.filter_queryset(self.request, self.model, "view")).distinct()
class ReadOnlyProtectedModelViewSet(ReadOnlyModelViewSet):
@ -39,21 +47,20 @@ class ReadOnlyProtectedModelViewSet(ReadOnlyModelViewSet):
self.model = ContentType.objects.get_for_model(self.serializer_class.Meta.model).model_class()
def get_queryset(self):
user = self.request.user
get_current_session().setdefault("permission_mask", 42)
return self.queryset.filter(PermissionBackend.filter_queryset(user, self.model, "view")).distinct()
return self.queryset.filter(PermissionBackend.filter_queryset(self.request, self.model, "view")).distinct()
class UserViewSet(ReadProtectedModelViewSet):
"""
REST API View set.
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
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):
queryset = super().get_queryset()
@ -63,34 +70,38 @@ class UserViewSet(ReadProtectedModelViewSet):
if "search" in self.request.GET:
pattern = self.request.GET["search"]
# Check if this is a valid regex. If not, we won't check regex
valid_regex = is_regex(pattern)
suffix = "__iregex" if valid_regex else "__istartswith"
prefix = "^" if valid_regex else ""
# Filter with different rules
# We use union-all to keep each filter rule sorted in result
queryset = queryset.filter(
# Match without normalization
note__alias__name__iregex="^" + pattern
Q(**{f"note__alias__name{suffix}": prefix + pattern})
).union(
queryset.filter(
# Match with normalization
Q(note__alias__normalized_name__iregex="^" + Alias.normalize(pattern))
& ~Q(note__alias__name__iregex="^" + pattern)
Q(**{f"note__alias__normalized_name{suffix}": prefix + Alias.normalize(pattern)})
& ~Q(**{f"note__alias__name{suffix}": prefix + pattern})
),
all=True,
).union(
queryset.filter(
# Match on lower pattern
Q(note__alias__normalized_name__iregex="^" + pattern.lower())
& ~Q(note__alias__normalized_name__iregex="^" + Alias.normalize(pattern))
& ~Q(note__alias__name__iregex="^" + pattern)
Q(**{f"note__alias__normalized_name{suffix}": prefix + pattern.lower()})
& ~Q(**{f"note__alias__normalized_name{suffix}": prefix + Alias.normalize(pattern)})
& ~Q(**{f"note__alias__name{suffix}": prefix + pattern})
),
all=True,
).union(
queryset.filter(
# Match on firstname or lastname
(Q(last_name__iregex="^" + pattern) | Q(first_name__iregex="^" + pattern))
& ~Q(note__alias__normalized_name__iregex="^" + pattern.lower())
& ~Q(note__alias__normalized_name__iregex="^" + Alias.normalize(pattern))
& ~Q(note__alias__name__iregex="^" + pattern)
(Q(**{f"last_name{suffix}": prefix + pattern}) | Q(**{f"first_name{suffix}": prefix + pattern}))
& ~Q(**{f"note__alias__normalized_name{suffix}": prefix + pattern.lower()})
& ~Q(**{f"note__alias__normalized_name{suffix}": prefix + Alias.normalize(pattern)})
& ~Q(**{f"note__alias__name{suffix}": prefix + pattern})
),
all=True,
)
@ -106,7 +117,10 @@ class ContentTypeViewSet(ReadOnlyModelViewSet):
"""
REST API View set.
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
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
filterset_fields = ['id', 'app_label', 'model', ]
search_fields = ['$app_label', '$model', ]

0
apps/food/__init__.py Normal file
View File

59
apps/food/admin.py Normal file
View File

@ -0,0 +1,59 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib import admin
from polymorphic.admin import PolymorphicChildModelAdmin, PolymorphicParentModelAdmin
from note_kfet.admin import admin_site
from .models import Allergen, Food, BasicFood, TransformedFood, QRCode
@admin.register(Allergen, site=admin_site)
class AllergenAdmin(admin.ModelAdmin):
"""
Admin customisation for Allergen
"""
ordering = ['name']
@admin.register(Food, site=admin_site)
class FoodAdmin(PolymorphicParentModelAdmin):
"""
Admin customisation for Food
"""
child_models = (Food, BasicFood, TransformedFood)
list_display = ('name', 'expiry_date', 'owner', 'is_ready')
list_filter = ('is_ready', 'end_of_life')
search_fields = ['name']
ordering = ['expiry_date', 'name']
@admin.register(BasicFood, site=admin_site)
class BasicFood(PolymorphicChildModelAdmin):
"""
Admin customisation for BasicFood
"""
list_display = ('name', 'expiry_date', 'date_type', 'owner', 'is_ready')
list_filter = ('is_ready', 'date_type', 'end_of_life')
search_fields = ['name']
ordering = ['expiry_date', 'name']
@admin.register(TransformedFood, site=admin_site)
class TransformedFood(PolymorphicChildModelAdmin):
"""
Admin customisation for TransformedFood
"""
list_display = ('name', 'expiry_date', 'shelf_life', 'owner', 'is_ready')
list_filter = ('is_ready', 'end_of_life', 'shelf_life')
search_fields = ['name']
ordering = ['expiry_date', 'name']
@admin.register(QRCode, site=admin_site)
class QRCodeAdmin(admin.ModelAdmin):
"""
Admin customisation for QRCode
"""
list_diplay = ('qr_code_number', 'food_container')
search_fields = ['food_container__name']

View File

View File

@ -0,0 +1,46 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from rest_framework import serializers
from ..models import Allergen, BasicFood, TransformedFood, QRCode
class AllergenSerializer(serializers.ModelSerializer):
"""
REST API Serializer for Allergen.
The djangorestframework plugin will analyse the model `Allergen` and parse all fields in the API.
"""
class Meta:
model = Allergen
fields = '__all__'
class BasicFoodSerializer(serializers.ModelSerializer):
"""
REST API Serializer for BasicFood.
The djangorestframework plugin will analyse the model `BasicFood` and parse all fields in the API.
"""
class Meta:
model = BasicFood
fields = '__all__'
class TransformedFoodSerializer(serializers.ModelSerializer):
"""
REST API Serializer for TransformedFood.
The djangorestframework plugin will analyse the model `TransformedFood` and parse all fields in the API.
"""
class Meta:
model = TransformedFood
fields = '__all__'
class QRCodeSerializer(serializers.ModelSerializer):
"""
REST API Serializer for QRCode.
The djangorestframework plugin will analyse the model `QRCode` and parse all fields in the API.
"""
class Meta:
model = QRCode
fields = '__all__'

14
apps/food/api/urls.py Normal file
View File

@ -0,0 +1,14 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from .views import AllergenViewSet, BasicFoodViewSet, TransformedFoodViewSet, QRCodeViewSet
def register_food_urls(router, path):
"""
Configure router for Food REST API.
"""
router.register(path + '/allergen', AllergenViewSet)
router.register(path + '/basicfood', BasicFoodViewSet)
router.register(path + '/transformedfood', TransformedFoodViewSet)
router.register(path + '/qrcode', QRCodeViewSet)

61
apps/food/api/views.py Normal file
View File

@ -0,0 +1,61 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from api.viewsets import ReadProtectedModelViewSet
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import SearchFilter
from .serializers import AllergenSerializer, BasicFoodSerializer, TransformedFoodSerializer, QRCodeSerializer
from ..models import Allergen, BasicFood, TransformedFood, QRCode
class AllergenViewSet(ReadProtectedModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `Allergen` objects, serialize it to JSON with the given serializer,
then render it on /api/food/allergen/
"""
queryset = Allergen.objects.order_by('id')
serializer_class = AllergenSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['name', ]
search_fields = ['$name', ]
class BasicFoodViewSet(ReadProtectedModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `BasicFood` objects, serialize it to JSON with the given serializer,
then render it on /api/food/basicfood/
"""
queryset = BasicFood.objects.order_by('id')
serializer_class = BasicFoodSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['name', ]
search_fields = ['$name', ]
class TransformedFoodViewSet(ReadProtectedModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `TransformedFood` objects, serialize it to JSON with the given serializer,
then render it on /api/food/transformedfood/
"""
queryset = TransformedFood.objects.order_by('id')
serializer_class = TransformedFoodSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['name', ]
search_fields = ['$name', ]
class QRCodeViewSet(ReadProtectedModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `QRCode` objects, serialize it to JSON with the given serializer,
then render it on /api/food/qrcode/
"""
queryset = QRCode.objects.order_by('id')
serializer_class = QRCodeSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['qr_code_number', ]
search_fields = ['$qr_code_number', ]

11
apps/food/apps.py Normal file
View File

@ -0,0 +1,11 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.utils.translation import gettext_lazy as _
from django.apps import AppConfig
class FoodkfetConfig(AppConfig):
name = 'food'
verbose_name = _('food')

View File

@ -0,0 +1,100 @@
[
{
"model": "food.allergen",
"pk": 1,
"fields": {
"name": "Lait"
}
},
{
"model": "food.allergen",
"pk": 2,
"fields": {
"name": "Oeufs"
}
},
{
"model": "food.allergen",
"pk": 3,
"fields": {
"name": "Gluten"
}
},
{
"model": "food.allergen",
"pk": 4,
"fields": {
"name": "Fruits à coques"
}
},
{
"model": "food.allergen",
"pk": 5,
"fields": {
"name": "Arachides"
}
},
{
"model": "food.allergen",
"pk": 6,
"fields": {
"name": "Sésame"
}
},
{
"model": "food.allergen",
"pk": 7,
"fields": {
"name": "Soja"
}
},
{
"model": "food.allergen",
"pk": 8,
"fields": {
"name": "Céléri"
}
},
{
"model": "food.allergen",
"pk": 9,
"fields": {
"name": "Lupin"
}
},
{
"model": "food.allergen",
"pk": 10,
"fields": {
"name": "Moutarde"
}
},
{
"model": "food.allergen",
"pk": 11,
"fields": {
"name": "Sulfites"
}
},
{
"model": "food.allergen",
"pk": 12,
"fields": {
"name": "Crustacés"
}
},
{
"model": "food.allergen",
"pk": 13,
"fields": {
"name": "Mollusques"
}
},
{
"model": "food.allergen",
"pk": 14,
"fields": {
"name": "Poissons"
}
}
]

153
apps/food/forms.py Normal file
View File

@ -0,0 +1,153 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from random import shuffle
from bootstrap_datepicker_plus.widgets import DateTimePickerInput
from django import forms
from django.forms.widgets import NumberInput
from django.utils.translation import gettext_lazy as _
from member.models import Club
from note_kfet.inputs import Autocomplete
from note_kfet.middlewares import get_current_request
from permission.backends import PermissionBackend
from .models import BasicFood, TransformedFood, QRCode
class QRCodeForms(forms.ModelForm):
"""
Form for create QRCode for container
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['food_container'].queryset = self.fields['food_container'].queryset.filter(
is_ready=False,
end_of_life__isnull=True,
polymorphic_ctype__model='transformedfood',
).filter(PermissionBackend.filter_queryset(
get_current_request(),
TransformedFood,
"view",
))
class Meta:
model = QRCode
fields = ('food_container',)
class BasicFoodForms(forms.ModelForm):
"""
Form for add basicfood
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['name'].widget.attrs.update({"autofocus": "autofocus"})
self.fields['name'].required = True
self.fields['owner'].required = True
# Some example
self.fields['name'].widget.attrs.update({"placeholder": _("Pasta METRO 5kg")})
clubs = list(Club.objects.filter(PermissionBackend.filter_queryset(get_current_request(), Club, "change")).all())
shuffle(clubs)
self.fields['owner'].widget.attrs["placeholder"] = ", ".join(club.name for club in clubs[:4]) + ", ..."
self.fields['order'].widget.attrs["placeholder"] = _("Specific order given to GCKs")
class Meta:
model = BasicFood
fields = ('name', 'owner', 'date_type', 'expiry_date', 'allergens', 'order',)
widgets = {
"owner": Autocomplete(
model=Club,
attrs={"api_url": "/api/members/club/"},
),
"expiry_date": DateTimePickerInput(),
}
class TransformedFoodForms(forms.ModelForm):
"""
Form for add transformedfood
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['name'].required = True
self.fields['owner'].required = True
# Some example
self.fields['name'].widget.attrs.update({"placeholder": _("Lasagna")})
clubs = list(Club.objects.filter(PermissionBackend.filter_queryset(get_current_request(), Club, "change")).all())
shuffle(clubs)
self.fields['owner'].widget.attrs["placeholder"] = ", ".join(club.name for club in clubs[:4]) + ", ..."
self.fields['order'].widget.attrs["placeholder"] = _("Specific order given to GCKs")
class Meta:
model = TransformedFood
fields = ('name', 'owner', 'order',)
widgets = {
"owner": Autocomplete(
model=Club,
attrs={"api_url": "/api/members/club/"},
),
}
class BasicFoodUpdateForms(forms.ModelForm):
"""
Form for update basicfood object
"""
class Meta:
model = BasicFood
fields = ('name', 'owner', 'date_type', 'expiry_date', 'end_of_life', 'is_ready', 'order', 'allergens')
widgets = {
"owner": Autocomplete(
model=Club,
attrs={"api_url": "/api/members/club/"},
),
"expiry_date": DateTimePickerInput(),
}
class TransformedFoodUpdateForms(forms.ModelForm):
"""
Form for update transformedfood object
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['shelf_life'].label = _('Shelf life (in hours)')
class Meta:
model = TransformedFood
fields = ('name', 'owner', 'end_of_life', 'is_ready', 'order', 'shelf_life')
widgets = {
"owner": Autocomplete(
model=Club,
attrs={"api_url": "/api/members/club/"},
),
"expiry_date": DateTimePickerInput(),
"shelf_life": NumberInput(),
}
class AddIngredientForms(forms.ModelForm):
"""
Form for add an ingredient
"""
fully_used = forms.BooleanField()
fully_used.initial = True
fully_used.required = False
fully_used.label = _("Fully used")
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# TODO find a better way to get pk (be not url scheme dependant)
pk = get_current_request().path.split('/')[-1]
self.fields['ingredients'].queryset = self.fields['ingredients'].queryset.filter(
polymorphic_ctype__model="transformedfood",
is_ready=False,
end_of_life='',
).filter(PermissionBackend.filter_queryset(get_current_request(), TransformedFood, "change")).exclude(pk=pk)
class Meta:
model = TransformedFood
fields = ('ingredients',)

View File

@ -0,0 +1,199 @@
# Generated by Django 4.2.20 on 2025-04-17 21:43
import datetime
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
initial = True
dependencies = [
("contenttypes", "0002_remove_content_type_name"),
("member", "0013_auto_20240801_1436"),
]
operations = [
migrations.CreateModel(
name="Allergen",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=255, verbose_name="name")),
],
options={
"verbose_name": "Allergen",
"verbose_name_plural": "Allergens",
},
),
migrations.CreateModel(
name="Food",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=255, verbose_name="name")),
("expiry_date", models.DateTimeField(verbose_name="expiry date")),
(
"end_of_life",
models.CharField(max_length=255, verbose_name="end of life"),
),
(
"is_ready",
models.BooleanField(max_length=255, verbose_name="is ready"),
),
("order", models.CharField(max_length=255, verbose_name="order")),
(
"allergens",
models.ManyToManyField(
blank=True, to="food.allergen", verbose_name="allergens"
),
),
(
"owner",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="+",
to="member.club",
verbose_name="owner",
),
),
(
"polymorphic_ctype",
models.ForeignKey(
editable=False,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="polymorphic_%(app_label)s.%(class)s_set+",
to="contenttypes.contenttype",
),
),
],
options={
"verbose_name": "Food",
"verbose_name_plural": "Foods",
},
),
migrations.CreateModel(
name="BasicFood",
fields=[
(
"food_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="food.food",
),
),
(
"arrival_date",
models.DateTimeField(
default=django.utils.timezone.now, verbose_name="arrival date"
),
),
(
"date_type",
models.CharField(
choices=[("DLC", "DLC"), ("DDM", "DDM")], max_length=255
),
),
],
options={
"verbose_name": "Basic food",
"verbose_name_plural": "Basic foods",
},
bases=("food.food",),
),
migrations.CreateModel(
name="QRCode",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"qr_code_number",
models.PositiveIntegerField(
unique=True, verbose_name="qr code number"
),
),
(
"food_container",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="QR_code",
to="food.food",
verbose_name="food container",
),
),
],
options={
"verbose_name": "QR-code",
"verbose_name_plural": "QR-codes",
},
),
migrations.CreateModel(
name="TransformedFood",
fields=[
(
"food_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="food.food",
),
),
(
"creation_date",
models.DateTimeField(
default=django.utils.timezone.now, verbose_name="creation date"
),
),
(
"shelf_life",
models.DurationField(
default=datetime.timedelta(days=3), verbose_name="shelf life"
),
),
(
"ingredients",
models.ManyToManyField(
blank=True,
related_name="transformed_ingredient_inv",
to="food.food",
verbose_name="transformed ingredient",
),
),
],
options={
"verbose_name": "Transformed food",
"verbose_name_plural": "Transformed foods",
},
bases=("food.food",),
),
]

View File

286
apps/food/models.py Normal file
View File

@ -0,0 +1,286 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from datetime import timedelta
from django.db import models, transaction
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from polymorphic.models import PolymorphicModel
from member.models import Club
class Allergen(models.Model):
"""
Allergen and alimentary restrictions
"""
name = models.CharField(
verbose_name=_('name'),
max_length=255,
)
class Meta:
verbose_name = _("Allergen")
verbose_name_plural = _("Allergens")
def __str__(self):
return self.name
class Food(PolymorphicModel):
"""
Describe any type of food
"""
name = models.CharField(
verbose_name=_("name"),
max_length=255,
)
owner = models.ForeignKey(
Club,
on_delete=models.PROTECT,
related_name='+',
verbose_name=_('owner'),
)
allergens = models.ManyToManyField(
Allergen,
blank=True,
verbose_name=_('allergens'),
)
expiry_date = models.DateTimeField(
verbose_name=_('expiry date'),
null=False,
)
end_of_life = models.CharField(
blank=True,
verbose_name=_('end of life'),
max_length=255,
)
is_ready = models.BooleanField(
verbose_name=_('is ready'),
max_length=255,
)
order = models.CharField(
blank=True,
verbose_name=_('order'),
max_length=255,
)
def __str__(self):
return self.name
@transaction.atomic
def update_allergens(self):
# update parents
for parent in self.transformed_ingredient_inv.iterator():
old_allergens = list(parent.allergens.all()).copy()
parent.allergens.clear()
for child in parent.ingredients.iterator():
if child.pk != self.pk:
parent.allergens.set(parent.allergens.union(child.allergens.all()))
parent.allergens.set(parent.allergens.union(self.allergens.all()))
if old_allergens != list(parent.allergens.all()):
parent.save(old_allergens=old_allergens)
def update_expiry_date(self):
# update parents
for parent in self.transformed_ingredient_inv.iterator():
old_expiry_date = parent.expiry_date
parent.expiry_date = parent.shelf_life + parent.creation_date
for child in parent.ingredients.iterator():
if (child.pk != self.pk
and not (child.polymorphic_ctype.model == 'basicfood'
and child.date_type == 'DDM')):
parent.expiry_date = min(parent.expiry_date, child.expiry_date)
if self.polymorphic_ctype.model == 'basicfood' and self.date_type == 'DLC':
parent.expiry_date = min(parent.expiry_date, self.expiry_date)
if old_expiry_date != parent.expiry_date:
parent.save()
class Meta:
verbose_name = _('Food')
verbose_name_plural = _('Foods')
class BasicFood(Food):
"""
A basic food is a food directly buy and stored
"""
arrival_date = models.DateTimeField(
default=timezone.now,
verbose_name=_('arrival date'),
)
date_type = models.CharField(
max_length=255,
choices=(
("DLC", "DLC"),
("DDM", "DDM"),
)
)
@transaction.atomic
def save(self, force_insert=False, force_update=False, using=None, update_fields=None, **kwargs):
created = self.pk is None
if not created:
# Check if important fields are updated
old_food = Food.objects.select_for_update().get(pk=self.pk)
if not hasattr(self, "_force_save"):
# Allergens
if ('old_allergens' in kwargs
and list(self.allergens.all()) != kwargs['old_allergens']):
self.update_allergens()
# Expiry date
if ((self.expiry_date != old_food.expiry_date
and self.date_type == 'DLC')
or old_food.date_type != self.date_type):
self.update_expiry_date()
return super().save(force_insert, force_update, using, update_fields)
@staticmethod
def get_lastests_objects(number, distinct_field, order_by_field):
"""
Get the last object with distinct field and ranked with order_by
This methods exist because we can't distinct with one field and
order with another
"""
foods = BasicFood.objects.order_by(order_by_field).all()
field = []
for food in foods:
if getattr(food, distinct_field) in field:
continue
else:
field.append(getattr(food, distinct_field))
number -= 1
yield food
if not number:
return
class Meta:
verbose_name = _('Basic food')
verbose_name_plural = _('Basic foods')
def __str__(self):
return self.name
class TransformedFood(Food):
"""
A transformed food is a food with ingredients
"""
creation_date = models.DateTimeField(
default=timezone.now,
verbose_name=_('creation date'),
)
# Without microbiological analyzes, the storage time is 3 days
shelf_life = models.DurationField(
default=timedelta(days=3),
verbose_name=_('shelf life'),
)
ingredients = models.ManyToManyField(
Food,
blank=True,
symmetrical=False,
related_name='transformed_ingredient_inv',
verbose_name=_('transformed ingredient'),
)
def check_cycle(self, ingredients, origin, checked):
for ingredient in ingredients:
if ingredient == origin:
# We break the cycle
self.ingredients.remove(ingredient)
if ingredient.polymorphic_ctype.model == 'transformedfood' and ingredient not in checked:
ingredient.check_cycle(ingredient.ingredients.all(), origin, checked)
checked.append(ingredient)
@transaction.atomic
def save(self, force_insert=False, force_update=False, using=None, update_fields=None, **kwargs):
created = self.pk is None
if not created:
# Check if important fields are updated
update = {'allergens': False, 'expiry_date': False}
old_food = Food.objects.select_for_update().get(pk=self.pk)
if not hasattr(self, "_force_save"):
# Allergens
# Unfortunately with the many-to-many relation we can't access
# to old allergens
if ('old_allergens' in kwargs
and list(self.allergens.all()) != kwargs['old_allergens']):
update['allergens'] = True
# Expiry date
update['expiry_date'] = (self.shelf_life != old_food.shelf_life
or self.creation_date != old_food.creation_date)
if update['expiry_date']:
self.expiry_date = self.creation_date + self.shelf_life
# Unfortunately with the set method ingredients are already save,
# we check cycle after if possible
if ('old_ingredients' in kwargs
and list(self.ingredients.all()) != list(kwargs['old_ingredients'])):
update['allergens'] = True
update['expiry_date'] = True
# it's preferable to keep a queryset but we allow list too
if type(kwargs['old_ingredients']) is list:
kwargs['old_ingredients'] = Food.objects.filter(
pk__in=[food.pk for food in kwargs['old_ingredients']])
self.check_cycle(self.ingredients.all().difference(kwargs['old_ingredients']), self, [])
if update['allergens']:
self.update_allergens()
if update['expiry_date']:
self.update_expiry_date()
if created:
self.expiry_date = self.shelf_life + self.creation_date
# We save here because we need pk for many-to-many relation
super().save(force_insert, force_update, using, update_fields)
for child in self.ingredients.iterator():
self.allergens.set(self.allergens.union(child.allergens.all()))
if not (child.polymorphic_ctype.model == 'basicfood' and child.date_type == 'DDM'):
self.expiry_date = min(self.expiry_date, child.expiry_date)
return super().save(force_insert, force_update, using, update_fields)
class Meta:
verbose_name = _('Transformed food')
verbose_name_plural = _('Transformed foods')
def __str__(self):
return self.name
class QRCode(models.Model):
"""
QR-code for register food
"""
qr_code_number = models.PositiveIntegerField(
unique=True,
verbose_name=_('qr code number'),
)
food_container = models.ForeignKey(
Food,
on_delete=models.CASCADE,
related_name='QR_code',
verbose_name=_('food container'),
)
class Meta:
verbose_name = _('QR-code')
verbose_name_plural = _('QR-codes')
def __str__(self):
return _('QR-code number') + ' ' + str(self.qr_code_number)

21
apps/food/tables.py Normal file
View File

@ -0,0 +1,21 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import django_tables2 as tables
from .models import Food
class FoodTable(tables.Table):
"""
List all foods.
"""
class Meta:
model = Food
template_name = 'django_tables2/bootstrap4.html'
fields = ('name', 'owner', 'allergens', 'expiry_date')
row_attrs = {
'class': 'table-row',
'data-href': lambda record: 'detail/' + str(record.pk),
'style': 'cursor:pointer',
}

View File

@ -0,0 +1,48 @@
{% extends "base.html" %}
{% comment %}
Copyright (C) by BDE ENS Paris-Saclay
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load i18n crispy_forms_tags %}
{% block content %}
<div class="card bg-white mb-3">
<h3 class="card-header text-center">
{{ title }} {{ food.name }}
</h3>
<div class="card-body">
<ul>
{% for field, value in fields %}
<li> {{ field }} : {{ value }}</li>
{% endfor %}
{% if meals %}
<li> {% trans "Contained in" %} :
{% for meal in meals %}
<a href="{% url "food:transformedfood_view" pk=meal.pk %}">{{ meal.name }}</a>{% if not forloop.last %},{% endif %}
{% endfor %}
</li>
{% endif %}
{% if foods %}
<li> {% trans "Contain" %} :
{% for food in foods %}
<a href="{% url "food:food_view" pk=food.pk %}">{{ food.name }}</a>{% if not forloop.last %},{% endif %}
{% endfor %}
</li>
{% endif %}
</ul>
{% if update %}
<a class="btn btn-sm btn-secondary" href="{% url "food:food_update" pk=food.pk %}">
{% trans "Update" %}
</a>
{% endif %}
{% if add_ingredient %}
<a class="btn btn-sm btn-primary" href="{% url "food:add_ingredient" pk=food.pk %}">
{% trans "Add to a meal" %}
</a>
{% endif %}
<a class="btn btn-sm btn-primary" href="{% url "food:food_list" %}">
{% trans "Return to the food list" %}
</a>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,71 @@
{% extends "base_search.html" %}
{% comment %}
Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load render_table from django_tables2 %}
{% load i18n %}
{% block content %}
{{ block.super }}
<br>
<div class="card bg-light mb-3">
<h3 class="card-header text-center">
{% trans "Meal served" %}
</h3>
{% if can_add_meal %}
<div class="card-footer">
<a class="btn btn-sm btn-primary" href="{% url 'food:transformedfood_create' %}">
{% trans "New meal" %}
</a>
</div>
{% endif %}
{% if served.data %}
{% render_table served %}
{% else %}
<div class="card-body">
<div class="alert alert-warning">
{% trans "There is no meal served." %}
</div>
</div>
</div>
{% endif %}
<div class="card bg-light mb-3">
<h3 class="card-header text-center">
{% trans "Free food" %}
</h3>
{% if open.data %}
{% render_table open %}
{% else %}
<div class="card-body">
<div class="alert alert-warning">
{% trans "There is no free food." %}
</div>
</div>
{% endif %}
</div>
{% if club_tables %}
<div class="card bg-light mb-3">
<h3 class="card-header text-center">
{% trans "Food of your clubs" %}
</h3>
</div>
{% for table in club_tables %}
<div class="card bg-light mb-3">
<h3 class="card-header text-center">
{% trans "Food of club" %} {{ table.prefix }}
</h3>
{% if table.data %}
{% render_table table %}
{% else %}
<div class="card-body">
<div class="alert alert-warning">
{% trans "Yours club has not food yet." %}
</div>
</div>
{% endif %}
</div>
{% endfor %}
{% endif %}
</div>
{% endblock %}

View File

@ -0,0 +1,21 @@
{% extends "base.html" %}
{% comment %}
Copyright (C) by BDE ENS Paris-Saclay
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load i18n crispy_forms_tags %}
{% block content %}
<div class="card bg-white mb-3">
<h3 class="card-header text-center">
{{ title }}
</h3>
<div class="card-body" id="form">
<form method="post">
{% csrf_token %}
{{ form | crispy }}
<button class="btn btn-primary" type="submit">{% trans "Submit"%}</button>
</form>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,52 @@
{% extends "base.html" %}
{% comment %}
Copyright (C) by BDE ENS Paris-Saclay
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load i18n crispy_forms_tags %}
{% load render_table from django_tables2 %}
{% block content %}
<div class="card bg-white mb-3">
<h3 class="card-header text-center">
{{ title }}
</h3>
<div class="card-body" id="form">
<form method="post">
{% csrf_token %}
{{ form | crispy }}
<button class="btn btn-primary" type="submit">{% trans "Submit"%}</button>
</form>
<div class="card-body">
<h4>
{% trans "Copy constructor" %}
<a class="btn btn-secondary" href="{% url "food:basicfood_create" slug=slug %}">{% trans "New food" %}</a>
</h4>
<table class="table">
<thead>
<tr>
<th class="orderable">
{% trans "Name" %}
</th>
<th class="orderable">
{% trans "Owner" %}
</th>
<th class="orderable">
{% trans "Expiry date" %}
</th>
</tr>
</thead>
<tbody>
{% for food in last_items %}
<tr>
<td><a href="{% url "food:basicfood_create" slug=slug %}?copy={{ food.pk }}">{{ food.name }}</a></td>
<td>{{ food.owner }}</td>
<td>{{ food.expiry_date }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,170 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from api.tests import TestAPI
from django.contrib.auth.models import User
from django.test import TestCase
from django.urls import reverse
from django.utils import timezone
from ..api.views import AllergenViewSet, BasicFoodViewSet, TransformedFoodViewSet, QRCodeViewSet
from ..models import Allergen, BasicFood, TransformedFood, QRCode
class TestFood(TestCase):
"""
Test food
"""
fixtures = ('initial',)
def setUp(self):
self.user = User.objects.create_superuser(
username='admintoto',
password='toto1234',
email='toto@example.com'
)
self.client.force_login(self.user)
sess = self.client.session
sess['permission_mask'] = 42
sess.save()
self.allergen = Allergen.objects.create(
name='allergen',
)
self.basicfood = BasicFood.objects.create(
name='basicfood',
owner_id=1,
expiry_date=timezone.now(),
is_ready=False,
date_type='DLC',
)
self.transformedfood = TransformedFood.objects.create(
name='transformedfood',
owner_id=1,
expiry_date=timezone.now(),
is_ready=False,
)
self.qrcode = QRCode.objects.create(
qr_code_number=1,
food_container=self.basicfood,
)
def test_food_list(self):
"""
Display food list
"""
response = self.client.get(reverse('food:food_list'))
self.assertEqual(response.status_code, 200)
def test_qrcode_create(self):
"""
Display QRCode creation
"""
response = self.client.get(reverse('food:qrcode_create'))
self.assertEqual(response.status_code, 200)
def test_basicfood_create(self):
"""
Display BasicFood creation
"""
response = self.client.get(reverse('food:basicfood_create'))
self.assertEqual(response.status_code, 200)
def test_transformedfood_create(self):
"""
Display TransformedFood creation
"""
response = self.client.get(reverse('food:transformedfood_create'))
self.assertEqual(response.status_code, 200)
def test_food_create(self):
"""
Display Food update
"""
response = self.client.get(reverse('food:food_update'))
self.assertEqual(response.status_code, 200)
def test_food_view(self):
"""
Display Food detail
"""
response = self.client.get(reverse('food:food_view'))
self.assertEqual(response.status_code, 302)
def test_basicfood_view(self):
"""
Display BasicFood detail
"""
response = self.client.get(reverse('food:basicfood_view'))
self.assertEqual(response.status_code, 200)
def test_transformedfood_view(self):
"""
Display TransformedFood detail
"""
response = self.client.get(reverse('food:transformedfood_view'))
self.assertEqual(response.status_code, 200)
def test_add_ingredient(self):
"""
Display add ingredient view
"""
response = self.client.get(reverse('food:add_ingredient'))
self.assertEqual(response.status_code, 200)
class TestFoodAPI(TestAPI):
def setUp(self) -> None:
super().setUP()
self.allergen = Allergen.objects.create(
name='name',
)
self.basicfood = BasicFood.objects.create(
name='basicfood',
owner_id=1,
expiry_date=timezone.now(),
is_ready=False,
date_type='DLC',
)
self.transformedfood = TransformedFood.objects.create(
name='transformedfood',
owner_id=1,
expiry_date=timezone.now(),
is_ready=False,
)
self.qrcode = QRCode.objects.create(
qr_code_number=1,
food_container=self.basicfood,
)
def test_allergen_api(self):
"""
Load Allergen API page and test all filters and permissions
"""
self.check_viewset(AllergenViewSet, '/api/food/allergen/')
def test_basicfood_api(self):
"""
Load BasicFood API page and test all filters and permissions
"""
self.check_viewset(BasicFoodViewSet, '/api/food/basicfood/')
def test_transformedfood_api(self):
"""
Load TransformedFood API page and test all filters and permissions
"""
self.check_viewset(TransformedFoodViewSet, '/api/food/transformedfood/')
def test_qrcode_api(self):
"""
Load QRCode API page and test all filters and permissions
"""
self.check_viewset(QRCodeViewSet, '/api/food/qrcode/')

20
apps/food/urls.py Normal file
View File

@ -0,0 +1,20 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.urls import path
from . import views
app_name = 'food'
urlpatterns = [
path('', views.FoodListView.as_view(), name='food_list'),
path('<int:slug>', views.QRCodeCreateView.as_view(), name='qrcode_create'),
path('<int:slug>/add/basic', views.BasicFoodCreateView.as_view(), name='basicfood_create'),
path('add/transformed', views.TransformedFoodCreateView.as_view(), name='transformedfood_create'),
path('update/<int:pk>', views.FoodUpdateView.as_view(), name='food_update'),
path('detail/<int:pk>', views.FoodDetailView.as_view(), name='food_view'),
path('detail/basic/<int:pk>', views.BasicFoodDetailView.as_view(), name='basicfood_view'),
path('detail/transformed/<int:pk>', views.TransformedFoodDetailView.as_view(), name='transformedfood_view'),
path('add/ingredient/<int:pk>', views.AddIngredientView.as_view(), name='add_ingredient'),
]

53
apps/food/utils.py Normal file
View File

@ -0,0 +1,53 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.utils.translation import gettext_lazy as _
seconds = (_('second'), _('seconds'))
minutes = (_('minute'), _('minutes'))
hours = (_('hour'), _('hours'))
days = (_('day'), _('days'))
weeks = (_('week'), _('weeks'))
def plural(x):
if x == 1:
return 0
return 1
def pretty_duration(duration):
"""
I receive datetime.timedelta object
You receive string object
"""
text = []
sec = duration.seconds
d = duration.days
if d >= 7:
w = d // 7
text.append(str(w) + ' ' + weeks[plural(w)])
d -= w * 7
if d > 0:
text.append(str(d) + ' ' + days[plural(d)])
if sec >= 3600:
h = sec // 3600
text.append(str(h) + ' ' + hours[plural(h)])
sec -= h * 3600
if sec >= 60:
m = sec // 60
text.append(str(m) + ' ' + minutes[plural(m)])
sec -= m * 60
if sec > 0:
text.append(str(sec) + ' ' + seconds[plural(sec)])
if len(text) == 0:
return ''
if len(text) == 1:
return text[0]
if len(text) >= 2:
return ', '.join(t for t in text[:-1]) + ' ' + _('and') + ' ' + text[-1]

408
apps/food/views.py Normal file
View File

@ -0,0 +1,408 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from datetime import timedelta
from api.viewsets import is_regex
from django_tables2.views import MultiTableMixin
from django.db import transaction
from django.db.models import Q
from django.http import HttpResponseRedirect
from django.views.generic import DetailView, UpdateView
from django.views.generic.list import ListView
from django.urls import reverse_lazy
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from member.models import Club, Membership
from permission.backends import PermissionBackend
from permission.views import ProtectQuerysetMixin, ProtectedCreateView, LoginRequiredMixin
from .models import Food, BasicFood, TransformedFood, QRCode
from .forms import AddIngredientForms, BasicFoodForms, TransformedFoodForms, BasicFoodUpdateForms, TransformedFoodUpdateForms, QRCodeForms
from .tables import FoodTable
from .utils import pretty_duration
class FoodListView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, ListView):
"""
Display Food
"""
model = Food
tables = [FoodTable, FoodTable, FoodTable, ]
extra_context = {"title": _('Food')}
template_name = 'food/food_list.html'
def get_queryset(self, **kwargs):
return super().get_queryset(**kwargs).distinct()
def get_tables(self):
bureau_role_pk = 4
clubs = Club.objects.filter(membership__in=Membership.objects.filter(
user=self.request.user, roles=bureau_role_pk).filter(
date_end__gte=timezone.now()))
tables = [FoodTable] * (clubs.count() + 3)
self.tables = tables
tables = super().get_tables()
tables[0].prefix = 'search-'
tables[1].prefix = 'open-'
tables[2].prefix = 'served-'
for i in range(clubs.count()):
tables[i + 3].prefix = clubs[i].name
return tables
def get_tables_data(self):
# table search
qs = self.get_queryset().order_by('name')
if "search" in self.request.GET and self.request.GET['search']:
pattern = self.request.GET['search']
# check regex
valid_regex = is_regex(pattern)
suffix = '__iregex' if valid_regex else '__istartswith'
prefix = '^' if valid_regex else ''
qs = qs.filter(Q(**{f'name{suffix}': prefix + pattern}))
else:
qs = qs.none()
search_table = qs.filter(PermissionBackend.filter_queryset(self.request, Food, 'view'))
# table open
open_table = self.get_queryset().order_by('expiry_date').filter(
Q(polymorphic_ctype__model='transformedfood')
| Q(polymorphic_ctype__model='basicfood', basicfood__date_type='DLC')).filter(
expiry_date__lt=timezone.now()).filter(
PermissionBackend.filter_queryset(self.request, Food, 'view'))
# table served
served_table = self.get_queryset().order_by('-pk').filter(
end_of_life='', is_ready=True).filter(
Q(polymorphic_ctype__model='basicfood',
basicfood__date_type='DLC',
expiry_date__lte=timezone.now(),)
| Q(polymorphic_ctype__model='transformedfood',
expiry_date__lte=timezone.now(),
))
# tables club
bureau_role_pk = 4
clubs = Club.objects.filter(membership__in=Membership.objects.filter(
user=self.request.user, roles=bureau_role_pk).filter(
date_end__gte=timezone.now()))
club_table = []
for club in clubs:
club_table.append(self.get_queryset().order_by('expiry_date').filter(
owner=club, end_of_life='').filter(
PermissionBackend.filter_queryset(self.request, Food, 'view')
))
return [search_table, open_table, served_table] + club_table
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
tables = context['tables']
# for extends base_search.html we need to name 'search_table' in 'table'
for name, table in zip(['table', 'open', 'served'], tables):
context[name] = table
context['club_tables'] = tables[3:]
context['can_add_meal'] = PermissionBackend.check_perm(self.request, 'food.transformedfood_add')
return context
class QRCodeCreateView(ProtectQuerysetMixin, ProtectedCreateView):
"""
A view to add qrcode
"""
model = QRCode
template_name = 'food/qrcode.html'
form_class = QRCodeForms
extra_context = {"title": _("Add a new QRCode")}
def get(self, *args, **kwargs):
qrcode = kwargs["slug"]
if self.model.objects.filter(qr_code_number=qrcode).count() > 0:
pk = self.model.objects.get(qr_code_number=qrcode).food_container.pk
return HttpResponseRedirect(reverse_lazy("food:food_view", kwargs={"pk": pk}))
else:
return super().get(*args, **kwargs)
@transaction.atomic
def form_valid(self, form):
qrcode_food_form = QRCodeForms(data=self.request.POST)
if not qrcode_food_form.is_valid():
return self.form_invalid(form)
qrcode = form.save(commit=False)
qrcode.qr_code_number = self.kwargs['slug']
qrcode._force_save = True
qrcode.save()
qrcode.refresh_from_db()
return super().form_valid(form)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['slug'] = self.kwargs['slug']
# get last 10 BasicFood objects with distincts 'name' ordered by '-pk'
# we can't use .distinct and .order_by with differents columns hence the generator
context['last_items'] = [food for food in BasicFood.get_lastests_objects(10, 'name', '-pk')]
return context
def get_success_url(self, **kwargs):
self.object.refresh_from_db()
return reverse_lazy('food:food_view', kwargs={'pk': self.object.food_container.pk})
def get_sample_object(self):
return QRCode(
qr_code_number=self.kwargs['slug'],
food_container_id=1,
)
class BasicFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView):
"""
A view to add basicfood
"""
model = BasicFood
form_class = BasicFoodForms
extra_context = {"title": _("Add an aliment")}
template_name = "food/food_update.html"
def get_sample_object(self):
return BasicFood(
name="",
owner_id=1,
expiry_date=timezone.now(),
is_ready=True,
arrival_date=timezone.now(),
date_type='DLC',
)
@transaction.atomic
def form_valid(self, form):
if QRCode.objects.filter(qr_code_number=self.kwargs['slug']).count() > 0:
return HttpResponseRedirect(reverse_lazy('food:qrcode_create', kwargs={'slug': self.kwargs['slug']}))
food_form = BasicFoodForms(data=self.request.POST)
if not food_form.is_valid():
return self.form_invalid(form)
food = form.save(commit=False)
food.is_ready = False
food.save()
food.refresh_from_db()
qrcode = QRCode()
qrcode.qr_code_number = self.kwargs['slug']
qrcode.food_container = food
qrcode.save()
return super().form_valid(form)
def get_success_url(self, **kwargs):
self.object.refresh_from_db()
return reverse_lazy('food:basicfood_view', kwargs={"pk": self.object.pk})
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
copy = self.request.GET.get('copy', None)
if copy is not None:
food = BasicFood.objects.get(pk=copy)
print(context['form'].fields)
for field in context['form'].fields:
if field == 'allergens':
context['form'].fields[field].initial = getattr(food, field).all()
else:
context['form'].fields[field].initial = getattr(food, field)
return context
class TransformedFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView):
"""
A view to add transformedfood
"""
model = TransformedFood
form_class = TransformedFoodForms
extra_context = {"title": _("Add a meal")}
template_name = "food/food_update.html"
def get_sample_object(self):
return TransformedFood(
name="",
owner_id=1,
expiry_date=timezone.now(),
is_ready=True,
)
@transaction.atomic
def form_valid(self, form):
form.instance.expiry_date = timezone.now() + timedelta(days=3)
form.instance.is_ready = False
return super().form_valid(form)
def get_success_url(self, **kwargs):
self.object.refresh_from_db()
return reverse_lazy('food:transformedfood_view', kwargs={"pk": self.object.pk})
class AddIngredientView(ProtectQuerysetMixin, UpdateView):
"""
A view to add ingredient to a meal
"""
model = Food
extra_context = {"title": _("Add the ingredient:")}
form_class = AddIngredientForms
template_name = 'food/food_update.html'
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
context['title'] += ' ' + self.object.name
return context
@transaction.atomic
def form_valid(self, form):
meals = TransformedFood.objects.filter(pk__in=form.data.getlist('ingredients')).all()
for meal in meals:
old_ingredients = list(meal.ingredients.all()).copy()
old_allergens = list(meal.allergens.all()).copy()
meal.ingredients.add(self.object.pk)
# update allergen and expiry date if necessary
if not (self.object.polymorphic_ctype.model == 'basicfood'
and self.object.date_type == 'DDM'):
meal.expiry_date = min(meal.expiry_date, self.object.expiry_date)
meal.allergens.set(meal.allergens.union(self.object.allergens.all()))
meal.save(old_ingredients=old_ingredients, old_allergens=old_allergens)
if 'fully_used' in form.data:
if not self.object.end_of_life:
self.object.end_of_life = _(f'Food fully used in : {meal.name}')
else:
self.object.end_of_life += ', ' + meal.name
if 'fully_used' in form.data:
self.object.is_ready = False
self.object.save()
# We redirect only the first parent
parent_pk = meals[0].pk
return HttpResponseRedirect(self.get_success_url(parent_pk=parent_pk))
def get_success_url(self, **kwargs):
return reverse_lazy('food:transformedfood_view', kwargs={"pk": kwargs['parent_pk']})
class FoodUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
"""
A view to update Food
"""
model = Food
extra_context = {"title": _("Update an aliment")}
template_name = 'food/food_update.html'
@transaction.atomic
def form_valid(self, form):
form.instance.creater = self.request.user
food = Food.objects.get(pk=self.kwargs['pk'])
old_allergens = list(food.allergens.all()).copy()
if food.polymorphic_ctype.model == 'transformedfood':
old_ingredients = food.ingredients.all()
form.instance.shelf_life = timedelta(
seconds=int(form.data['shelf_life']) * 60 * 60)
food_form = self.get_form_class()(data=self.request.POST)
if not food_form.is_valid():
return self.form_invalid(form)
ans = super().form_valid(form)
if food.polymorphic_ctype.model == 'transformedfood':
form.instance.save(old_ingredients=old_ingredients)
else:
form.instance.save(old_allergens=old_allergens)
return ans
def get_form_class(self, **kwargs):
food = Food.objects.get(pk=self.kwargs['pk'])
if food.polymorphic_ctype.model == 'basicfood':
return BasicFoodUpdateForms
else:
return TransformedFoodUpdateForms
def get_form(self, **kwargs):
form = super().get_form(**kwargs)
if 'shelf_life' in form.initial:
hours = form.initial['shelf_life'].days * 24 + form.initial['shelf_life'].seconds // 3600
form.initial['shelf_life'] = hours
return form
def get_success_url(self, **kwargs):
self.object.refresh_from_db()
return reverse_lazy('food:food_view', kwargs={"pk": self.object.pk})
class FoodDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
"""
A view to see a food
"""
model = Food
extra_context = {"title": _('Details of:')}
context_object_name = "food"
template_name = "food/food_detail.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
fields = ["name", "owner", "expiry_date", "allergens", "is_ready", "end_of_life", "order"]
fields = dict([(field, getattr(self.object, field)) for field in fields])
if fields["is_ready"]:
fields["is_ready"] = _("Yes")
else:
fields["is_ready"] = _("No")
fields["allergens"] = ", ".join(
allergen.name for allergen in fields["allergens"].all())
context["fields"] = [(
Food._meta.get_field(field).verbose_name.capitalize(),
value) for field, value in fields.items()]
context["meals"] = self.object.transformed_ingredient_inv.all()
context["update"] = PermissionBackend.check_perm(self.request, "food.change_food")
context["add_ingredient"] = (self.object.end_of_life == '' and PermissionBackend.check_perm(self.request, "food.change_transformedfood"))
return context
def get(self, *args, **kwargs):
model = Food.objects.get(pk=kwargs['pk']).polymorphic_ctype.model
if 'stop_redirect' in kwargs and kwargs['stop_redirect']:
return super().get(*args, **kwargs)
kwargs = {'pk': kwargs['pk']}
if model == 'basicfood':
return HttpResponseRedirect(reverse_lazy("food:basicfood_view", kwargs=kwargs))
return HttpResponseRedirect(reverse_lazy("food:transformedfood_view", kwargs=kwargs))
class BasicFoodDetailView(FoodDetailView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
fields = ['arrival_date', 'date_type']
for field in fields:
context["fields"].append((
BasicFood._meta.get_field(field).verbose_name.capitalize(),
getattr(self.object, field)
))
return context
def get(self, *args, **kwargs):
kwargs['stop_redirect'] = (Food.objects.get(pk=kwargs['pk']).polymorphic_ctype.model == 'basicfood')
return super().get(*args, **kwargs)
class TransformedFoodDetailView(FoodDetailView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["fields"].append((
TransformedFood._meta.get_field("creation_date").verbose_name.capitalize(),
self.object.creation_date
))
context["fields"].append((
TransformedFood._meta.get_field("shelf_life").verbose_name.capitalize(),
pretty_duration(self.object.shelf_life)
))
context["foods"] = self.object.ingredients.all()
return context
def get(self, *args, **kwargs):
kwargs['stop_redirect'] = (Food.objects.get(pk=kwargs['pk']).polymorphic_ctype.model == 'transformedfood')
return super().get(*args, **kwargs)

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
default_app_config = 'logs.apps.LogsConfig'

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from rest_framework import serializers

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from .views import ChangelogViewSet

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django_filters.rest_framework import DjangoFilterBackend
@ -15,7 +15,7 @@ class ChangelogViewSet(ReadOnlyProtectedModelViewSet):
The djangorestframework plugin will get all `Changelog` objects, serialize it to JSON with the given serializer,
then render it on /api/logs/
"""
queryset = Changelog.objects.all()
queryset = Changelog.objects.order_by('id')
serializer_class = ChangelogSerializer
filter_backends = [DjangoFilterBackend, OrderingFilter]
filterset_fields = ['model', 'action', "instance_pk", 'user', 'ip', ]

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.apps import AppConfig

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.conf import settings
@ -76,9 +76,6 @@ class Changelog(models.Model):
verbose_name=_('timestamp'),
)
def delete(self, using=None, keep_parents=False):
raise ValidationError(_("Logs cannot be destroyed."))
class Meta:
verbose_name = _("changelog")
verbose_name_plural = _("changelogs")
@ -86,3 +83,6 @@ class Changelog(models.Model):
def __str__(self):
return _("Changelog of type \"{action}\" for model {model} at {timestamp}").format(
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."))

View File

@ -1,11 +1,11 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib.contenttypes.models import ContentType
from rest_framework.renderers import JSONRenderer
from rest_framework.serializers import ModelSerializer
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
@ -56,13 +56,13 @@ def save_object(sender, instance, **kwargs):
# noinspection PyProtectedMember
previous = instance._previous
# Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP
user, ip = get_current_authenticated_user(), get_current_ip()
# Si un⋅e utilisateur⋅rice est connecté⋅e, on récupère l'utilisateur⋅rice courant⋅e ainsi que son adresse IP
request = get_current_request()
if 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`
# On récupère alors l'utilisateur·trice connecté·e à la VM, et on récupère la note associée
# IMPORTANT : l'utilisateur dans la VM doit être un des alias note du respo info
# IMPORTANT : l'utilisateur⋅rice dans la VM doit être un des alias note du respo info
ip = "127.0.0.1"
username = Alias.normalize(getpass.getuser())
note = NoteUser.objects.filter(alias__normalized_name=username)
@ -71,9 +71,23 @@ def save_object(sender, instance, **kwargs):
# else:
if note.exists():
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
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
if instance.last_login != previous.last_login:
return
@ -120,13 +134,13 @@ def delete_object(sender, instance, **kwargs):
if instance._meta.label_lower in EXCLUDED or hasattr(instance, "_no_signal"):
return
# Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP
user, ip = get_current_authenticated_user(), get_current_ip()
# Si un⋅e utilisateur⋅rice est connecté⋅e, on récupère l'utilisateur⋅rice courant⋅e ainsi que son adresse IP
request = get_current_request()
if 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`
# On récupère alors l'utilisateur·trice connecté·e à la VM, et on récupère la note associée
# IMPORTANT : l'utilisateur dans la VM doit être un des alias note du respo info
# IMPORTANT : l'utilisateur⋅rice dans la VM doit être un des alias note du respo info
ip = "127.0.0.1"
username = Alias.normalize(getpass.getuser())
note = NoteUser.objects.filter(alias__normalized_name=username)
@ -135,6 +149,20 @@ def delete_object(sender, instance, **kwargs):
# else:
if note.exists():
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
class CustomSerializer(ModelSerializer):

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
default_app_config = 'member.apps.MemberConfig'

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib import admin

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from rest_framework import serializers

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from .views import ProfileViewSet, ClubViewSet, MembershipViewSet

View File

@ -1,7 +1,9 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from rest_framework.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 .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,
then render it on /api/members/profile/
"""
queryset = Profile.objects.all()
queryset = Profile.objects.order_by('id')
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):
@ -24,10 +33,13 @@ class ClubViewSet(ReadProtectedModelViewSet):
The djangorestframework plugin will get all `Club` objects, serialize it to JSON with the given serializer,
then render it on /api/members/club/
"""
queryset = Club.objects.all()
queryset = Club.objects.order_by('id')
serializer_class = ClubSerializer
filter_backends = [SearchFilter]
search_fields = ['$name', ]
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
filterset_fields = ['name', 'email', 'note__alias__name', 'note__alias__normalized_name', 'parent_club',
'parent_club__name', 'require_memberships', 'membership_fee_paid', 'membership_fee_unpaid',
'membership_duration', 'membership_start', 'membership_end', ]
search_fields = ['$name', '$email', '$note__alias__name', '$note__alias__normalized_name', ]
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,
then render it on /api/members/membership/
"""
queryset = Membership.objects.all()
queryset = Membership.objects.order_by('id')
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', ]

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.apps import AppConfig

17
apps/member/auth.py Normal file
View File

@ -0,0 +1,17 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from cas_server.auth import DjangoAuthUser # pragma: no cover
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

View File

@ -1,9 +1,9 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import io
from PIL import Image, ImageSequence
from bootstrap_datepicker_plus.widgets import DatePickerInput
from django import forms
from django.conf import settings
from django.contrib.auth.forms import AuthenticationForm
@ -13,8 +13,9 @@ from django.forms import CheckboxSelectMultiple
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from note.models import NoteSpecial, Alias
from note_kfet.inputs import Autocomplete, AmountInput, DatePickerInput
from note_kfet.inputs import Autocomplete, AmountInput
from permission.models import PermissionMask, Role
from PIL import Image, ImageSequence
from .models import Profile, Club, Membership
@ -22,7 +23,7 @@ from .models import Profile, Club, Membership
class CustomAuthenticationForm(AuthenticationForm):
permission_mask = forms.ModelChoiceField(
label=_("Permission mask"),
queryset=PermissionMask.objects.order_by("rank"),
queryset=PermissionMask.objects.order_by("-rank"),
empty_label=None,
)
@ -32,7 +33,7 @@ class UserForm(forms.ModelForm):
# Django usernames can only contain letters, numbers, @, ., +, - and _.
# We want to allow users to have uncommon and unpractical usernames:
# That is their problem, and we have normalized aliases for us.
return super()._get_validation_exclusions() + ["username"]
return super()._get_validation_exclusions() | {"username"}
class Meta:
model = User
@ -43,10 +44,18 @@ class ProfileForm(forms.ModelForm):
"""
A form for the extras field provided by the :model:`member.Profile` model.
"""
# Remove widget=forms.HiddenInput() if you want to use report frequency.
report_frequency = forms.IntegerField(required=False, initial=0, label=_("Report frequency"))
last_report = forms.DateTimeField(required=False, disabled=True, label=_("Last report date"))
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):
promotion = self.cleaned_data["promotion"]
if promotion > timezone.now().year:
@ -68,7 +77,8 @@ class ProfileForm(forms.ModelForm):
class Meta:
model = Profile
fields = '__all__'
exclude = ('user', 'email_confirmed', 'registration_valid', )
# Remove ml_[asso]_registration from exclude if the concerned association uses nk20 to manage its mailing list.
exclude = ('user', 'email_confirmed', 'registration_valid', 'ml_sport_registration', )
class ImageForm(forms.Form):
@ -114,7 +124,7 @@ class ImageForm(forms.Form):
frame = frame.crop((x, y, x + w, y + h))
frame = frame.resize(
(settings.PIC_WIDTH, settings.PIC_RATIO * settings.PIC_WIDTH),
Image.ANTIALIAS,
Image.LANCZOS,
)
frames.append(frame)
@ -131,6 +141,9 @@ class ImageForm(forms.Form):
return cleaned_data
def is_valid(self):
return super().is_valid() or super().clean().get('image') is None
class ClubForm(forms.ModelForm):
def clean(self):
@ -144,12 +157,13 @@ class ClubForm(forms.ModelForm):
class Meta:
model = Club
fields = '__all__'
exclude = ("add_registration_form",)
widgets = {
"membership_fee_paid": AmountInput(),
"membership_fee_unpaid": AmountInput(),
"parent_club": Autocomplete(
Club,
resetable=True,
attrs={
'api_url': '/api/members/club/',
}
@ -199,9 +213,9 @@ class MembershipForm(forms.ModelForm):
class Meta:
model = Membership
fields = ('user', 'date_start')
# Le champ d'utilisateur est remplacé par un champ d'auto-complétion.
# Le champ d'utilisateur⋅rice est remplacé par un champ d'auto-complétion.
# Quand des lettres sont tapées, une requête est envoyée sur l'API d'auto-complétion
# et récupère les noms d'utilisateur valides
# et récupère les noms d'utilisateur⋅rices valides
widgets = {
'user':
Autocomplete(

View File

@ -1,12 +1,14 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import hashlib
from collections import OrderedDict
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 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):
@ -24,16 +26,22 @@ class CustomNK15Hasher(PBKDF2PasswordHasher):
def must_update(self, encoded):
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:
return False
return True
def verify(self, password, encoded):
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\
and get_current_session().get("permission_mask", -1) >= 42:
and request.session.get("permission_mask", -1) >= 42:
return True
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 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):
"""
@ -51,8 +71,11 @@ class DebugSuperuserBackdoor(PBKDF2PasswordHasher):
def verify(self, password, encoded):
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\
and get_current_session().get("permission_mask", -1) >= 42:
and request.session.get("permission_mask", -1) >= 42:
return True
return super().verify(password, encoded)

View File

@ -7,6 +7,7 @@ def create_bde_and_kfet(apps, schema_editor):
"""
Club = apps.get_model("member", "club")
NoteClub = apps.get_model("note", "noteclub")
Alias = apps.get_model("note", "alias")
ContentType = apps.get_model('contenttypes', 'ContentType')
polymorphic_ctype_id = ContentType.objects.get_for_model(NoteClub).id
@ -18,8 +19,8 @@ def create_bde_and_kfet(apps, schema_editor):
membership_fee_paid=500,
membership_fee_unpaid=500,
membership_duration=396,
membership_start="2020-08-01",
membership_end="2021-09-30",
membership_start="2021-08-01",
membership_end="2022-09-30",
)
Club.objects.get_or_create(
id=2,
@ -30,8 +31,8 @@ def create_bde_and_kfet(apps, schema_editor):
membership_fee_paid=3500,
membership_fee_unpaid=3500,
membership_duration=396,
membership_start="2020-08-01",
membership_end="2021-09-30",
membership_start="2021-08-01",
membership_end="2022-09-30",
)
NoteClub.objects.get_or_create(
@ -45,6 +46,19 @@ def create_bde_and_kfet(apps, schema_editor):
polymorphic_ctype_id=polymorphic_ctype_id,
)
Alias.objects.get_or_create(
id=5,
note_id=5,
name="BDE",
normalized_name="bde",
)
Alias.objects.get_or_create(
id=6,
note_id=6,
name="Kfet",
normalized_name="kfet",
)
class Migration(migrations.Migration):
dependencies = [

View File

@ -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),
]

View 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'),
),
]

View 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'),
),
]

View 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'),
),
]

View 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'),
),
]

View 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'),
),
]

View 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'),
),
]

View 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'),
),
]

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import datetime
@ -28,7 +28,6 @@ class Profile(models.Model):
We do not want to patch the Django Contrib :model:`auth.User`model;
so this model add an user profile with additional information.
"""
user = models.OneToOneField(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
@ -57,7 +56,7 @@ class Profile(models.Model):
('A1', _("Mathematics (A1)")),
('A2', _("Physics (A2)")),
("A'2", _("Applied physics (A'2)")),
('A''2', _("Chemistry (A''2)")),
("A''2", _("Chemistry (A''2)")),
('A3', _("Biology (A3)")),
('B1234', _("SAPHIRE (B1234)")),
('B1', _("Mechanics (B1)")),
@ -74,7 +73,7 @@ class Profile(models.Model):
promotion = models.PositiveSmallIntegerField(
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"),
help_text=_("Year of entry to the school (None if not ENS student)"),
)
@ -134,6 +133,22 @@ class Profile(models.Model):
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
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 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):
subject = "[Note Kfet] " + str(_("Activate your Note Kfet account"))
token = email_validation_token.make_token(self.user)
@ -200,9 +204,11 @@ class Club(models.Model):
max_length=255,
unique=True,
)
email = models.EmailField(
verbose_name=_('email'),
)
parent_club = models.ForeignKey(
'self',
null=True,
@ -253,23 +259,17 @@ class Club(models.Model):
help_text=_('Maximal date of a membership, after which members must renew it.'),
)
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:
return
add_registration_form = models.BooleanField(
verbose_name=_("add to registration form"),
default=False,
)
today = datetime.date.today()
class Meta:
verbose_name = _("club")
verbose_name_plural = _("clubs")
if (today - self.membership_start).days >= 365:
self.membership_start = datetime.date(self.membership_start.year + 1,
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)
def __str__(self):
return self.name
@transaction.atomic
def save(self, force_insert=False, force_update=False, using=None,
@ -282,16 +282,36 @@ class Club(models.Model):
self.membership_end = None
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):
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):
"""
@ -313,6 +333,7 @@ class Membership(models.Model):
roles = models.ManyToManyField(
"permission.Role",
related_name="memberships",
verbose_name=_("roles"),
)
@ -330,6 +351,66 @@ class Membership(models.Model):
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
def valid(self):
"""
@ -399,60 +480,14 @@ class Membership(models.Model):
if self.club.parent_club.name == "BDE":
parent_membership.roles.set(
Role.objects.filter(Q(name="Adhérent BDE") | Q(name="Membre de club")).all())
Role.objects.filter(Q(name="Adhérent⋅e BDE") | Q(name="Membre de club")).all())
elif self.club.parent_club.name == "Kfet":
parent_membership.roles.set(
Role.objects.filter(Q(name="Adhérent Kfet") | Q(name="Membre de club")).all())
Role.objects.filter(Q(name="Adhérent⋅e Kfet") | Q(name="Membre de club")).all())
else:
parent_membership.roles.set(Role.objects.filter(name="Membre de club").all())
parent_membership.save()
@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):
"""
Create Membership transaction associated to this membership.
@ -490,11 +525,3 @@ class Membership(models.Model):
soge_credit.save()
else:
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'])]

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later

View File

@ -14,7 +14,7 @@ function create_alias (e) {
}).done(function () {
// Reload table
$('#alias_table').load(location.pathname + ' #alias_table')
addMsg('Alias ajouté', 'success')
addMsg(gettext('Alias successfully added'), 'success')
}).fail(function (xhr, _textStatus, _error) {
errMsg(xhr.responseJSON)
})
@ -22,7 +22,7 @@ function create_alias (e) {
/**
* 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) {
$.ajax({
@ -30,7 +30,7 @@ function delete_button (button_id) {
method: 'DELETE',
headers: { 'X-CSRFTOKEN': CSRF_TOKEN }
}).done(function () {
addMsg('Alias supprimé', 'success')
addMsg(gettext('Alias successfully deleted'), 'success')
$('#alias_table').load(location.pathname + ' #alias_table')
}).fail(function (xhr, _textStatus, _error) {
errMsg(xhr.responseJSON)

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