1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2025-06-21 18:08:21 +02:00

Compare commits

...

1472 Commits

Author SHA1 Message Date
9dc11aa016 Merge branch 'main' into potvieux 2025-05-22 12:01:40 +02:00
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
a90f45bd8b Replace Diolistos.png 2025-04-15 17:38:45 +02:00
10c22ccc53 Replace Diolistos_bg.jpg 2025-04-15 17:38:26 +02:00
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
ddeada200b Changement logo factures 2025-04-15 17:26:14 +02:00
8e2b24b2da Merge branch 'options_order' into 'main'
Options order

See merge request bde/nk20!306
2025-04-13 23:12:05 +02:00
bd76c280ec Update forms.py 2025-04-13 22:59:04 +02:00
ca0a95ba9e Update transaction_form.html 2025-04-13 22:32:49 +02:00
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
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
84e9fea15f linters 2025-04-04 14:46:43 +02:00
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
b9ebb1718a Fixed some non timezone-aware displays 2025-04-04 00:29:22 +02:00
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
702ddb5679 add school field to guest 2025-03-25 17:39:31 +01:00
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
60355196ce Merge branch 'openid-connect' into 'main'
Openid connect

See merge request bde/nk20!293
2025-03-20 18:42:51 +01:00
9bffb32a5e documentation 2025-03-20 17:36:38 +01:00
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
8da62e62fb Rewrite script and add test 2025-03-18 15:53:02 +01:00
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
7966d6f397 Update file initial.json 2025-03-17 13:15:07 +01:00
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
25bfa575ed Another tables and doc 2025-03-14 00:31:25 +01:00
e21d9fcfbe Merge branch 'notekfet_wrapped' into 'main'
Notekfet wrapped

See merge request bde/nk20!298
2025-03-14 00:11:04 +01:00
b293904525 Another tables and doc 2025-03-13 23:56:10 +01:00
bd7e6b8ad4 add table, add some translation 2025-03-13 21:08:52 +01:00
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
4799b2c52d Resize and compress image, add shiny button 2025-03-12 23:42:37 +01:00
562dcfb908 Update file initial.json 2025-03-11 19:34:56 +01:00
12ef258ff0 Update file initial.json 2025-03-11 19:27:02 +01:00
2ae32ee3b6 Update file initial.json 2025-03-11 19:26:49 +01:00
ec1bd45481 Update file initial.json 2025-03-11 19:14:09 +01:00
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
7f0a3784e9 Rave Part[list] colors 2025-03-11 10:15:11 +01:00
36f4adf2e7 Merge branch 'update_copyright' into 'main'
update copyright

See merge request bde/nk20!292
2025-03-09 18:35:44 +01:00
ae7d5d5489 update copyright 2025-03-09 18:14:58 +01:00
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
a0ebf8658d nerf invalidate perm 2025-03-09 17:34:14 +01:00
423454ba5d nerf PC Kfet perms 2025-03-09 14:37:35 +01:00
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
5fb12a1388 improve permissions for GC anti-VSS 2025-03-09 13:11:46 +01:00
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
767e98c2a3 Update file initial.json 2025-03-08 22:05:22 +01:00
1bdad76fe9 Update file initial.json 2025-03-08 22:00:46 +01:00
0196db7fff Update file initial.json 2025-03-08 21:54:28 +01:00
1f53ad4407 Update file initial.json 2025-03-08 21:47:21 +01:00
018f6e3f13 Update file initial.json 2025-03-08 21:37:53 +01:00
9752a030d9 Update file initial.json 2025-03-08 21:30:25 +01:00
b27bdb090d Update file initial.json 2025-03-08 21:30:16 +01:00
55a0fbb6cb Update file initial.json 2025-03-08 21:15:56 +01:00
c356534309 Update file initial.json 2025-03-08 20:25:33 +01:00
51315a0555 Update file initial.json 2025-03-08 20:25:16 +01:00
e5f9fe2cf5 Update file initial.json 2025-03-08 20:00:20 +01:00
6c63c6417c Typesetting 2025-03-08 16:08:40 +01:00
4563b2b640 Added configusation for OpenID support, along with installation information 2025-03-08 16:04:25 +01:00
c630a3fbd5 Merge branch 'change_template' into 'main'
Change template

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

See merge request bde/nk20!287
2025-03-05 22:03:51 +01:00
bd9773a8af change icon 2025-03-05 13:28:55 +01:00
cdeb76d9f8 Merge branch 'main' into notekfet_wrapped 2025-03-04 19:08:32 +01:00
ac4574200d Modify font 2025-03-04 18:45:22 +01:00
b17d31e8ee translation 2025-02-25 14:11:53 +01:00
30d27459dd modify tox.ini to use complex script for make wrapped (bypass C901 in linters) 2025-02-25 01:52:13 +01:00
333f7aa284 update font and minor change 2025-02-24 18:37:18 +01:00
587314e03c linters 2025-02-24 16:10:58 +01:00
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
88b1a25ca0 Update file initial.json 2025-02-18 21:26:55 +01:00
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
041a8f20a9 A permission was missing 2025-02-17 14:28:00 +01:00
b1ffb28532 Update file initial.json 2025-02-17 14:19:00 +01:00
6225fb51f1 Add some permissions 2025-02-17 14:10:21 +01:00
1dd74e8024 Merge branch 'openers' into 'main'
Patch Openers

See merge request bde/nk20!284
2025-02-17 02:13:47 +01:00
1af9f5f23c some updates 2025-02-17 02:12:44 +01:00
83d5a7ceff Update file initial.json 2025-02-17 01:58:13 +01:00
a7cba0a4a3 Update file initial.json 2025-02-16 23:33:18 +01:00
ccd9a66ab9 Update file initial.json 2025-02-16 23:24:39 +01:00
c7a92fa4b2 Update file initial.json 2025-02-16 20:49:11 +01:00
5f1b698d58 Finish script, finish view, make some progress on template 2025-02-16 18:10:53 +01:00
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
26b351a51c Add another permission for model guest in activity 2025-02-14 18:14:35 +01:00
1836677c47 Update file initial.json 2025-02-13 22:30:36 +01:00
e7a98c86f0 Tried something with permissions 2025-02-13 21:51:26 +01:00
eb5044490b Delete a useless permission 2025-02-13 21:37:58 +01:00
983d7ec052 linters 2025-02-13 21:35:29 +01:00
dc56deaf85 Final modifications 2025-02-13 21:17:57 +01:00
19d1ecfc66 continue the script and few change to model 2025-02-13 02:39:33 +01:00
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
b0c3eee699 start to write generate_wrapped script 2025-02-12 00:00:23 +01:00
cd942779ca Wrapped apps 2025-02-11 18:19:24 +01:00
0d0fdef363 fix issue with activity entry view 2025-02-09 17:58:38 +01:00
7ed544b3ac fix issues with activity entry view 2025-02-09 17:50:15 +01:00
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
a209e0d366 Update file forms.py 2025-02-02 14:30:53 +01:00
ef485e0628 Update file forms.py 2025-02-02 14:06:22 +01:00
1481aa0635 Update file forms.py 2025-02-02 14:05:05 +01:00
867bf9fd25 Update file forms.py 2025-02-02 13:33:41 +01:00
47fda0ea36 Update file forms.py 2025-02-02 13:17:19 +01:00
623290827a Update file forms.py 2025-01-27 16:34:45 +01:00
a87ce625f3 Update file note.cron 2025-01-25 13:55:21 +01:00
2ec5a0d9ca Les couleurs des surviva[list]s 2025-01-21 20:11:29 +01:00
3faf611816 Merge main into potvieux 2025-01-20 17:43:25 +01:00
3559787fa7 Merge branch 'New_permission' into 'main'
New permission

See merge request bde/nk20!278
2025-01-18 15:41:15 +01:00
bd6ed27ae5 Update 2 files
- /apps/permission/fixtures/initial.json
- /apps/permission/admin.py
2025-01-18 15:11:57 +01:00
43dc676747 Update file initial.json 2025-01-18 12:57:42 +01:00
caaeab6b0b Update file initial.json 2025-01-17 19:39:26 +01:00
54ba786884 Update file initial.json 2025-01-17 19:03:59 +01:00
80e109114f Update file initial.json 2025-01-17 18:23:28 +01:00
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
414e103686 finitio le message sda 2025-01-05 23:17:01 +01:00
942d887c2e Update file initial.json 2024-12-23 18:31:11 +01:00
a63c34fe37 Update file initial.json 2024-12-22 21:38:17 +01:00
2be6133458 Update file initial.json 2024-12-22 20:42:20 +01:00
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
476fbceeea Donation goal la note kfet x les SdA 2024-10-10 01:48:23 +02:00
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
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
09fb1d227e Correction translation of sport events ml 2024-09-18 08:54:04 +02:00
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
5d16dc4e7d Added some necessary rights 2024-09-17 17:13:47 +02:00
3c34033bf5 fix linters for WEI 2024 survey 2024-09-12 13:41:04 +02:00
131f508433 Merge branch 'survey_wei_2024' into 'main'
update hardcoded

See merge request bde/nk20!273
2024-09-12 12:03:10 +02:00
c1a353963a handle hardcoded corrected 2024-09-12 11:36:37 +02:00
178ce2b579 update hardcoded 2024-09-10 22:41:35 +02:00
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
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
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
5040e8e8ea Merge branch 'continuous-intergration' into 'main'
continuous-intergration

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

Closes #119

See merge request bde/nk20!270
2024-09-05 20:29:30 +02:00
94e1fdc93a add ubuntu 24.4 in tox.ini and remove debian bullseye in gitlab-ci 2024-09-05 20:19:46 +02:00
d1ef367bab Permissions for child clubs, also changed spaces for tabs 2024-09-05 20:17:45 +02:00
0fbb19c5fd limite mail sending to 10 per minute and purge fail mail log 2024-09-05 19:48:54 +02:00
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
185a2cabf2 corrected emoji + linting 2024-08-29 22:47:33 +02:00
7552e55c8d removed diet filed 2024-08-29 22:19:11 +02:00
361de9f8b4 more bug fixing 2024-08-29 21:06:34 +02:00
e2426bd6a6 Bugfix 2024-08-29 20:03:43 +02:00
7fea619a9f add permission to make transfers with members of your club 2024-08-29 20:02:06 +02:00
7b5eefcc0a Update 2 files
- /apps/registration/views.py
- /apps/permission/fixtures/initial.json
2024-08-29 19:23:26 +02:00
e4aa16986f Merge branch 'survey_wei_2024' into 'main'
linting

See merge request bde/nk20!267
2024-08-29 19:12:23 +02:00
b92e6e4e10 linting 2024-08-29 18:36:20 +02:00
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
f50849b4f8 delete print 2024-08-29 14:01:55 +02:00
73ff35c232 updated bus descr 2024-08-29 12:42:26 +02:00
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
2cb9ac8735 replace "…" -> "..." (#130) and disable sorting on certain columns (#129) 2024-08-29 10:19:06 +02:00
35d4849a28 fix Oauth 2024-08-29 00:43:33 +02:00
96539d262f working html for survey + fixed json error + added specific diet text field 2024-08-29 00:05:44 +02:00
946674f59b inclusif, avoids python3.10 syntax 2024-08-28 11:11:32 +02:00
a201d8376a updated survey 2024-08-28 11:01:33 +02:00
a21b9275ea Add caution_check in the validation form, #96 2024-08-28 09:48:52 +02:00
d4e85e8215 test wei 2024, linters 2024-08-28 09:48:52 +02:00
7af2ebba40 basic survey 2024-08-28 09:48:52 +02:00
bd94400883 Merge branch 'food_traceability' into 'main'
Change deprecated function

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

See merge request bde/nk20!263
2024-08-27 18:46:54 +02:00
9ccac36831 Copy constructor 2024-08-27 18:01:13 +02:00
2e71ce05a9 Merge branch 'main' into food_traceability 2024-08-27 17:11:32 +02:00
f2cb10b69f Fix problem in addingredientform, change filter for container in QrcodeForm 2024-08-27 15:12:15 +02:00
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
213e9a8b12 Fix problem in addingredientform, change filter for container in QrcodeForm 2024-08-27 10:47:44 +02:00
2c56178b15 Merge branch 'main' into migration-django-4-2 2024-08-25 16:14:59 +02:00
48a5b04579 Merge branch 'beta' into migration-django-4-2 2024-08-25 16:13:01 +02:00
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
053225c6dc revert sort tables to member views 2024-08-25 15:13:02 +02:00
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
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
ff9c78ed4e added opener in admin and fixed the guest view 2024-08-25 14:29:06 +02:00
1e121297d1 Update invoice_sample.tex, remove link toward bde.ens-cachan 2024-08-23 00:32:37 +02:00
549f56dc0b Translation 2024-08-17 11:58:33 +02:00
debeb33d46 Improve/modify form, view, template. Add permissions 2024-08-17 02:42:29 +02:00
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
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
28117c8c61 Add developers, Opener comments 2024-08-10 11:50:27 +02:00
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
4be4a18dd1 Merge branch 'sortable_tables' into 'beta'
Sortable tables

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

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

See merge request bde/nk20!256
2024-08-08 16:41:06 +02:00
a324d3a892 Merge branch 'beta' into ouvreureuse 2024-08-08 16:28:22 +02:00
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
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
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
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
ae4213d087 Merge branch 'colored_linters' into 'beta'
Colored linters

See merge request bde/nk20!255
2024-08-07 21:25:22 +02:00
b2b1f03b46 Edit food HTML template for translation, translations. Now the mandatory allergens are automatically created 2024-08-06 15:20:22 +02:00
1c5ed2bd3f Edit base.html and few translations 2024-08-06 13:59:30 +02:00
a7e87ea639 API Food 2024-08-04 23:38:21 +02:00
cbf92651f0 Returning 403 when you don't have enough permissions 2024-08-04 21:58:57 +02:00
12c93ff9da bug du jour 31 juillet (bissextile) 2024-08-04 14:45:17 +02:00
354c79bb82 Inclusif manquant 2024-08-04 13:32:33 +02:00
1ea7b3dda1 documentation and modification of permissions 2024-08-02 15:21:34 +02:00
35ffbfcf55 Colored linters 2024-08-01 17:29:24 +02:00
162371042c Creation of "Opener", Fix #117 2024-08-01 14:49:52 +02:00
581715d804 Fix #95 (calendar) 2024-07-31 23:18:41 +02:00
c7c6f0350f Looks unused 2024-07-31 22:19:16 +02:00
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
6f67d2c629 Documentation 2024-07-22 15:52:09 +02:00
4b97ab2e2a linters 2024-07-22 15:52:09 +02:00
dcfd0167e7 Security against the cycles 2024-07-22 15:52:09 +02:00
50a680eed2 Open table and shelf life 2024-07-22 15:52:09 +02:00
226a2a6357 Automatic allergens and expiry_date update 2024-07-22 15:52:09 +02:00
48462f2ffc Adding ingredients to a preparation 2024-07-22 15:52:09 +02:00
260513ae3b Migration fixes 2024-07-22 15:52:09 +02:00
210a3cc93c Implementing QRcode creation, modifying Allergen model and creating of few views 2024-07-22 15:52:09 +02:00
896095a44c Un peu de nettoyage, rajout de commentaires 2024-07-22 15:52:09 +02:00
3f997f94fa few changes in models, delete default label 2024-07-22 15:52:09 +02:00
0801ad64ae création de forms fonctionnel (form + views + url + html), few changes in models.py 2024-07-22 15:52:09 +02:00
64bd5ed546 création d'un form pour l'ajout d'aliments basiques 2024-07-22 15:52:09 +02:00
4c390dce17 nom app 2024-07-22 15:52:09 +02:00
adacc293f5 First forms 2024-07-22 15:52:09 +02:00
968fa64d37 Réagencement des tables et de leurs attributs 2024-07-22 15:52:09 +02:00
a481adbae4 création de l'interface admin temporaire 2024-07-22 15:52:09 +02:00
4de2e987ef Rajout de la pseudo-doc 2024-07-22 15:52:07 +02:00
9e6342c929 Création de l'apps et de la base de donnée 2024-07-22 15:50:09 +02:00
74de358953 Update README.md 2024-07-22 15:50:09 +02:00
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
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
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
076d065ffa Merge branch 'main' into 'remove_picture'
# Conflicts:
#   locale/fr/LC_MESSAGES/django.po
2024-07-18 17:52:22 +02:00
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
01584d6330 Merge branch 'modif_perm' into 'main'
Modif perm

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

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

See merge request bde/nk20!248
2024-06-03 21:53:01 +02:00
2807b6ef44 Note en mode vieux pour la Kokarde de remise des diplomes 2024-05-30 22:06:51 +02:00
10a01c5bc2 linters 2024-05-30 20:21:56 +02:00
989905ea64 Update .gitlab-ci.yml 2024-05-26 18:41:49 +02:00
0218d43a17 Update .gitlab-ci.yml 2024-05-26 16:00:26 +02:00
5d30b0e819 charte info 2024-05-26 15:46:50 +02:00
ec759dd3c0 error py37-django22 2024-05-23 22:38:09 +02:00
2eb965291d new_logo 2024-05-23 21:46:01 +02:00
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
3132aa4c38 Prise en compte des commentaires de Korenstin 2024-03-30 12:44:51 +01:00
c7eb774859 Prise en compte des commentaires 2024-03-30 11:20:23 +01:00
32f8d285b3 Prise en compte des commentaires 2024-03-30 11:12:33 +01:00
050256ea13 Réécriture en inclusif de l'ensemble des textes français de la note 2024-03-29 17:59:43 +01:00
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
258361f116 Update forms.py 2024-03-27 10:25:38 +01:00
a307530579 Merge branch 'change_date' into 'main'
change date

See merge request bde/nk20!245
2024-03-27 10:19:37 +01:00
5de930bf40 Update forms.py 2024-03-27 10:04:14 +01:00
f7ebe0e99b Update forms.py 2024-03-27 09:43:49 +01:00
73de6e2176 Update forms.py 2024-03-27 09:20:32 +01:00
201611b105 change date 2024-03-26 08:33:34 +01:00
40c239e9da Update models.py 2024-03-24 16:41:18 +01:00
2aaab2b454 Update test_treasury.py 2024-03-24 15:55:46 +01:00
fc088dec86 Update test_treasury.py 2024-03-24 15:20:46 +01:00
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
7b48b09329 patch sort and optional description 2024-03-23 14:32:31 +01:00
ffac940511 changement template facture 2024-03-22 18:22:08 +01:00
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
402e19d1ce changed permission for club president 2024-03-22 12:27:08 +01:00
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
98422d8259 réparation photo de profil 2024-03-21 18:37:47 +01:00
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
0d64ad31e0 Update custom.css 2024-03-14 17:22:41 +01:00
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
5295e61a00 Changement couleur de la note 2024-03-14 15:59:53 +01:00
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
68152e6354 Upload New Migration (change bde) 2024-03-11 16:11:54 +01:00
6c61daf1c5 Update views.py
Passage à la time zone Europe/Paris
2024-03-11 10:25:48 +01:00
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
cd8224f2e0 Upload New File 2024-03-09 16:06:39 +01:00
3c882a7854 Delete RavePartlist_bg.png 2024-03-09 16:06:01 +01:00
357e1bbaa2 Replace RavePartlist_bg.png 2024-03-09 16:05:29 +01:00
f5c4c58525 Replace RavePartlist_bg.png 2024-03-09 14:03:17 +01:00
dafb602b08 Update models.py 2024-03-09 13:40:45 +01:00
5b377e6a75 Update facture template 2024-03-09 13:04:33 +01:00
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
b3a31c27a5 Add : Documentation years flag for Extract ML Registrations 2024-03-08 19:34:48 +01:00
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
546a3a72b1 Remove BDE compaign banner 2024-02-15 10:32:39 +01:00
2e5664f79d Merge branch 'Compromis' into 'main'
Update base.html compromis

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

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

See merge request bde/nk20!231
2024-02-13 14:58:31 +01:00
7d59cd6cd2 Compaign banner 2024-02-13 14:26:28 +01:00
96215cc1ff oidc_claim_scope in Class instead of method 2024-02-13 13:43:14 +01:00
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
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
fb3337966e bootstrap4 is now a standalone package from crispy-forms 2024-02-11 22:24:37 +01:00
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
2b3eb15f59 fix one copyright and a string before merge 2024-02-11 16:58:53 +01:00
399a32bece default auto field 2024-02-11 16:51:48 +01:00
82fea65b5e django_htcpcp_tea in middleware only if in apps 2024-02-07 20:03:57 +01:00
abc88d0118 replace url from django.conf.urls by re_path from django.urls 2024-02-07 18:21:08 +01:00
b6b81a8b8f typo 2024-02-07 18:05:32 +01:00
d228dbf225 fix some breaking changes and linters 2024-02-07 18:02:56 +01:00
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
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
7b11cb0797 update Copyright 2024 2024-02-07 01:37:43 +01:00
516a7f4be5 Remove importation of django-htcpcp-tea which is not compatible with django 4.2 2024-01-24 20:14:32 +01:00
2f8c9b54e7 Remove importation of django-cas-server which is not compatible with django 4.2 2024-01-24 19:58:55 +01:00
e9f18c3ed9 migrate to django 4.2 (LTS), change requirement and tests. remove depreciated ifnotequal 2024-01-24 19:18:02 +01:00
d6645900d3 PC Kfet can see kfet memberships to accurately display non-Kfet members in blue 2024-01-11 21:01:55 +01:00
e35847ebd8 Show old people with PC Kfet 2024-01-11 20:37:35 +01:00
57268bc9c2 Styling and catching late registrations as old 2024-01-11 19:48:41 +01:00
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
f481ea6acb happy new year (contain annually WEI change and update to follow Django Style Guide) 2024-01-11 16:32:37 +01:00
ab6c943126 linters 2024-01-11 14:58:41 +01:00
5dc5f56ae4 guests cannot be old 2024-01-11 14:38:43 +01:00
81017fc393 Couleurs Splpp 2024-01-11 11:22:58 +01:00
90e3871934 Merge branch 'main' into potvieux 2024-01-11 02:19:51 +01:00
95e07f3148 New scripts 2024-01-10 18:09:53 +01:00
802fd8c2d7 Merge branch 'search_conso_bugfix' into 'main'
Bugfix

See merge request bde/nk20!225
2023-11-13 14:29:29 +01:00
5209a586a9 Fixed const being redeclared when script is reevaluated 2023-11-08 17:10:05 +01:00
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
988b4c9e88 Linting 2023-10-26 21:03:48 +02:00
e32c267995 Moved js code to the external conso file 2023-10-26 19:10:43 +02:00
5e39209ab1 Made searchbar completely client-based 2023-10-26 19:01:09 +02:00
08b2fabe07 Removing jquery means changing the event API... 2023-10-26 00:22:51 +02:00
405479e5ad Execute script to add behavior to searched buttons 2023-10-26 00:10:56 +02:00
0cc130092f Added a search tab for the conso page 2023-10-25 20:01:48 +02:00
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
0f1e4d2e60 check for a model in permission and use that in treasury 2023-09-28 18:48:57 +02:00
6255bcbbb1 Merge branch 'beta' into 'main'
Merge beta

See merge request bde/nk20!221
2023-09-27 17:14:49 +02:00
d82a1001c4 Moved transaction through frienships right to basic rights 2023-09-27 16:55:00 +02:00
31a54482f0 Updated doc to tell maintainers to create psql superusers 2023-09-27 16:53:30 +02:00
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
422c087d17 fix wei test 2023-09-20 07:04:13 +02:00
30d6e2c95e Added trusts to note admin site 2023-09-19 15:07:30 +02:00
f3a3f07e38 Tweaked message and did missing french translations 2023-09-18 17:29:52 +02:00
a5e802f370 Improved the error message when trying to duplicate a Trust 2023-09-18 17:12:31 +02:00
540f3bc354 regenerated messages so locations are consistent with codebase 2023-09-02 00:04:54 +02:00
2d19457506 Add spanish translation for friendship 2023-09-01 17:35:52 +02:00
72786d0d2b Translated js strings, unified some case 2023-09-01 17:34:52 +02:00
f099cbc879 Linting 2023-09-01 17:32:29 +02:00
977eb7c0d4 Generated translation files, did french 2023-09-01 17:30:38 +02:00
d81b1f2710 Tweaked trust back display 2023-09-01 17:15:24 +02:00
6a69590a82 Added a 'trust back' button, front can be improved 2023-09-01 17:15:24 +02:00
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
4fb0b7d736 First pass on a display of users trusting you, added a corresponding right 2023-09-01 17:15:13 +02:00
18a5b65a1c Merge branch 'VSS' into 'main'
anti VSS

See merge request bde/nk20!219
2023-08-31 15:58:52 +02:00
f545af4977 typo 2023-08-31 15:40:49 +02:00
103e2d0635 add GC anti-VSS 2023-08-31 15:25:44 +02:00
aedf0e87ba prez BDE can block note 2023-08-31 13:46:27 +02:00
dab45b5fd4 translation 2023-08-31 13:40:53 +02:00
b3353b563c add VSS checkbox on registration 2023-08-31 12:21:38 +02:00
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
834d68fe35 typo 2023-08-31 11:45:17 +02:00
c6a2849d35 test 2023-08-30 16:16:29 +02:00
4ab22c92b3 After WEI registration validation, come back to unvalidate registration page 2023-08-30 09:52:17 +02:00
c328c1457c add register button at the end of WEI registration 2023-08-28 22:27:45 +02:00
96da7d01ae change on a field that everyone have (1A don't have bus) 2023-08-28 19:26:51 +02:00
d27f942339 typo 2023-08-28 10:13:28 +02:00
738d6c932d questions ! 2023-08-28 00:42:33 +02:00
1760196578 more tests 2023-08-27 23:11:40 +02:00
13b9b6edea tests 2023-08-27 18:09:46 +02:00
e06e3b2972 one question by page 2023-08-26 23:47:10 +02:00
9596aa7b8c base for questions instead of words 2023-08-26 17:52:48 +02:00
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
8d17801e28 new default year 2023-08-23 23:32:01 +02:00
609362c4f8 Merge branch 'update_permission' into 'main'
Update permission

See merge request bde/nk20!216
2023-08-23 22:50:24 +02:00
03d2d5f03e change -50€ to -20€ and doc 2023-08-22 21:51:02 +02:00
d2057a9f45 remove respo-info perm and change Prez BDE prem 2023-08-22 21:19:05 +02:00
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
6410542027 Update forms.py - Homogénéisation des cases 2023-08-08 15:38:29 +02:00
6b1cd3ba7a manage self aliases for BDE member instead of kfet 2023-07-24 12:42:44 +02:00
9f114b8ca2 fixtures activities 2023-07-24 12:26:34 +02:00
e0132b6dc8 migration permission 2023-07-24 12:20:16 +02:00
f1cc82fab3 Merge branch 'linters' into 'main'
Linters

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

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

See merge request bde/nk20!212
2023-07-04 19:17:17 +02:00
7aeb977e72 Oubli dans le fichier test_wei_registration_.py d'un 2022 en 2023 2023-07-04 18:33:54 +02:00
52fef1df42 Préparation WEI 2023 2023-07-04 18:23:43 +02:00
16f8a60a3f possibilité de l'adhésion au BDA lors de l'inscription 2023-07-04 17:32:48 +02:00
2839d3de1e club facultatif pour un role lors du changement dans l'interface admin 2023-06-22 14:52:11 +02:00
30afa6da0a création d'une permission pour faire les crédits uniquement 2023-06-12 18:29:23 +02:00
84fc77696f see activities: BDE members instead of kfet 2023-06-05 19:04:19 +02:00
19fc620d1f see kfet members' note for respot 2023-06-05 17:26:49 +02:00
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
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
364b18e188 migrations 2023-04-14 16:52:46 +02:00
10a883b2e5 new treasury phone number 2023-04-14 16:00:48 +02:00
1410ab6c4f Almost on time, the SIRET number is now changed 2023-04-14 15:35:18 +02:00
623dd61be6 Remove phone number 2023-04-14 14:56:34 +02:00
48a0a87e7c changement du fond des factures 2023-04-14 00:25:26 +02:00
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
63c1d74f1a Ignore notes containing '- BDE-' in the list of negative balances 2023-04-07 15:47:06 +02:00
c42fb380a6 frequence des mails de négatif aux trez : 1 mois -> 1 semiane 2023-04-06 09:04:27 +02:00
c636d52a73 traduction (allemand et espagnol probablement pas optimal) 2023-03-31 17:21:58 +02:00
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
9c9149b53a Ajout d'un lien vers la FAQ de la note. 2023-03-31 12:34:14 +02:00
cb74311e7b Commit migration, j'étais triggered 2023-03-30 19:14:52 +02:00
9d7dd566c9 Ignore /tmp/ 2023-03-30 17:26:06 +02:00
6bceb394c5 prez BDE sould see invoice list 2023-03-29 20:43:54 +02:00
62cf8f9d84 forgetted coma 2023-03-28 20:41:53 +02:00
9944ebcaad changement des couleurs de la note vers les couleurs totalist spies 2023-03-25 02:13:16 +01:00
8537f043f7 changement des couleurs de la note vers les couleurs totalist spies 2023-03-25 00:57:19 +01:00
2dd1c3fb89 change mask for some perm 2023-03-20 22:35:51 +01:00
c8665c5798 change permissions for role 2023-03-20 22:21:18 +01:00
e9f1b6f52d change permanent permissions 2023-03-20 17:19:14 +01:00
1d95ae4810 sort perm by number 2023-03-20 16:16:32 +01:00
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
73640b1dfa changement du fond des factures 2023-01-30 00:06:45 +01:00
770c748bd9 couleurs finalist pour le pot vieux 2023-01-29 21:18:13 +01:00
06fa096405 Fina[list] colors 2023-01-27 20:42:00 +01:00
182f680507 Merge branch 'main' into potvieux 2023-01-27 19:00:02 +01:00
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
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
c441a43a8b link SogeCredit to WEI by creation date instead of civil year 2023-01-10 21:40:03 +01:00
87f3b51b04 Add custom pagination size as an API parameter 2022-12-14 18:37:13 +01:00
0a853fd3e6 Merge branch 'permission_trez' into 'main'
fix trez perm

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

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

See merge request bde/nk20!202
2022-09-04 22:56:07 +02:00
7068170f18 fixing grammar in comments 2022-09-04 13:24:39 +02:00
45ee9a8941 Soge only payd WEI (not bde/kfet membership) 2022-09-04 12:52:40 +02:00
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
761fc170eb Update Spanish translation 2022-04-13 12:30:22 +02:00
ac23d7eb54 Generated translation files for de/es (but didn't translate anything) 2022-04-13 12:30:22 +02:00
40e7415062 Added translations for friendships 2022-04-13 12:30:22 +02:00
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
633ab88b04 Linting 2022-04-13 12:30:22 +02:00
e29b42eecc Add permissions related to trusting 2022-04-13 12:30:22 +02:00
dc69faaf1d Better user search to add friendships 2022-04-13 12:30:22 +02:00
442a5c5e36 First proro of trusting, with models and front, but no additional permissions 2022-04-13 12:30:22 +02:00
7ab0fec3bc Added trust model 2022-04-13 12:30:22 +02:00
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
ee22e9b3b6 fixing color to follow the proper theme 2022-04-12 18:33:22 +02:00
19ae616fb4 switching to survivalist color 2022-04-12 17:40:52 +02:00
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
4d03d9460d Passage des couleurs ttlsp 2022-04-05 14:45:41 +02:00
5ceda66ded use the right colors. 2022-04-01 08:54:57 +02:00
6443d64b69 View and Highlight Vieux people. 2022-04-01 08:54:05 +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
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
e479e1e3a4 Added messages for Hide/Show 2021-10-07 23:06:40 +02:00
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
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
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
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
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
6b4d18f4b3 fix #97 2021-09-26 23:03:25 +02:00
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
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
c4c4e9594f couleur 4.0 2021-05-22 12:34:31 +02:00
4166823d55 couleurs 3.0 2021-05-22 12:29:02 +02:00
dc0f3dbcef Changement de couleur 3.0 2021-05-22 12:19:29 +02:00
4583958f50 Merge branch 'beta' into 'master'
Changement de couleurs

See merge request bde/nk20!165
2021-05-22 09:56:55 +00:00
b3abe9ab18 Changement de couleur 2.0 2021-05-22 11:53:13 +02:00
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
67e170d4a6 Bonjour c le changement de couleur 2021-05-22 11:30:11 +02:00
8f895dc4d7 note: use native selector rather than Query 2021-05-12 18:03:44 +02:00
1187577728 Do not lint scripts submodule 2021-05-12 17:33:11 +02:00
8a58af3b31 Reformat apps/registration/tokens.py 2021-05-05 19:48:19 +02:00
0c23625147 Remove newline in imports 2021-05-05 19:47:16 +02:00
21219b9c62 Rename join_BDE and join_Kfet to lowercase 2021-05-05 19:46:53 +02:00
5ab8beecef Use _ prefix for ignored loop variable 2021-05-05 19:14:59 +02:00
1ca5133026 BaseException.message is removed in Python 3 2021-05-05 19:12:23 +02:00
93bc6bb245 Do not call setattr with a constant attribute value 2021-05-05 19:12:03 +02:00
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
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
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
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
c109cd3ddd Source is not destination
Signed-off-by: Yohann D'ANELLO <yohann.danello@gmail.com>
2021-01-19 15:17:03 +01:00
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
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
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
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
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
b5f3b3ffc1 Use Nginx certbot challenge
Signed-off-by: Yohann D'ANELLO <yohann.danello@gmail.com>
2020-12-29 21:44:28 +01:00
3aad4e7398 Agree Let's Encrypt ToS
Signed-off-by: Yohann D'ANELLO <yohann.danello@gmail.com>
2020-12-29 21:41:29 +01:00
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
9d8f47115c ConsumerViewSet is a bit tricky
Signed-off-by: Yohann D'ANELLO <yohann.danello@gmail.com>
2020-12-23 21:50:48 +01:00
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
e60994e065 API Documentation
Signed-off-by: Yohann D'ANELLO <yohann.danello@gmail.com>
2020-12-23 21:06:30 +01:00
801f711994 Merge branch 'beta' into docs 2020-12-23 20:19:40 +01:00
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
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
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
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
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
6b2638c271 Documentation is located on /doc
Signed-off-by: Yohann D'ANELLO <yohann.danello@gmail.com>
2020-12-23 15:20:30 +01:00
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
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
95be0042e9 Fix transaction API page 2020-12-22 13:28:43 +01:00
48880e7fd3 More API filters for the wei app 2020-12-22 13:11:01 +01:00
e0030771e4 More API filters for the treasury app 2020-12-22 12:53:35 +01:00
d47799e6ee More API filters for the permission app 2020-12-22 12:42:54 +01:00
eae091625a More API filters for the note app 2020-12-22 12:37:21 +01:00
aceb77ffb9 More API filters for the activity app 2020-12-22 03:18:43 +01:00
338c94ed05 More API filters for the member app 2020-12-22 02:58:12 +01:00
290848f904 Non-member people can update their profile everytime 2020-12-02 14:58:14 +01:00
72dca54bbf Wrong path for artifact uploading 2020-11-26 03:13:57 +01:00
117d9da3ba Gitlab compiles the documentation 2020-11-26 02:55:36 +01:00
37efebe85b Ansible builds and deploys the documentation 2020-11-26 02:49:57 +01:00
3af2ec71b6 Extract documentation from the Gitlab wiki and convert it to reStructuredText 2020-11-26 02:29:51 +01:00
0b4a95525b Use RTD theme 2020-11-26 01:07:18 +01:00
af664e481f Initial setup for Sphinx 2020-11-26 01:01:08 +01:00
0171f16311 Merge branch 'beta' into 'master'
Translate some words

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

See merge request bde/nk20!141
2020-11-21 13:37:24 +01:00
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
c1efb87180 Fix spanish translations 2020-11-21 12:37:31 +01:00
72eead8595 Add spanish javascript translation 2020-11-21 12:26:31 +01:00
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
2f54e64ea2 Merge branch 'JS_translations' into 'beta'
Js translations

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

See merge request bde/nk20!138
2020-10-25 22:08:00 +01:00
051591cb7a Don't see user detail in update form 2020-10-25 21:49:16 +01:00
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
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
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
08e50ffc22 Credit form didn't raise an error when the data didn't validate 2020-10-23 18:19:21 +02:00
9cb65277f3 Merge branch 'beta' into 'master'
Ajustement de permissions

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

See merge request bde/nk20!136
2020-10-20 10:43:01 +02:00
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
6cc3cf4174 A migration put the right role in the note account's memberships 2020-10-20 00:28:49 +02:00
2097e67321 Add permissions to PC Kfet 2020-10-20 00:19:49 +02:00
d773303d18 Add possibility to authenticate an account with its IP address 2020-10-19 23:44:56 +02:00
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
bf29efda0a Display real user name in the Soge credits list/detail 2020-10-08 10:36:30 +02:00
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
3eced33082 Well, everyone doesn't want a secondary bank account 2020-10-07 17:43:28 +02:00
acb3fb4a91 Highlight future users that declared that they opened a bank account 2020-10-07 17:42:46 +02:00
1c5e951c2f Merge branch 'beta' into 'master'
Various fixes

See merge request bde/nk20!133
2020-10-07 12:06:48 +02:00
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
0078eb8f90 Index page is a redirection 2020-10-07 11:53:42 +02:00
e5e758f9d9 Display banners when a user is no more a BDE or Kfet member 2020-10-07 11:46:43 +02:00
4a78328717 The checkbox to tell that a Sogé account got opened is not mandatory 2020-10-07 11:31:20 +02:00
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
b5fa428bad Non-Kfet members can see their old aliases only, but no one else 2020-10-07 11:22:02 +02:00
fb72385773 Warn users that they have to open they Sogé account 2020-10-07 10:59:37 +02:00
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
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
8ada0e51f2 The validation filter of the soge credit list was buggy 2020-10-07 10:42:52 +02:00
c3d613947f Pre-registered users can declare that they opened a bank account in the signup form 2020-10-07 10:33:57 +02:00
36b8157372 Fix membership table order 2020-10-07 10:03:43 +02:00
992cfe8e23 Can set a parent club to None 2020-10-07 09:48:21 +02:00
18a8ff1b8a Set credit/debit reason non mandatory 2020-10-07 09:45:09 +02:00
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
4b12e3ed08 Display only the most recent membership 2020-10-07 09:29:41 +02:00
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
bbe53b3b63 Update README.md 2020-10-05 16:25:06 +02:00
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
541ed59f40 When a membership is created, redirect to the user profile page rather than club detail 2020-10-04 21:08:35 +02:00
e172b4f4bb When a membership is renewed, set the same roles as the previous membership 2020-10-04 20:54:03 +02:00
d666179037 Display Renew membership button 15 days more 2020-10-04 20:50:10 +02:00
f22e92132c Select for update transaction notes, and not only the transaction 2020-10-04 20:47:15 +02:00
ca7ad05746 Use a signal to prevent a user that the note balance is negative 2020-10-04 20:26:43 +02:00
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
d4e4ed580f remove the display limit for pre-registred users. 2020-10-03 17:53:38 +02:00
8756751344 Merge branch 'beta' into 'master'
Fix some membership issues

See merge request bde/nk20!129
2020-10-01 15:00:36 +02:00
fd83fe19bf Fix some membership date control 2020-10-01 09:17:02 +02:00
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
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
e48ef92137 Revert commit that broke beta branch 2020-09-23 18:32:09 +02:00
919d0b7e85 Merge branch 'ics_cache' into 'beta'
Ics cache

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

See merge request bde/nk20!126
2020-09-21 12:53:03 +02:00
4f188ca3e5 Admin is autodiscovering partially 2020-09-21 12:34:34 +02:00
72bac75fbd Add Django OAuth toolkit admin 2020-09-21 12:15:40 +02:00
6d54aae614 Fix django-oauth-toolkit version 2020-09-21 11:15:00 +02:00
8052152ea5 Add OAuth2 endpoints 2020-09-21 11:03:07 +02:00
70448db8e5 Remove Django CAS server and add oauth toolkit 2020-09-21 10:31:42 +02:00
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
3ba61385a3 Debit is not credit 2020-09-20 11:12:44 +02:00
7353348d7a Rollback transaction when checking an add permission (experimental) 2020-09-20 09:07:51 +02:00
f63e2e088e Don't log when the permission to lock a note is checked 2020-09-20 08:56:42 +02:00
420a24ebac enable JavaScriptCatalog view 2020-09-19 22:42:35 +02:00
d566def706 Try to translate js, not working... 2020-09-19 22:03:45 +02:00
eaf6769e8b Treasurers can make transactions with people that are no longer a member 2020-09-19 16:33:52 +02:00
a61ec81cff note.crans.org is the default domain 2020-09-19 16:03:32 +02:00
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
bcd96b2ed8 The BDE membership and the club membership must now be in two parts 2020-09-18 12:35:36 +02:00
5c702187e5 Merge branch 'beta' into 'master'
Corrections diverses

See merge request bde/nk20!123
2020-09-14 10:16:13 +02:00
905d65371f The user validation form was ugly 2020-09-14 09:56:15 +02:00
180cd3e1ec Fix registration permissions and procedure 2020-09-14 09:49:30 +02:00
73ca65aa91 Merge branch 'atomicity' into 'beta'
Atomicité

See merge request bde/nk20!122
2020-09-14 09:38:54 +02:00
5ed0560953 Fix linting 2020-09-14 09:09:20 +02:00
dbc6fbbf71 Fix the validation clicker issue, now the note is safe 2020-09-14 09:05:35 +02:00
872fd8f86d Don't cache permissions in debug mode, that's very slow 2020-09-14 08:58:12 +02:00
f89234b69a JS was broken, please close your HTML tags 2020-09-13 23:18:00 +02:00
36a980555b Revert "Make the nk20 usable for pirates"
This reverts commit 0f53ac45f7.
2020-09-13 20:42:44 +02:00
826cd4d87f Revert "Use underscore in locales"
This reverts commit 2270a0aa82.
2020-09-13 20:42:34 +02:00
e8005a6c58 Update Django locale selector 2020-09-13 20:17:59 +02:00
2270a0aa82 Use underscore in locales 2020-09-13 20:10:26 +02:00
0f53ac45f7 Make the nk20 usable for pirates 2020-09-13 20:05:06 +02:00
670556c59e Merge branch 'traduction' into 'beta'
End of Spanish translation

See merge request bde/nk20!121
2020-09-13 18:55:56 +02:00
5b02ba48e0 Some OTL, but so much remain 2020-09-13 12:40:10 +02:00
f3f18bc25e Merge branch 'beta' into traduction 2020-09-13 12:17:25 +02:00
03124e124c Translation : typo 2020-09-13 11:52:43 +02:00
6308964e93 Translation : French OTL 2020-09-13 11:52:13 +02:00
ed79097288 Spanish translation : ~end 2020-09-13 11:51:29 +02:00
d7eaef8cee Spanish translation : 88%, ¡ quedan 77 para mañana ! 2020-09-12 22:54:41 +02:00
01d405e54b Spanish translation : 70%, a comer! 2020-09-12 20:18:11 +02:00
80e3cba4c6 BDE Treasurers can see the remittance interface 2020-09-12 18:40:14 +02:00
f190053e84 Display the right amount in soge credit detail 2020-09-12 18:36:05 +02:00
218960adb5 Spanish translation : 57%, not always 57 2020-09-12 16:34:24 +02:00
88a1eae631 Spanish translation : 42%, always 42 2020-09-12 11:59:59 +02:00
2a2ecb2acc Activate es locale 2020-09-12 09:17:15 +02:00
f5486bdb63 Merge branch 'traduction' into 'beta'
Traduction

See merge request bde/nk20!120
2020-09-12 09:19:04 +02:00
9b090a145c All transactions are now atomic 2020-09-11 22:52:16 +02:00
860c7b50e5 Filter a consumer by its note id 2020-09-10 14:42:52 +02:00
afdc75c0bd Access to consumer object wa buggy 2020-09-10 14:41:09 +02:00
c6603e8aa7 Add more filters in the API 2020-09-10 14:37:11 +02:00
72cc1638e6 Authenticate correctly users that connect with an authorization token 2020-09-10 09:31:27 +02:00
6a0dc4cb10 Users can see every API page since querysets are filtered and modifications are protected 2020-09-09 22:27:07 +02:00
0f1f3b9560 Do not serve static files outside of debug server 2020-09-09 17:14:03 +02:00
c720e5483e Move transfer.js where it belongs 2020-09-09 16:45:15 +02:00
0fd3e9db78 Move consos.js where it belongs 2020-09-09 16:42:45 +02:00
c34296c923 Merge branch 'fixed_width_image' into 'beta'
Fix profile picture width

See merge request bde/nk20!119
2020-09-09 15:18:50 +02:00
ce4c22a4a1 Smaller text and larger padding on note label 2020-09-09 15:03:34 +02:00
3e0f665ef8 Resync es translation 2020-09-09 14:32:01 +02:00
be8751c815 Merge branch 'beta' into traduction 2020-09-09 14:28:19 +02:00
8225445c3e Update translations 2020-09-09 14:10:07 +02:00
f333e6a875 Fix profile picture width 2020-09-09 14:03:49 +02:00
e5835b46a5 Backups are sent to Zamok 2020-09-08 13:31:22 +02:00
fe937405a6 Merge remote-tracking branch 'origin/beta' into beta 2020-09-08 10:11:44 +02:00
0741c8ad2b Refactor the script to extract the mails that are registered to an events mailing list 2020-09-08 10:11:33 +02:00
3191dba31f Merge branch 'beta' into 'master'
Fix permissions to let treasurers to make some initial registrations

See merge request bde/nk20!118
2020-09-07 23:52:12 +02:00
428de69d93 Fix permissions to let treasurers to make some initial registrations 2020-09-07 23:36:50 +02:00
9b8caa7fa1 Merge branch 'beta' into 'master'
Add animated profile picture support

See merge request bde/nk20!116
2020-09-07 21:48:28 +02:00
fa3c723140 The BDE offers 80 € to each new member that registers to the Société générale 2020-09-07 21:33:23 +02:00
dc6a5f56f6 Remove WEI mention from register page and mail 2020-09-07 19:44:54 +02:00
6b06853678 Add cards to registration apps 2020-09-07 19:28:18 +02:00
346aa94ead Don't trigger signals when we add an object through a permission check 2020-09-07 14:57:30 +02:00
78586b9343 Don't trigger signals when we add an object through a permission check 2020-09-07 14:52:37 +02:00
353416618a Linebreaks are rendered as <<BR>> in the wiki 2020-09-07 13:54:06 +02:00
9eff3d8850 Merge branch 'no_null_charfield' into 'beta'
Add __str__ to models, remove null=True in CharField and TextField

See merge request bde/nk20!117
2020-09-07 11:39:19 +02:00
7a32c30b8c Model names translations were missing 2020-09-07 11:23:05 +02:00
0183ba193c Plain text mode in reports 2020-09-07 11:07:31 +02:00
f3f746aba8 Plain text mode in reports 2020-09-07 11:03:58 +02:00
53c4e38771 Add __str__ to models, remove null=True in CharField and TextField 2020-09-07 01:06:22 +02:00
4a9c37905c Fix alias count on club info 2020-09-06 21:54:12 +02:00
4452d112e3 Navbar should expand only on large screen 2020-09-06 21:50:06 +02:00
27aa2e9da8 JQuery is unable to cancel Turbolinks 2020-09-06 21:41:33 +02:00
89b2ff52e3 Fix I'm the emitter button 2020-09-06 21:38:55 +02:00
48407cacf8 Call subprocesses with absolute path 2020-09-06 21:19:17 +02:00
b6901ea1e5 Use flake8-django 2020-09-06 20:49:06 +02:00
012b84614c Hide asterix on login form 2020-09-06 20:32:46 +02:00
3988261a64 Tippfehler 2020-09-06 20:25:12 +02:00
c06354211b Translate login page 2020-09-06 20:21:31 +02:00
1023c6c502 Use pre_delete signal insted of Model.delete() to prevent note balance issues when deleting a transaction (don't do it) in Django Admin 2020-09-06 20:18:59 +02:00
cc5996121b Change HTML localization 2020-09-06 20:04:10 +02:00
40a3405f47 Fix missing spaces before comment 2020-09-06 19:16:35 +02:00
82924c999a Add animated profile picture support 2020-09-06 18:54:21 +02:00
f1dac73c08 Merge branch 'beta' into 'master'
First release

See merge request bde/nk20!115
2020-09-06 17:03:51 +02:00
72c004cb56 Remove ugly semicolon in invoices template, release first version 🎉 2020-09-06 16:49:06 +02:00
1ed74021a2 Merge branch 'beta' into 'master'
v1.0.0

See merge request bde/nk20!114
2020-09-06 16:06:26 +02:00
b1fed3d476 Remove .png in invoice model 2020-09-06 15:49:44 +02:00
d5f324c2d5 Test the render of the rights page (more coverage, yeah) 2020-09-06 15:32:18 +02:00
dcdd8e56e8 Migrate LaTeX to XeTeX 2020-09-06 15:30:12 +02:00
ae028b7d06 Disable pdflatex interactivity 2020-09-06 13:36:07 +02:00
5ebdb015ad Decompose some membership functions, now we have a good linting :) 2020-09-06 13:30:38 +02:00
69f87c0f64 Dev should do a recursive clone 2020-09-06 13:16:47 +02:00
eb58db7df9 Inscrease max function complexity to 15 2020-09-06 13:13:29 +02:00
81ad38927d Merge branch 'beta' into 'master'
Fix note pictures, better ansible

See merge request bde/nk20!113
2020-09-06 13:09:28 +02:00
8aac738c4a Treasurers can see any profile and change the note picture of their clubs 2020-09-06 12:55:27 +02:00
eb4641ed35 Upload button wasn't translated 2020-09-06 12:35:59 +02:00
ae31cdf15e Group ansible hosts 2020-09-06 12:32:17 +02:00
fcd1bb98a8 Merge branch 'image_upload' into 'beta'
Move image upload code to form clean

See merge request bde/nk20!112
2020-09-06 12:30:45 +02:00
15ed9d81d5 Check image size before sending it 2020-09-06 12:16:36 +02:00
de3660b23c Move image upload code to form clean 2020-09-06 12:04:54 +02:00
487c3ef0da Human-readable activity wiki page should be updated in cron file 2020-09-06 11:11:02 +02:00
af48eeeaec Activities refresh should not be commented in cron file 2020-09-06 11:04:06 +02:00
c503e77b23 Fix some typo in Ansible 2020-09-06 10:46:03 +02:00
1a28e876b8 Rework on Ansible config, this is now more universal 2020-09-06 10:32:52 +02:00
2a824cadf6 COPYING is standard in the GNU project 2020-09-06 09:47:39 +02:00
2a1bfa9735 Update Django CAS server to 1.2.0 2020-09-06 09:41:32 +02:00
a64dc9ffc2 Certbot and Nginx disappeared in Ansible conf 2020-09-06 09:38:27 +02:00
b63fa19644 With normal rights, notes were displayed as there were inactive 2020-09-06 09:10:57 +02:00
2f6c7ed156 Merge branch 'beta' into 'master'
Remove padding around note picture

See merge request bde/nk20!111
2020-09-05 19:41:28 +02:00
00bc9550f2 Add padding to the the note picture (cc shirenn) 2020-09-05 19:27:22 +02:00
be8e74d056 If a note is saved and the main name changed without changing the normalized form, update the main alias 2020-09-05 15:41:47 +02:00
2b2dde85dc Merge branch 'beta' into 'master'
Few fixes

Closes #61

See merge request bde/nk20!110
2020-09-05 14:52:29 +02:00
9f619a9df8 Center profile picture in transfer interface, closes #61 2020-09-05 14:36:49 +02:00
96954b1afd Club managers can change the picture of the club note 2020-09-05 14:32:47 +02:00
2a8a5cd736 Fix some linting, some complex functions are remaining 2020-09-05 14:29:40 +02:00
e73b3cf69d Fix refresh activities cron 2020-09-05 14:28:05 +02:00
2e13356e39 Fix a bug in note saving 2020-09-05 13:52:03 +02:00
d273193b1d Save the list of changed usernames and lost aliases 2020-09-05 13:51:00 +02:00
3e9b3d690f Note is up on note.crans.org 2020-09-05 11:23:11 +02:00
863150d200 Merge branch 'beta' into 'master'
Last deployment fixes

See merge request bde/nk20!109
2020-09-05 11:21:07 +02:00
f96b1f26a4 Skip invoice rendering (have to fix later) 2020-09-05 11:07:04 +02:00
466db42318 Add texlive-latex-recommended in Gitlab-CI 2020-09-05 10:39:33 +02:00
3af083fb6b Remove temporary bera font in invoices, add texlive-base-recommended for invoices 2020-09-05 10:34:19 +02:00
bcb2398d68 Use migrations instead of fixtures to create BDE, Kfet and special notes 2020-09-05 10:33:24 +02:00
8c23726f88 Don't rebuild systematically migrations 2020-09-05 10:07:32 +02:00
751a4291ab We are in production, then we commit migrations 2020-09-05 10:05:17 +02:00
77b0241406 Log TeX error directly 2020-09-05 09:00:16 +02:00
afc367cfb8 Use === in JS 2020-09-05 08:38:31 +02:00
bad5fe3c22 Format JS files 2020-09-05 08:30:41 +02:00
a97a36bc9e Add apps/script submodule to CI 2020-09-05 08:17:27 +02:00
94706328ff Tests can run between 12pm and 2am 2020-09-05 00:47:55 +02:00
2fc13e5418 Edit the wiki after an activity update iff the wiki password is defined, and don't run the script asynchronous with a SQLite database 2020-09-05 00:47:30 +02:00
2e80233cbc Change debug option to "print stdout" / "edit wiki" in the Refresh activities script 2020-09-05 00:45:14 +02:00
ebe6ce61e4 Merge branch 'beta' into 'master'
Fix Ansible script for production

See merge request bde/nk20!108
2020-09-04 22:42:42 +02:00
0f47412c38 Fix Ansible script for production 2020-09-04 22:37:18 +02:00
3c636e9f71 Merge branch 'beta' into 'master'
Beta

Closes #59

See merge request bde/nk20!107
2020-09-04 22:32:14 +02:00
4ddd763886 Test activity app 2020-09-04 21:46:40 +02:00
0888afe439 I am hungry, so I ham hungry 2020-09-04 20:38:57 +02:00
6d1b75b9b6 Fix linebreaks in ICS file 2020-09-04 19:24:48 +02:00
70e1a611dd Export activites as an ICS Calendar 2020-09-04 18:36:20 +02:00
3111c30e56 Add spanish translation 2020-09-04 18:24:49 +02:00
5c7fe716ad Fix JSON 2020-09-04 16:43:57 +02:00
9b4923fc04 Fix some permissions, grant temporary all treasurers to make transactions from anyone to anyone while a better system is not implemented 2020-09-04 16:37:17 +02:00
c93c81861d Users can change their password, fix #59 2020-09-04 16:28:50 +02:00
f71fb1fa81 Use pre-defined queryset by default in API views 2020-09-04 16:02:42 +02:00
c03c18e93a Test and cover treasury app 2020-09-04 15:53:00 +02:00
b6847415b5 Remove unused imports in tests 2020-09-04 07:53:31 +02:00
cb545417ac Linting does not require deps 2020-09-04 07:47:52 +02:00
f8a0e20772 Regenerate locales 2020-09-04 07:44:59 +02:00
43fffdf56f Merge branch 'traduction' into beta 2020-09-03 23:48:39 +02:00
c66d66bc64 fertig ! 2020-09-03 23:47:31 +02:00
d29e1d69d1 Format api viewsets 2020-09-03 21:47:08 +02:00
ff187581c9 Remove useless blank lines and spaces in api app 2020-09-03 21:21:19 +02:00
f02efd3b39 100% coverage on registration app 2020-09-03 20:03:40 +02:00
4b85a35a9d Fix double consumptions 2020-09-03 16:10:29 +02:00
f7f6f053f7 Format date to ISO standard 2020-09-03 14:33:26 +02:00
22bae51808 mehr Deutsch 2020-09-03 14:23:00 +02:00
76aacaf048 Definitively more usable 2020-09-03 11:47:17 +02:00
cc7ebd2d8a Sometimes the nk20 is too laggy 2020-09-03 00:35:25 +02:00
42778baf20 51% der Übersetzung, Sandmännchen ruf an. 2020-09-03 00:02:45 +02:00
fed9567522 Force line breaks on transactions reason in history, but don't wrap dates or amounts 2020-09-02 23:49:10 +02:00
177128f593 do not gather build in translation. 2020-09-02 23:26:28 +02:00
7bdf5a4366 Update picture path in member test 2020-09-02 23:25:32 +02:00
4b149213f9 Anfang der Deutschen Übersetzung 2020-09-02 23:15:20 +02:00
7fc2559530 French update 2020-09-02 23:14:35 +02:00
be6cf93cdb Move default profile picture in member app 2020-09-02 23:08:40 +02:00
bf7f5b9cd6 Test and cover fully member app 2020-09-02 22:54:01 +02:00
1b8cb7abb0 Send user id and group id in Docker console 2020-09-02 22:53:43 +02:00
3d20987b18 Ignore W503 in linting 2020-09-02 22:33:31 +02:00
06679b2e6a Remove deprecation warning and enable some linting rules 2020-09-02 22:27:04 +02:00
9d1a355ea1 French translation 2020-09-02 21:14:02 +02:00
6a2b46be72 make API token button nicer 2020-09-02 19:16:08 +02:00
4da5c41f40 move viewsets and serializers out of urls.py 2020-09-02 19:00:04 +02:00
8db9e92986 Sqlite does not support order by in subqueries 2020-09-02 18:01:41 +02:00
3e42f4fffb superusers at creation gets automatically valid registration 2020-09-02 17:22:06 +02:00
cde35ea9f9 Merge branch 'doc_install' into 'beta'
Doc install

See merge request bde/nk20!106
2020-09-02 16:24:30 +02:00
f1fe6c4996 Fix some pip requirements versions 2020-09-02 15:52:44 +02:00
d73d9f8bda Add Debian requirements to Pip 2020-09-02 15:45:07 +02:00
e85ec1fa05 Fix TOC link in README 2020-09-02 15:31:57 +02:00
d74007d523 TOC and update dev instructions 2020-09-02 15:30:03 +02:00
cc5f04e2b3 Add script to launch a Docker bash easily 2020-09-02 15:26:57 +02:00
d054d58661 Merge branch 'beta' into 'master'
Better CI

See merge request bde/nk20!105
2020-09-02 13:17:37 +02:00
cf7101fc0f Less deps in README and ansible role 2020-09-02 12:57:46 +02:00
fb47e22ae1 Remove LaTeX extra 2020-09-02 12:30:38 +02:00
980032bfbf Remove ltablex and tabularx TeX depency 2020-09-02 12:23:45 +02:00
31585a9c7e Revert to a simpler CI 2020-09-02 12:14:41 +02:00
9f42ecb97a Merge branch 'beta' into 'master'
Beta

Closes #52, #54, #55, #56 et #57

See merge request bde/nk20!104
2020-09-02 10:00:22 +02:00
b5028b9814 Desactivate docker entrypoint on CI 2020-09-01 22:42:56 +02:00
0e8557404d Fix gitlab ci format 2020-09-01 22:26:34 +02:00
0f56e90e48 Also keep local cache for CI 2020-09-01 22:25:16 +02:00
9bd7569935 Cache Docker image creation 2020-09-01 22:22:36 +02:00
a3fe13aeb4 Use docker image for CI 2020-09-01 22:17:48 +02:00
22140a1428 Remove LaTeX font-extra from Docker image 2020-09-01 21:19:37 +02:00
cdae654034 Do not install recommends in Docker 2020-09-01 21:13:45 +02:00
ebe9c62823 Use v1 auth to dockerhub 2020-09-01 19:59:52 +02:00
d76aa3fec9 Some table accessors weren't updated 2020-09-01 19:04:35 +02:00
05164636a1 Switch to kaniko to build docker image 2020-09-01 17:59:56 +02:00
361ea8cad3 Update Django Tables 2, change accessor from dot to __ 2020-09-01 17:58:58 +02:00
819795c1f9 Add docker build to CI 2020-09-01 17:33:03 +02:00
b5c1289358 Update PIP dependencies 2020-09-01 17:19:53 +02:00
0395717d19 Add instructions for backports 2020-09-01 16:54:34 +02:00
4bb8a443d5 Update sample Traefik config 2020-09-01 16:49:55 +02:00
646c23ccb8 Fix note.cron in ansible 2020-09-01 16:42:53 +02:00
cf4e1f33b4 Add mirror variable 2020-09-01 16:35:43 +02:00
5efb150583 Update Ansible packages 2020-09-01 16:32:13 +02:00
08defd84e6 Merge branch 'debian_deps' into 'beta'
Debian deps

See merge request bde/nk20!103
2020-09-01 16:09:00 +02:00
7c9287e387 Test and cover note app 2020-09-01 15:54:56 +02:00
c6abad107a RecurrentTransaction has no longer a category 2020-09-01 15:54:35 +02:00
81e418e17e Use DateTimeField instead of Field in Transaction search form 2020-09-01 15:53:56 +02:00
1977e403e3 History tables are not orderable 2020-09-01 15:52:54 +02:00
eaf256b1b6 Fix mails when the user or the club has a negative balance 2020-09-01 15:52:27 +02:00
2b70a05a9e Remove useless category field in RecurrentTransaction (that is the category of the template) 2020-09-01 15:51:47 +02:00
be08c12dca Don't cover scripts app (don't import old data everytime) 2020-09-01 15:49:47 +02:00
aa247c281f Fix tzdata prompt during apt install 2020-09-01 15:42:00 +02:00
739da3a090 CI 4 VS erdnaxe 0 2020-09-01 15:19:06 +02:00
85c9c4b2ba CI 3 VS erdnaxe 0 2020-09-01 15:13:25 +02:00
27845303b8 CI 2 VS erdnaxe 0 2020-09-01 15:13:08 +02:00
e3fc79231d CI 1 VS erdnaxe 0 2020-09-01 14:53:11 +02:00
9b2a8c4f6f Try to fix Gitlab CI 2020-09-01 14:47:03 +02:00
d89f6dcf5c Update install instructions 2020-09-01 14:39:03 +02:00
5feb23ad51 Use Debian font awesome 2020-09-01 14:33:38 +02:00
b4ef4b8089 Use local javascript and css libs 2020-09-01 14:28:11 +02:00
6de46a9264 Remove useless nginx/uwsgi config 2020-09-01 14:19:38 +02:00
bfd08fec09 Working Docker production env 2020-09-01 14:16:18 +02:00
55be6be6c5 Docker instruction before Docker compose 2020-09-01 14:03:06 +02:00
bf7b187048 Working docker devserver 2020-09-01 13:52:28 +02:00
09853ce990 Use debian deps in Dockerfile 2020-09-01 12:09:02 +02:00
2acb47c516 Base docker on Debian Buster 2020-09-01 11:23:56 +02:00
ff7e954652 Add missing card on button edit 2020-09-01 10:47:51 +02:00
9c794205f0 More readable navbar and no more padding in mark 2020-09-01 10:47:29 +02:00
3922fcd93a Merge branch 'no_content_title' into 'beta'
No content title

See merge request bde/nk20!102
2020-09-01 10:35:04 +02:00
534831f380 Use buttons in profile page 2020-09-01 10:30:47 +02:00
dd9ca315fa Clean up templates header 2020-09-01 10:20:16 +02:00
d9e003a8f4 Remove contenttitle 2020-09-01 10:13:05 +02:00
dbca5db7d7 Merge branch 'front_erdnaxe' into 'beta'
Front erdnaxe

See merge request bde/nk20!101
2020-09-01 10:07:25 +02:00
c1c211629d Change loading bar color 2020-09-01 10:02:22 +02:00
b787c8cfe2 Do not make long alias break layout 2020-09-01 09:56:19 +02:00
affa2b1a4d Autocomplete of fixed size 2020-09-01 09:52:45 +02:00
e0c1a5f590 Move highlighted buttons under the note selector 2020-09-01 09:46:56 +02:00
e8dcf295ad Make outline button have a background 2020-09-01 09:46:33 +02:00
5642c268e9 Move transfer type selector in credit/debit mode 2020-08-31 23:06:21 +02:00
e74f92cf8d Set current page as active button 2020-08-31 22:11:46 +02:00
ee26850e34 Add a line to describe superusers, remove useless roles in rights table 2020-08-31 21:49:02 +02:00
08c8792aed Fix alias deletion 2020-08-31 21:32:45 +02:00
a9da4a38e1 Order superusers by last name 2020-08-31 21:15:09 +02:00
b8c1cfba40 Display superusers in rights list 2020-08-31 21:11:00 +02:00
ca6f7cac9a Translate "lock note" messages 2020-08-31 20:32:28 +02:00
5e65e2d74a Add "Lock note" feature 2020-08-31 20:15:48 +02:00
0c753c3288 Prevent also club owners when the note balance is negative 2020-08-31 16:13:26 +02:00
1bbe7df797 API app must have no dependency 2020-08-31 00:49:41 +02:00
abbe74cc55 Add activity type "Other" 2020-08-31 00:20:09 +02:00
8744455cbe Add placeholders in activity form 2020-08-31 00:15:02 +02:00
56c41258b9 Highlight non-validated activities 2020-08-30 23:54:54 +02:00
48eb0749e0 Users can create a past activity 2020-08-30 23:14:57 +02:00
8ac551e1bc Hide activity creater if the user is not able to validate it 2020-08-30 23:10:41 +02:00
805ceda249 Don't display the alias create form if the user can't create anyone 2020-08-30 23:06:51 +02:00
a9258c332a Order note research results: match first aliases then normalized names 2020-08-30 22:33:59 +02:00
ca7f4791ed Preserve dashes in Alias normalisation 2020-08-30 17:28:36 +02:00
b454ad8dad Hide note selector when the user clicks elsewhere 2020-08-30 16:56:16 +02:00
7d539d44e5 Display form error when a permission is missing rather than display a 403 page 2020-08-30 16:23:55 +02:00
227cb2a801 Add light background to "Gift/Transfer" buttons 2020-08-30 15:49:06 +02:00
ef1e805538 Clear note selector once a consumption is performed 2020-08-30 15:18:34 +02:00
374e6ed7f8 💚 Fix CI 2020-08-30 11:59:10 +02:00
c5f40e0952 🐛 Fix entry page view 2020-08-29 23:06:50 +02:00
4cb162de87 Card for wei templates 2020-08-25 18:36:49 +02:00
bca301700d Cards for all treasury 2020-08-25 18:10:21 +02:00
5ba18a2d89 Merge branch 'front_activity' into 'beta'
Cards for activity templates

See merge request bde/nk20!100
2020-08-25 17:40:05 +02:00
22a0af640e Cards for activity templates 2020-08-25 17:39:30 +02:00
1712d1725a Update translations 2020-08-25 16:47:54 +02:00
93e5e4c8cd Merge branch 'morefront' into 'beta'
Do not list alias on profile page

See merge request bde/nk20!99
2020-08-25 16:44:55 +02:00
1fd37bb1ce Smaller membership renew btn 2020-08-25 16:44:01 +02:00
e3785e11f1 Cards everywhere in member app 2020-08-25 16:30:02 +02:00
2e659c63cd Member templates inherit from member/base.html 2020-08-25 15:39:57 +02:00
63dc184ce4 Do not list alias on profile page 2020-08-25 15:05:50 +02:00
b25935e579 When data is imported from the NK15, prevent users whenever some aliases are deleted 2020-08-24 12:41:51 +02:00
550242226e 🎨 Normalize - to _ since these characters are used a lot 2020-08-24 11:54:00 +02:00
bac14521ae Merge branch 'more_front' into 'beta'
More front

See merge request bde/nk20!97
2020-08-23 14:21:53 +02:00
e14c8734c2 Template formating on member app 2020-08-23 14:21:31 +02:00
c64de202a6 Remove title from error pages 2020-08-23 14:21:13 +02:00
44b7fe8f52 List aliases on profile page 2020-08-23 12:07:04 +02:00
cbc3e39bd6 Create base template for member and wei 2020-08-23 10:06:16 +02:00
1c16d6ef18 Use block.parent to extend content 2020-08-23 09:56:42 +02:00
e3898d0b1e Cards on error pages 2020-08-23 09:51:03 +02:00
ac0e9b9da2 Merge branch 'more_front' into 'beta'
More front

See merge request bde/nk20!96
2020-08-23 00:35:31 +02:00
0ba77fb8f0 Make alias form a bit more HTML friendly 2020-08-23 00:33:36 +02:00
342d3910c7 Set fluid container on parent template 2020-08-23 00:03:10 +02:00
2c1cf148fa Remove alias_update.html 2020-08-22 23:55:22 +02:00
196f796570 Move alias.js to local static 2020-08-22 23:54:58 +02:00
8691421ce3 Change cursor on select 2020-08-22 23:23:44 +02:00
9cad8fcc65 Simplify future user search 2020-08-22 10:13:48 +02:00
891955cedf Cards for all rights template 2020-08-22 10:01:22 +02:00
8063354e0f He's stupid Tim\! 2020-08-22 09:39:07 +02:00
f077a5d72f Merge branch 'unified_search' into 'beta'
Unified search

See merge request bde/nk20!95
2020-08-22 09:38:03 +02:00
2272cf5294 Update URL when searching 2020-08-22 09:35:55 +02:00
8465b24d7d Use base search for club list 2020-08-21 23:20:45 +02:00
aa98c4848d Create base template for search page 2020-08-21 23:11:25 +02:00
00b07147f6 Merge branch 'color_front' into 'beta'
Color front

See merge request bde/nk20!94
2020-08-21 21:07:24 +02:00
26775aa561 Clear and hide tooltips when the conso/transfer page is cleared 2020-08-21 21:00:46 +02:00
83d2c18d1e Debounce user search 2020-08-21 19:12:28 +02:00
5ea1eed76d Password reset use cards 2020-08-21 18:34:20 +02:00
b7d4a17ffd Better footer on mobile phone 2020-08-21 18:08:32 +02:00
5c3451bda7 Use cards for registration templates 2020-08-21 17:52:48 +02:00
8c46321c95 Make page title always white 2020-08-21 17:52:30 +02:00
a3af2b0d9a Hide login select arrow on firefox 2020-08-21 17:52:10 +02:00
501d02d05c Fix back to top btn 2020-08-21 15:25:39 +02:00
310f55a28e Light background on login box 2020-08-21 14:43:25 +02:00
197bd28ceb Use a fluid-container for navbar 2020-08-21 14:38:07 +02:00
e03a3f7fd2 Do not show login and register on these pages 2020-08-21 14:33:43 +02:00
51230e029d Better navbar buttons and less shadow 2020-08-21 14:21:26 +02:00
bd49a36bcc Dark background and custom navbar color 2020-08-21 13:24:04 +02:00
2672721235 Add blocks with collapse animation instead of display all 2020-08-21 08:18:00 +02:00
ba636fc401 Transfer from the Société générale is antedated 2020-08-21 07:40:27 +02:00
c090b4af76 Superusers can see their note even if they have no membership for local dev 2020-08-20 23:13:27 +02:00
a1dc8fe530 fix trailing comma 2020-08-19 23:00:49 +02:00
6ea92cdcde Merge branch 'documents' into beta 2020-08-19 13:18:12 +02:00
9c9214b5df [registration] comments 2020-08-19 13:03:36 +02:00
00935a8c02 [activity] comments on view and forms 2020-08-19 11:31:15 +02:00
b0ebc7c0a4 mv imageForm 2020-08-19 11:30:56 +02:00
60b1cdbcf8 comments member views 2020-08-18 18:19:39 +02:00
f324965f1a [note] comments view and templates 2020-08-18 14:27:04 +02:00
7c291b115a Ensure that date_end ≥ date_start in activities 2020-08-18 12:10:52 +02:00
6217f35f67 Notes are force-updated when a transaction is saved 2020-08-18 11:46:35 +02:00
448d379315 no need to disable turbolinks if we don't use select2 2020-08-18 11:45:30 +02:00
e974eaa1fe Tuple (name, date_start, date_end) must be unique for activities 2020-08-17 19:28:26 +02:00
61ace4af74 Replace timezone.now().date() by date.today() 2020-08-16 00:36:34 +02:00
b8c3dda95b Replace timezone.now().date() by date.today() 2020-08-16 00:35:13 +02:00
da23df05cb Kfet members can edit their own WEI registration 2020-08-16 00:15:33 +02:00
9c061d9837 Free the transfer lock in case of transfer error 2020-08-16 00:04:35 +02:00
5abb155287 Free the transfer lock in case of transfer error 2020-08-16 00:02:09 +02:00
9f258e39b6 Check that the user is a member of the parent club only at the creation of the membership 2020-08-15 23:52:57 +02:00
4997a37058 Ensure that the user is authenticated before that it has the permission to see page 2020-08-15 23:27:58 +02:00
b16871d925 Display a form error rather than a page error if a guest is already invited 2020-08-15 23:03:49 +02:00
1186b0f9a9 Don't serialize *_ptr fields in logs 2020-08-15 22:54:16 +02:00
5abbb84254 Permissions for activities must be more specific to prevent that anyone can validate its own activity 2020-08-15 22:24:48 +02:00
5f8c4a2857 Prevent time travelers to register in the note 2020-08-15 21:30:08 +02:00
14b969b2dd Fix link in negative balances mails 2020-08-15 21:12:16 +02:00
f95a0875db Fix link in negative balances mails 2020-08-15 21:11:02 +02:00
430036bfc2 Don't display "change password" button on other profile pages 2020-08-15 20:59:45 +02:00
d6fd925fdd Display email and phone number in profile page 2020-08-15 20:40:11 +02:00
89c15cbe3e Refresh filters to search a transaction when a source or a destination is selected 2020-08-15 20:19:34 +02:00
75cd34f5dd Enlarge buttons table and transactions table 2020-08-15 20:04:19 +02:00
6927f5fbb6 Search buttons by category or description, highlight matched words 2020-08-15 19:47:29 +02:00
482a04d37c Consumptions didn't get removed properly 2020-08-15 19:33:30 +02:00
0bf5067b60 Fix linters 2020-08-15 19:10:23 +02:00
fe2af5ac2b Pass resourcetype argument correctly when invalidating a transaction 2020-08-15 19:10:15 +02:00
d4090a4043 🎉 Use select_for_update tag to update note balances when we save a Transaction to avoid concurrency issues 2020-08-15 18:57:44 +02:00
242b85676d Floats are already formatted 2020-08-14 19:37:17 +02:00
eca4767155 Mark fields in TeX templates as safe 2020-08-14 19:35:21 +02:00
21ba46c1bc Don't escape numbers in TeX template 2020-08-14 19:16:51 +02:00
74097ecc44 "safe" template tag is not made for TeX templates, it replaces ' with &#39; but & is a special character 2020-08-14 19:13:24 +02:00
d962763987 datetime.today() => date.today() 2020-08-14 19:04:44 +02:00
a43abee00b Don't log database changes when we check a permission 2020-08-14 19:00:57 +02:00
912ce5da2e Fix the amount history in the button update page 2020-08-13 20:13:00 +02:00
29f8b9215d Fix the amount history in the button update page 2020-08-13 20:06:06 +02:00
f5f379e6ad BooleanField -> CharField (a locale name is not a boolean) 2020-08-13 19:48:15 +02:00
c50fdd6689 Move the mailing list registration to the Profile model, see #50 2020-08-13 19:43:37 +02:00
1e4cbf60c5 Display the full price of the WEI, including the BDE and the Kfet membership 2020-08-13 19:29:01 +02:00
dfe4bf2175 Register external apps in Django Admin, fix Django Admin Docs 2020-08-13 19:13:19 +02:00
a25e663a26 Use datetime.today for DateField 2020-08-13 18:54:53 +02:00
721da093e9 Don't update membership information every time 2020-08-13 18:16:26 +02:00
d98e46ffc2 Store note balances in a big integer 2020-08-13 18:04:28 +02:00
2d69e36adf Store only changed data in logs 2020-08-13 17:08:15 +02:00
bb2704323a Spam click on invalidity button is no longer possible 2020-08-13 17:04:10 +02:00
c466715e8a Raise permission denied on CreateView if you don't have the permission to create a sample instance, see #53 2020-08-13 15:20:15 +02:00
71f6436d06 More WEI tests, > 97 % coverage 2020-08-11 13:30:44 +02:00
106e97f5df Tests are better when they work (fix two tests) 2020-08-11 01:07:45 +02:00
b7a88a387c More tests in WEI app, but we can still go further 2020-08-11 01:03:29 +02:00
25e26fe8cf Don't test LaTeX pages if LaTeX is not installed 2020-08-10 23:29:11 +02:00
0fae5b3e62 Create tests for the WEI app 2020-08-10 23:18:40 +02:00
3784e97d60 Hide the credit interface when editing a WEI registration 2020-08-10 20:09:49 +02:00
6567d2f8cc When an user is registering to the WEI, it doesn't pay the membership + the credit amount. The credit amount is deducted instead 2020-08-10 19:59:01 +02:00
999cc0a6b2 Tesdt login page 2020-08-10 19:36:04 +02:00
60de58b78a Execute tests from apps/ 2020-08-10 18:43:50 +02:00
9c816a288d Stronger alias normalisation, ensure that normalized strings are encoded in ASCII. Closes #52 2020-08-10 18:36:47 +02:00
c277d8bccd Fix CI broken link in README 2020-08-10 18:27:45 +02:00
4a4c3d33b0 Format README and fix link 2020-08-10 18:26:45 +02:00
9c679d5bc9 Regen locales 2020-08-10 18:12:59 +02:00
3b49b7f4c1 Add how to translate in README 2020-08-10 18:12:50 +02:00
747a878cca Do not hover table when not clickable 2020-08-10 18:01:39 +02:00
c612e159cf See user information does not imply see the note balance 2020-08-10 16:32:45 +02:00
1b84c8c603 🐛 The balance must be greater than the *total* amount of a transaction, not the unit price 2020-08-10 16:05:50 +02:00
3a52af33a2 🍻 Make coffee, closes #54 2020-08-10 15:36:41 +02:00
ccfc1e74ac Reorder import statements of apps/activity 2020-08-10 15:30:39 +02:00
7719ff41ad Make tables responsive 2020-08-10 15:10:02 +02:00
8933fddaf3 Add comment on custom CSS 2020-08-10 15:05:00 +02:00
eadc8fa193 Fix autocompletion not showing up in rare cases 2020-08-10 14:45:44 +02:00
f74b19b2af Fix autocompletion popup flipping direction 2020-08-10 14:45:17 +02:00
c3081d9cc3 Add missing space in menu 2020-08-10 13:59:29 +02:00
8e886f1431 Remove debug banner as it does not work 2020-08-10 13:56:06 +02:00
bf7c253607 Set HTML lang depending on user locale 2020-08-10 13:50:23 +02:00
027ae5b97f Color reauth warning on login page 2020-08-10 13:49:53 +02:00
63562d3fbb Merge branch 'frontnax' into 'beta'
Frontnax

See merge request bde/nk20!89
2020-08-10 12:25:26 +02:00
bba69f0a60 Give Pikachu as an example 2020-08-10 12:09:05 +02:00
beff848796 Use a fixed-width container by default for lisibility 2020-08-10 12:08:47 +02:00
e78ba49252 Override Django Registration templates 2020-08-10 12:08:21 +02:00
ce35e8f7e8 Make the container customizable 2020-08-10 11:40:51 +02:00
50f4a43343 Custom CSS and card for login page 2020-08-10 11:31:35 +02:00
b66d6635fc Ignore only collected statics 2020-08-10 11:31:21 +02:00
9a52c81bff Simplify NGINX examples 2020-08-09 20:33:48 +02:00
48d3e8960a Do not cover virtualenv and migrations 2020-08-09 19:54:31 +02:00
f6dfbb0b6c Fix amount of \ in apps/activity/views.py 2020-08-09 19:49:11 +02:00
c6e3a57801 Reorder import in apps/treasury/admin.py 2020-08-09 19:43:21 +02:00
40b826a375 Fix hanging indent in apps/note/tables.py 2020-08-09 19:42:09 +02:00
f0089d0bc5 Remove unused django.template.loader.render_to_string import 2020-08-09 19:39:17 +02:00
5e75a56eda missing whitespace after ':' in apps/wei/forms/registration.py 2020-08-09 19:38:23 +02:00
d73f7c31a1 Define BASE_DIR in development.py 2020-08-09 19:36:11 +02:00
31f4105c9a Do not test against Py3.6 2020-08-09 19:34:01 +02:00
e9ae8531b8 Fix date-picker.html import 2020-08-09 19:16:11 +02:00
7b40ee1ca4 Reorder templates 2020-08-09 19:06:57 +02:00
53b496546d Add django-bootstrap-datepicker-plus and django-colorfield, move statics 2020-08-09 18:54:20 +02:00
8c1cf754ed Revert to NOTE_URL 2020-08-09 18:39:17 +02:00
efe833cec3 Merge production settings in base settings 2020-08-09 18:34:51 +02:00
ccfc37d226 Reorder base Django settings and read env vars 2020-08-09 17:52:19 +02:00
764eaafb95 Emails are unique. Translate mail foooters. Closes #55 #56 2020-08-09 16:38:37 +02:00
5846f03220 🐛 Last report date is a datetime, not a date 2020-08-09 15:53:50 +02:00
52e8b46aa2 🐛 Last report date is a datetime, not a date 2020-08-09 15:50:51 +02:00
29f84ea007 Remove test code 2020-08-09 15:42:07 +02:00
49bda926c6 Disable turbolinks for pages that require custom JS, like calendars or autocomplete fields 2020-08-09 15:31:38 +02:00
11fbbca2a8 Amount help text in transaction templates forms can be misleading: they type euros not cents 2020-08-09 14:52:57 +02:00
901af1a86a Use var instead of let to declare a global var (turbolinks...) 2020-08-09 14:01:59 +02:00
5f87e76be8 Fix Profile model in Django Admin 2020-08-09 13:42:37 +02:00
8c885d372b Typo, closes #57 2020-08-09 13:23:22 +02:00
255e4dd0aa Lock interfaces when a transfer is performed to prevent spam click accidents 2020-08-09 13:19:27 +02:00
4afb849aec I hate you JS 2020-08-09 12:42:25 +02:00
872456df20 🐛 Don't break the note 2020-08-09 12:31:06 +02:00
963ba05d01 🎨 set default birthday to 2001-01-01 2020-08-08 16:20:59 +02:00
18eaf4477e 🐛 timezone.now is a DateTime, not a Date 2020-08-08 15:30:33 +02:00
e4998cb6e3 [WEI] Implement WEI Survey front 2020-08-07 20:11:28 +02:00
ad59b5c81e Change help text in membership fields in club form 2020-08-07 14:20:34 +02:00
88917dde23 Some memberships were detected twice 2020-08-07 14:00:54 +02:00
aab194b987 Import Société générale credits 2020-08-07 13:19:00 +02:00
9751a5ad92 🐛 Fix pagination in transaction page 2020-08-07 12:55:07 +02:00
679ac3a652 Lock invoices, delete them 2020-08-07 11:04:54 +02:00
1fb14ea33d Store invoice source code instead of generate it everytime 2020-08-06 22:30:14 +02:00
e23eafd56c Add invoices in Django Admin 2020-08-06 21:51:53 +02:00
3e28ed8716 Remove space in IBAN 2020-08-06 21:45:50 +02:00
5c01c0bb6c Display 2 decimals in invoices 2020-08-06 21:02:25 +02:00
979628b02d 🐛 default last_report date is today, not the migration date 2020-08-06 20:30:17 +02:00
bb8e3aaccf Display what is matched in user table (experimental, normalizing is not working) 2020-08-06 20:21:35 +02:00
86ff23357c Display what is matched in user table (experimental, normalizing is not working) 2020-08-06 20:19:29 +02:00
fd2f426f55 🐛 Fix signup 2020-08-06 19:56:37 +02:00
48a7128370 📦 On a déménagé 2020-08-06 19:42:22 +02:00
f222ba134d 🐛 Remove \eaddto in the invoice template, unicode characters weren't supported 2020-08-06 19:39:40 +02:00
d95cd8c7c7 🎨 Better autocomplete field 2020-08-06 18:27:57 +02:00
5b3361f086 Product quantity must be positive 2020-08-06 18:05:58 +02:00
9c7cb07dec Improve activity interface 2020-08-06 17:41:30 +02:00
dd4b24d999 Don't match users only with the start of the name 2020-08-06 15:21:16 +02:00
eb3d426947 💩 Don't reset a transaction before saving it... 2020-08-06 15:18:14 +02:00
d8c7018b9a FP is bugggy 2020-08-06 15:08:35 +02:00
f47a0b8c9d Use neg for negative numbers in invoices 2020-08-06 14:41:14 +02:00
c859fc7821 Use neg for negative numbers in invoices 2020-08-06 14:39:01 +02:00
a4702fca86 Escape special TeX characters 2020-08-06 14:19:51 +02:00
de5e0c958e Fix some activity errors 2020-08-06 14:11:55 +02:00
434a393f3b During the beta, don't update the wiki automatically 2020-08-06 13:09:35 +02:00
cba6a35b6c Display matched alias in user table 2020-08-06 13:07:22 +02:00
0de69cbfaf 💚 Fix linters 2020-08-06 12:50:24 +02:00
d9cf812074 🐛 Prevent transactions to have the same source and destination 2020-08-06 12:46:44 +02:00
252ddb832d 🐛 Invert membership date inequalities 2020-08-06 12:36:28 +02:00
6dcb82855d 🐛 Default comment is an empty string, not None 2020-08-06 12:31:57 +02:00
1247818033 🐛 lxml is required to parse html pages 2020-08-06 12:30:14 +02:00
9439b3cb2d 🐛 Add Beautifulsoup4 as a dependency 2020-08-06 12:22:19 +02:00
a07b942738 Export activities in the Crans Wiki 2020-08-06 12:15:37 +02:00
b7ae411f96 Export activities in the Crans Wiki 2020-08-06 12:15:23 +02:00
315af75c45 Backup database daily 2020-08-06 09:27:33 +02:00
fd7e314ca3 Don't log mails in database 2020-08-06 08:53:47 +02:00
0b46140771 Cron file must be owned by root 2020-08-06 08:21:05 +02:00
547fbf564b Add cron to refresh highlighted buttons 2020-08-06 08:14:54 +02:00
2aebeb8927 respoinfo.bde => respo-info.bde 2020-08-06 08:13:44 +02:00
93f7e1d45b Fix check consistency script 2020-08-05 23:53:50 +02:00
199219861c Fix check consistency script 2020-08-05 23:51:59 +02:00
8b66bcc3d5 Fix cron 2020-08-05 23:50:30 +02:00
6759586ef3 Setup crons 2020-08-05 23:19:30 +02:00
33806967c8 Prepare weekly reports 2020-08-05 23:19:17 +02:00
24ac3ce45f Display users that have surnormal roles 2020-08-05 21:07:31 +02:00
018ca84e2d Prevent superusers when they make a transaction with a non-member user 2020-08-05 20:40:30 +02:00
2851d7764c Profile pictures are clickable 2020-08-05 19:52:36 +02:00
c205219d47 🐛 Fix transaction update concurency 2020-08-05 19:42:44 +02:00
b0398e59b8 🐛 Fix treasury 2020-08-05 18:04:01 +02:00
9c3e978a41 🐛 Fix signup 2020-08-05 16:27:44 +02:00
21f1347a60 🐛 Fix signup 2020-08-05 16:26:44 +02:00
af857d6fae 🐛 Prevent transactions where note balances go out integer bounds 2020-08-05 16:23:32 +02:00
acf7ecc4ae Use phone number validator 2020-08-05 14:14:51 +02:00
6c9cf73848 Update permissions to see our own note 2020-08-05 12:22:35 +02:00
2222175d4e Fix search transactions page 2020-08-05 11:34:23 +02:00
a096dc4427 Adhere to parent clubs automatically, adhere to Kfet automatically when registering to the WEI 2020-08-04 20:04:41 +02:00
358691aaa9 🐛 Fix french translation 2020-08-03 23:57:59 +02:00
20ce817b16 🐛 WEI members must be members of the Kfet club *this year* 2020-08-03 23:55:01 +02:00
ba067f050e Mails to be sent are added in a queue thanks to Django Mailer (todo: configure cron) 2020-08-03 20:09:16 +02:00
2a744a8610 Display invalid transactions but don't count on them in the total 2020-08-03 19:37:47 +02:00
0e8058ab0d Add script to send weekly report to all members 2020-08-03 19:35:25 +02:00
655390b265 A longer transaction history is better 2020-08-03 18:50:51 +02:00
985a5ca876 Add "search transactions page" 2020-08-03 18:49:15 +02:00
55580bc11e Merge remote-tracking branch 'origin/beta' into beta 2020-08-03 16:11:25 +02:00
5ea8d8f870 🎨 Update activity interface 2020-08-03 16:11:05 +02:00
0a2c9d9c87 🐛 Better entry page 2020-08-03 15:44:06 +02:00
208dc7f865 🎨 Use multiple checkboxes than multiple select widget 2020-08-03 13:33:25 +02:00
fbf3a0bcf6 🐛 A new user can't take an existing alias as username 2020-08-03 12:35:51 +02:00
66defee3ea 🐛 Display the invalidity reason of an invalid transaction even if we can't validate it 2020-08-03 11:41:06 +02:00
f8a4087e56 🐛 Display full registration table when the search bar is empty 2020-08-03 11:32:37 +02:00
94086505e6 Fix note balances 2020-08-03 11:16:01 +02:00
6c8843e5fc 🐛 Reset transfer form even if the note has not enough money 2020-08-03 10:54:52 +02:00
0e8174aacd 🐛 Fix objects with pk 0 2020-08-03 10:50:55 +02:00
0e3c4fcaf6 Warn users when a transaction has no source or no destination 2020-08-03 10:03:51 +02:00
58fe8914cf 🐛 Fix infinite loop in permission check 2020-08-02 22:39:30 +02:00
f870af139e Typos 2020-08-02 09:51:39 +02:00
7742358b8f Secretaries can view and add memberships 2020-08-02 09:49:45 +02:00
8de7ba14bd Add permission for secretaries 2020-08-02 09:35:32 +02:00
8497dbb25c Club members can see the club 2020-08-02 09:30:18 +02:00
f148c8dacb Better autocomplete field 2020-08-02 09:20:21 +02:00
2f018f8c9d Always query distinct objects 2020-08-02 08:57:16 +02:00
0ae61f3643 BDE memberships can start on 1st august 2020-08-02 08:47:23 +02:00
b706efe463 2A+ can change their selected bus or team if the registration is not validated 2020-08-01 23:27:07 +02:00
37dc535d6d Display only one user 2020-08-01 23:05:14 +02:00
5ccbad8359 Fix transfer form reset 2020-08-01 23:03:10 +02:00
c0cdb13130 Can't concatenate string and proxy 2020-08-01 22:30:34 +02:00
8434841ec5 Fix one permission 2020-08-01 22:28:28 +02:00
cadf981013 passer en négatif -> être en négatif 2020-08-01 21:48:18 +02:00
efc2b6b0b0 Send mail to users when the note balance is negative 2020-08-01 21:44:16 +02:00
1fd7d76412 Merge branch 'beta' into 'master'
Fix import

See merge request bde/nk20!88
2020-08-01 18:11:01 +02:00
51fbde23b9 Add note consistency check script 2020-08-01 18:06:53 +02:00
36f1a3f0d4 Activity admin site was missing 2020-08-01 18:05:31 +02:00
cc04fa5555 default=timezone.default is better than auto_now_add=True 2020-08-01 17:49:23 +02:00
f38b9801d0 Merge branch 'fix-ansible' into 'master'
Fix ansible

See merge request bde/nk20!87
2020-08-01 16:42:17 +02:00
43a22cbed4 💩 Fix account import 2020-08-01 16:37:54 +02:00
5c2df41640 Fix Ansible 2020-08-01 16:32:26 +02:00
e50bff8e14 Merge branch 'beta-soon' into 'master'
Pre-beta fixes

Closes #51

See merge request bde/nk20!86
2020-08-01 16:12:09 +02:00
b5586c647b 💚 Fix linters 2020-08-01 16:07:47 +02:00
377397b319 🐛 Fix WEI tables 2020-08-01 15:59:39 +02:00
1abb40953f Reduce header width 2020-08-01 15:47:45 +02:00
f114265662 Remove btn btn-link on non-clickable table headers 2020-08-01 15:35:45 +02:00
7c369bd264 Update the balance of the Kfet club and import creation dates 2020-08-01 15:16:50 +02:00
b6453ce03d 💄 Improve Django Admin 2020-08-01 15:13:29 +02:00
59bfdbbfc7 💄 Warn users when a note balance is bellow 0 or -50 € 2020-08-01 12:12:56 +02:00
82aa0182e3 🐛 Hide "I am the emitter" button on credit and debit interfaces 2020-08-01 10:58:21 +02:00
23b775447c 🐛 Fix email validation link 2020-08-01 10:48:17 +02:00
d7b834d908 Translate rights 2020-07-31 22:29:23 +02:00
dca655949e Improve transfer UI 2020-07-31 21:24:23 +02:00
932a546213 Better error messages 2020-07-31 19:43:03 +02:00
d8127e8936 Escape strings 2020-07-31 19:00:19 +02:00
8409ee4cc4 Display "Create WEI" button only for people that can create a WEI, see #53 2020-07-31 17:15:31 +02:00
9008baad3a Better alias research, closes #51 2020-07-31 17:07:14 +02:00
fd705adb05 Filter members 2020-07-31 17:01:52 +02:00
bd35e4e21e Separate club members in a dedicated page (WIP) 2020-07-31 13:17:16 +02:00
72dcc93136 Club managers can register new members to a club, even if they don't have the right to create a transaction 2020-07-31 09:49:43 +02:00
cb38ceb2c6 New club members have only the role "Club member" by default and no other one 2020-07-31 09:41:22 +02:00
ad19b64b3a 💚 Add Python 3.8 to Gitlab CI 2020-07-30 20:45:17 +02:00
2642ae3a1a 🐛 Scripts subproject has disappeared 2020-07-30 20:41:19 +02:00
ad2cc22964 Transactions are not invalidable if the user doesn't have the right to 2020-07-30 17:52:04 +02:00
ae629b55ad Add HTML titles 2020-07-30 17:30:21 +02:00
a5e50e5de6 Display true note name next to the alias, whenever the user has low permissions 2020-07-30 16:48:34 +02:00
9da8d49223 Remove ... when invalidating transaction 2020-07-30 16:41:41 +02:00
aa66361ac7 Update permissions to create clubs.
For now, only superusers can edit the roles of a user.
2020-07-30 16:36:44 +02:00
c14d37eaeb Fix deletion of consumptions in double consumption mode 2020-07-30 16:06:21 +02:00
e9cbc8e623 Fix linters 2020-07-30 15:53:23 +02:00
9d8c588b78 Buttons list didn't work as well 2020-07-30 15:49:59 +02:00
484560fe4b Fix emitter button 2020-07-30 15:14:13 +02:00
9361f3f2f0 Aliases should load really faster 2020-07-30 15:07:30 +02:00
e63219f7ad Force delete some objects 2020-07-30 14:58:18 +02:00
0c0aed0234 🐛 Force delete didn't work as well when trying to check add permissions 2020-07-30 13:10:03 +02:00
fb775de923 Add backdoor to login as other users (in debug mode only) 2020-07-30 12:50:48 +02:00
b49db39080 Update Nginx conf: redirect automatically requests to the right domain 2020-07-29 23:00:57 +02:00
da1063862e Merge gift + transfer interfaces 2020-07-29 22:55:12 +02:00
224ef5b2f0 Allow users to have complicated username 2020-07-29 19:37:40 +02:00
cbd36f110a Another uplicated permission 2020-07-29 19:13:29 +02:00
c9e68ca66b Duplicated permission 2020-07-29 19:12:16 +02:00
20011db37e Order aliases by name 2020-07-29 19:03:11 +02:00
5a91cac08d Add permissions to see clubs and users 2020-07-29 18:37:42 +02:00
fa9159bb28 Alias pk != Note pk 2020-07-29 17:50:14 +02:00
4549255198 Treasurers can update invalidity reason 2020-07-29 17:42:06 +02:00
750bdcb2c5 Treasurers can of course click on buttons. Fix PATCH requests on the API 2020-07-29 12:25:53 +02:00
5c93301358 Beta banner is dismissible 2020-07-29 11:42:27 +02:00
b8a88eeda4 Only staff with good permission mask can visit Django Admin 2020-07-29 11:38:59 +02:00
d455c5c533 Add banner to warn when debug mode is active 2020-07-29 10:14:57 +02:00
f597b6dbd8 Prevent creating club when there exists an alias that is similar to the name of the club 2020-07-28 23:16:38 +02:00
3a4145e4d9 Woops, roles didn't have the permissions 2020-07-28 20:56:22 +02:00
54ce157019 Store clothing cut and size in WEI registration 2020-07-28 20:49:32 +02:00
7c6bab88f4 Update permissions to see buses 2020-07-28 20:22:10 +02:00
12ebf9d12a Before checking membership roles, we wait before the creation of the membership 2020-07-28 19:38:25 +02:00
76a6260b18 SMTP user and password are None by default, add From Email tag 2020-07-28 18:59:34 +02:00
7b3512c0be Add banner for beta 2020-07-28 18:27:25 +02:00
0bfc3b9454 Not-yet-registered WEI members can see their registrations 2020-07-28 18:09:43 +02:00
84e8b02594 🐛 Calculating permissions faster 2020-07-28 15:25:08 +02:00
09027ea35e Store transactions by default 2020-07-27 00:16:28 +02:00
2f334e0707 Fix remittance import 2020-07-26 23:59:18 +02:00
e163f86f69 Section is not importable yet 2020-07-26 12:11:17 +02:00
0335a47667 Anonymize data, fix remittance import, better Ansible 2020-07-26 12:05:41 +02:00
b8d4fb9df1 🐛 Don't import only the last remittance... 2020-07-26 09:06:08 +02:00
8cde94c5f5 Import remittances 2020-07-26 01:01:21 +02:00
ab5d5a6e94 Display the associated club of the role in the permission page 2020-07-25 20:15:26 +02:00
4c29d855d2 Fix RolePermissions merge 2020-07-25 20:07:45 +02:00
55bc288deb Some roles can only be given in some clubs 2020-07-25 19:59:04 +02:00
fb5e2578af Merge Role and RolePermissions 2020-07-25 19:40:30 +02:00
0b6cb4ef19 Production is not debug 2020-07-25 19:14:27 +02:00
18bdc8044b 🐛 Minor fixes 2020-07-25 18:26:18 +02:00
f0bca69825 🐛 Minor fixes 2020-07-25 18:18:53 +02:00
b2e1777fe0 Update Django CAS Server 2020-07-25 17:44:38 +02:00
afb35d7ae0 Fix linters 2020-07-25 17:42:44 +02:00
b212bf4093 Fix linters 2020-07-25 17:42:32 +02:00
3fea17c555 🐛 Fix Ansible script 2020-07-25 17:36:37 +02:00
2eb601bd66 💥 Improve performances 2020-07-25 17:25:57 +02:00
50024dc03d 🐛 Fix NK15 import 2020-07-23 07:21:49 +02:00
e3045522d1 Bind GuestTransaction to entry rather than guest (fix OneToOneField) 2020-07-22 21:05:25 +02:00
a098f70424 Better import 2020-07-22 01:46:31 +02:00
1735ba25a8 Merge branch 'beta-soon' into 'master'
Beta soon

See merge request bde/nk20!85
2020-07-21 22:47:50 +02:00
5d70a809c2 🔧 Better Ansible script 2020-07-21 22:36:37 +02:00
4761d46696 🎨 Apply Django migrations 2020-07-15 11:32:08 +02:00
084d22d33f Install PSQL and init DB 2020-07-15 10:09:28 +02:00
3f0208a664 🐛 First fix Ansible installation 2020-07-15 09:27:11 +02:00
3dfed70eb1 💩 Use HTTPS rather than SSH to clone nk20-scripts (may be reverted later) 2020-07-15 08:25:52 +02:00
cdc053718f 🚀 Adding Ansible configuration (not tested) 2020-07-15 07:46:42 +02:00
71f6daf0e8 Add permission for treasurers to update the validation status of a transaction 2020-07-13 12:10:01 +02:00
2c7995a79e A transaction can only be created between active notes 2020-06-21 22:47:05 +02:00
ac5041f3ec Better club search bar 2020-06-21 22:27:32 +02:00
b46854e479 Rework on Docker image 2020-06-21 20:27:42 +02:00
a90eb2a6eb Merge branch 'order_table' into 'master'
order table of club for Pollion

See merge request bde/nk20!84
2020-06-02 09:43:12 +02:00
f2ac0cd8cf order table of club for Pollion 2020-06-02 09:37:25 +02:00
8d9e05929b Merge branch 'beta-soon' into 'master'
Beta soon

See merge request bde/nk20!83
2020-05-30 16:23:51 +02:00
518de596bb Update one translation 2020-05-30 16:16:31 +02:00
c8fe3435de Some lines of the base.js file mysteriously disappeared... 2020-05-30 16:14:37 +02:00
8751b3936e One button should not be displayed 2020-05-30 15:48:43 +02:00
302f9e752c Create a test to check that permission queries are well formed 2020-05-30 15:46:09 +02:00
7bda0bb31f Fix JS 2020-05-30 15:45:44 +02:00
8d704d0730 Merge branch 'beta-soon' into 'master'
Beta soon

See merge request bde/nk20!82
2020-05-29 21:47:57 +02:00
a9ccf46010 Linters 2020-05-29 21:43:24 +02:00
f567b1a343 Activity list is displayed in the right order 2020-05-29 21:37:44 +02:00
155b2df330 Fix some permissions, users can log in 2020-05-29 21:26:05 +02:00
716232e27f With distinct permissions, we don't need to check ~ 100 000 permissions to check if someone can log in 2020-05-29 21:11:51 +02:00
c62b5f935a Merge remote-tracking branch 'origin/master' into beta-soon
# Conflicts:
#	apps/scripts
2020-05-29 21:11:14 +02:00
f468c2f939 Merge branch 'import_nk15' into 'master'
Import nk15

See merge request bde/nk20!81
2020-05-25 22:09:52 +02:00
7597ad859a Merge branch 'scripts' into 'master'
Scripts

See merge request bde/nk20!80
2020-05-25 12:31:46 +02:00
640d31d200 fix install for python 3.8 2020-05-25 12:27:05 +02:00
79fbe96fe1 updates scripts 2020-05-25 12:26:33 +02:00
f7b68678d8 Prepare production, we will send mails 2020-05-08 15:59:31 +02:00
b0efe08a13 Why coding something custom when Django implements it natively? 2020-05-08 15:59:03 +02:00
4108babdb4 Permissions can be permanent 2020-05-07 21:14:36 +02:00
fdf373d1d5 Merge remote-tracking branch 'origin/master' into import_nk15
# Conflicts:
#	apps/treasury/signals.py
2020-05-07 19:01:23 +02:00
6d27193ac4 update import script 2020-04-18 16:07:45 +02:00
038ddc2ab8 remittance need preciser check (bug during import) 2020-04-18 15:59:59 +02:00
cc97948c24 clean specialTransaction 2020-04-18 15:59:06 +02:00
53748cd534 frenglish -> english 2020-04-18 15:57:11 +02:00
757 changed files with 50250 additions and 48788 deletions

5
.dockerignore Normal file
View File

@ -0,0 +1,5 @@
__pycache__
media
db.sqlite3
.tox
.coverage

View File

@ -1,6 +1,6 @@
DJANGO_APP_STAGE=dev
DJANGO_APP_STAGE=prod
# Only used in dev mode, change to "postgresql" if you want to use PostgreSQL in dev
DJANGO_DEV_STORE_METHOD=sqllite
DJANGO_DEV_STORE_METHOD=sqlite
DJANGO_DB_HOST=localhost
DJANGO_DB_NAME=note_db
DJANGO_DB_USER=note
@ -8,8 +8,16 @@ DJANGO_DB_PASSWORD=CHANGE_ME
DJANGO_DB_PORT=
DJANGO_SECRET_KEY=CHANGE_ME
DJANGO_SETTINGS_MODULE=note_kfet.settings
DOMAIN=localhost
CONTACT_EMAIL=tresorerie.bde@localhost
NOTE_URL=localhost
# Config for mails. Only used in production
NOTE_MAIL=notekfet@localhost
WEBMASTER_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=

15
.gitignore vendored
View File

@ -36,12 +36,21 @@ coverage
# Local data
secrets.py
.env
map.json
*.log
media/
backups/
/static/
/media/
/tmp/
# Virtualenv
env/
venv/
db.sqlite3
shell.nix
# Ignore migrations during first phase dev
migrations/
# ansibles customs host
ansible/host_vars/*.yaml
!ansible/host_vars/bde*
ansible/hosts

View File

@ -1,26 +1,64 @@
image: python:3.6
stages:
- test
- quality-assurance
- docs
# Also fetch submodules
variables:
GIT_SUBMODULE_STRATEGY: recursive
# Ubuntu 22.04
py310-django42:
stage: test
image: ubuntu:22.04
before_script:
- pip install tox
# Fix tzdata prompt
- ln -sf /usr/share/zoneinfo/Europe/Paris /etc/localtime && echo Europe/Paris > /etc/timezone
- >
apt-get update &&
apt-get install --no-install-recommends -y
python3-django python3-django-crispy-forms
python3-django-extensions python3-django-filters python3-django-polymorphic
python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil
python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache
python3-bs4 python3-setuptools tox texlive-xetex
script: tox -e py310-django42
py36-django22:
image: python:3.6
# Debian Bookworm
py311-django42:
stage: test
script: tox -e py36-django22
py37-django22:
image: python:3.7
stage: test
script: tox -e py37-django22
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:
image: python:3.6
stage: quality-assurance
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 = git@gitlab.crans.org:bde/nk20-scripts.git
url = https://gitlab.crans.org/bde/nk20-scripts

View File

@ -1,25 +1,27 @@
FROM python:3-buster
FROM debian:buster-backports
# Force the stdout and stderr streams to be unbuffered
ENV PYTHONUNBUFFERED 1
RUN mkdir /code
WORKDIR /code
RUN apt update && \
apt install -y gettext nginx uwsgi uwsgi-plugin-python3 && \
# Install Django, external apps, LaTeX and dependencies
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-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 && \
rm -rf /var/lib/apt/lists/*
# Install LaTeX requirements
RUN apt update && \
apt install -y texlive-latex-extra texlive-fonts-extra texlive-lang-french && \
rm -rf /var/lib/apt/lists/*
# Instal PyPI requirements
COPY requirements.txt /var/www/note_kfet/
RUN pip3 install -r /var/www/note_kfet/requirements.txt --no-cache-dir
COPY . /code/
# Copy code
WORKDIR /var/www/note_kfet
COPY . /var/www/note_kfet/
# Comment what is not needed
RUN pip install -r requirements/base.txt
RUN pip install -r requirements/cas.txt
RUN pip install -r requirements/production.txt
ENTRYPOINT ["/code/entrypoint.sh"]
EXPOSE 8000
EXPOSE 8080
ENTRYPOINT ["/var/www/note_kfet/entrypoint.sh"]

674
LICENSE
View File

@ -1,674 +0,0 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

318
README.md
View File

@ -1,72 +1,173 @@
# 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/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)
## Installation sur un serveur
## Table des matières
On supposera pour la suite que vous utilisez Debian/Ubuntu sur un serveur tout nu ou bien configuré.
- [Installation d'une instance de développement](#installation-dune-instance-de-développement)
- [Installation d'une instance de production](#installation-dune-instance-de-production)
1. Paquets nécessaires
## Installation d'une instance de développement
$ sudo apt install nginx python3 python3-pip python3-dev uwsgi
$ sudo apt install uwsgi-plugin-python3 python3-venv git acl
L'instance de développement installe la majorité des dépendances dans un environnement Python isolé.
Bien que cela permette de créer une instance sur toutes les distributions,
**cela veut dire que vos dépendances ne seront pas mises à jour automatiquement.**
La génération des factures de l'application trésorerie nécessite une installation de LaTeX suffisante :
1. **Installation des dépendances de la distribution.**
Il y a quelques dépendances qui ne sont pas trouvable dans PyPI.
On donne ci-dessous l'exemple pour une distribution basée sur Debian, mais vous pouvez facilement adapter pour ArchLinux ou autre.
$ sudo apt install texlive-latex-extra texlive-fonts-extra texlive-lang-french
```bash
$ sudo apt update
$ sudo apt install --no-install-recommends -y \
ipython3 python3-setuptools python3-venv python3-dev \
texlive-xetex gettext libjs-bootstrap4 fonts-font-awesome git
```
2. Clonage du dépot
2. **Clonage du dépot** là où vous voulez :
on se met au bon endroit :
```bash
$ git clone git@gitlab.crans.org:bde/nk20.git --recursive && cd nk20
```
$ cd /var/www/
$ mkdir note_kfet
$ sudo chown www-data:www-data note_kfet
$ sudo usermod -a -G www-data $USER
$ sudo chmod g+ws note_kfet
$ sudo setfacl -d -m "g::rwx" note_kfet
$ cd note_kfet
$ git clone git@gitlab.crans.org:bde/nk20.git .
3. Environment Virtuel
À la racine du projet:
3. **Création d'un environment de travail Python décorrélé du système.**
On n'utilise pas `--system-site-packages` ici pour ne pas avoir des clashs de versions de modules avec le système.
```bash
$ python3 -m venv env
$ source env/bin/activate
(env)$ pip3 install -r requirements/base.txt
(env)$ pip3 install -r requirements/prod.txt # uniquement en prod, nécessite un base postgres
(env)$ deactivate
$ source env/bin/activate # entrer dans l'environnement
(env)$ pip3 install -r requirements.txt
(env)$ deactivate # sortir de l'environnement
```
4. uwsgi et Nginx
4. **Variable d'environnement.**
Copier le fichier `.env_example` vers `.env` à la racine du projet et mettre à jour
ce qu'il faut.
Un exemple de conf est disponible :
5. **Migrations et chargement des données initiales.**
Pour initialiser la base de données avec de quoi travailler.
```bash
(env)$ ./manage.py collectstatic --noinput
(env)$ ./manage.py compilemessages
(env)$ ./manage.py makemigrations
(env)$ ./manage.py migrate
(env)$ ./manage.py loaddata initial
(env)$ ./manage.py createsuperuser # Création d'un⋅e utilisateur⋅rice initial
```
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
```
En mettant `0.0.0.0:8000` après `runserver`, vous rendez votre instance Django
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**.
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.
```bash
$ echo "deb http://deb.debian.org/debian buster-backports main" | sudo tee /etc/apt/sources.list.d/deb_debian_org_debian.list
```
1. **Installation des dépendances APT.**
On tire les dépendances le plus possible à partir des dépôts de Debian.
On a besoin d'un environnement LaTeX pour générer les factures.
```bash
$ sudo apt update
$ 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-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
```
2. **Clonage du dépot** dans `/var/www/note_kfet`,
```bash
$ sudo mkdir -p /var/www/note_kfet && cd /var/www/note_kfet
$ sudo chown www-data:www-data .
$ sudo chmod g+rwx .
$ sudo -u www-data git clone https://gitlab.crans.org/bde/nk20.git --recursive
```
3. **Création d'un environment de travail Python décorrélé du système.**
```bash
$ python3 -m venv env --system-site-packages
$ source env/bin/activate # entrer dans l'environnement
(env)$ pip3 install -r requirements.txt
(env)$ deactivate # sortir de l'environnement
```
4. **Pour configurer UWSGI et NGINX**, des exemples de conf sont disponibles.
**_Modifier le fichier pour être en accord avec le reste de votre config_**
```bash
$ cp nginx_note.conf_example nginx_note.conf
***Modifier le fichier pour être en accord avec le reste de votre config***
On utilise uwsgi et Nginx pour gérer le coté serveur :
$ sudo ln -sf /var/www/note_kfet/nginx_note.conf /etc/nginx/sites-enabled/
```
Si l'on a un emperor (plusieurs instance uwsgi):
```bash
$ sudo ln -sf /var/www/note_kfet/uwsgi_note.ini /etc/uwsgi/sites/
```
Sinon:
Sinon si on est dans le cas habituel :
```bash
$ sudo ln -sf /var/www/note_kfet/uwsgi_note.ini /etc/uwsgi/apps-enabled/
```
Le touch-reload est activé par défault, pour redémarrer la note il suffit donc de faire `touch uwsgi_note.ini`.
5. Base de données
5. **Base de données.** En production on utilise PostgreSQL.
En prod on utilise postgresql.
$ sudo apt-get install postgresql postgresql-contrib libpq-dev
(env)$ pip3 install psycopg2
$ sudo apt-get install postgresql postgresql-contrib
La config de la base de donnée se fait comme suit:
@ -107,7 +208,7 @@ On supposera pour la suite que vous utilisez Debian/Ubuntu sur un serveur tout n
et on renseigne des secrets et des paramètres :
DJANGO_APP_STAGE=dev # ou "prod"
DJANGO_DEV_STORE_METHOD=sqllite # ou "postgres"
DJANGO_DEV_STORE_METHOD=sqlite # ou "postgres"
DJANGO_DB_HOST=localhost
DJANGO_DB_NAME=note_db
DJANGO_DB_USER=note
@ -115,93 +216,100 @@ On supposera pour la suite que vous utilisez Debian/Ubuntu sur un serveur tout n
DJANGO_DB_PORT=
DJANGO_SECRET_KEY=CHANGE_ME
DJANGO_SETTINGS_MODULE="note_kfet.settings
DOMAIN=localhost # note.example.com
NOTE_URL=localhost # URL où accéder à la note
CONTACT_EMAIL=tresorerie.bde@localhost
NOTE_URL=localhost # serveur cas note.example.com si auto-hébergé.
NOTE_MAIL=notekfet@localhost # Adresse expéditrice des mails
WEBMASTER_MAIL=notekfet@localhost # Adresse sur laquelle contacter les webmasters de la note
# Le reste n'est utile qu'en production, pour configurer l'envoi des mails
NOTE_MAIL=notekfet@localhost
EMAIL_HOST=smtp.localhost
EMAIL_PORT=25
EMAIL_USER=notekfet@localhost
EMAIL_PASSWORD=CHANGE_ME
WIKI_USER=NoteKfet2020
WIKI_PASSWORD=CHANGE_ME
Ensuite on (re)bascule dans l'environement virtuel et on lance les migrations
$ source /env/bin/activate
(env)$ ./manage.py check # pas de bêtise qui traine
(env)$ ./manage.py makemigrations
(env)$ ./manage.py migrate
7. Enjoy
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`).
## Installer avec Docker
8. *Enjoy \o/*
### Installation avec Docker
Il est possible de travailler sur une instance Docker.
1. Cloner le dépôt là où vous voulez :
Pour construire l'image Docker `nk20`,
$ git clone git@gitlab.crans.org:bde/nk20.git
```
git clone https://gitlab.crans.org/bde/nk20/ --recursive && cd nk20
docker build . -t nk20
```
2. Copiez le fichier `.env_example` à la racine du projet vers le fichier `.env`,
et mettez à jour vos variables d'environnement
Ensuite pour lancer la note Kfet en tant que vous (option `-u`),
l'exposer sur son port 80 (option `-p`) et monter le code en écriture (option `-v`),
3. Dans le fichier `docker_compose.yml`, qu'on suppose déjà configuré,
ajouter les lignes suivantes, en les adaptant à la configuration voulue :
```
docker run -it --rm -u $(id -u):$(id -g) -v "$(pwd):/var/www/note_kfet/" -p 80:8080 nk20
```
Si vous souhaitez lancer une commande spéciale, vous pouvez l'ajouter à la fin, par exemple,
```
docker run -it --rm -u $(id -u):$(id -g) -v "$(pwd):/var/www/note_kfet/" -p 80:8080 nk20 python3 ./manage.py createsuperuser
```
#### Avec Docker Compose
On vous conseilles de faire un fichier d'environnement `.env` en prenant exemple sur `.env_example`.
Pour par exemple utiliser le Docker de la note Kfet avec Traefik pour réaliser le HTTPS,
```YAML
nk20:
build: /chemin/vers/nk20
build: /chemin/vers/le/code/nk20
volumes:
- /chemin/vers/nk20:/code/
env_file: /chemin/vers/nk20/.env
- /chemin/vers/le/code/nk20:/var/www/note_kfet/
env_file: /chemin/vers/le/code/nk20/.env
restart: always
labels:
- traefik.domain=ndd.example.com
- traefik.frontend.rule=Host:ndd.example.com
- traefik.port=8000
3. Enjoy :
$ docker-compose up -d nk20
## Installer un serveur de développement
Avec `./manage.py runserver` il est très rapide de mettre en place
un serveur de développement par exemple sur son ordinateur.
1. Cloner le dépôt là où vous voulez :
$ git clone git@gitlab.crans.org:bde/nk20.git && cd nk20
2. Créer un environnement Python isolé
pour ne pas interférer avec les versions de paquets systèmes :
$ python3 -m venv venv
$ source venv/bin/activate
(env)$ pip install -r requirements/base.txt
3. Copier le fichier `.env_example` vers `.env` à la racine du projet et mettre à jour
ce qu'il faut
4. Migrations et chargement des données initiales :
(env)$ ./manage.py makemigrations
(env)$ ./manage.py migrate
(env)$ ./manage.py loaddata initial
5. Créer un super-utilisateur :
(env)$ ./manage.py createsuperuser
6. Enjoy :
(env)$ ./manage.py runserver 0.0.0.0:8000
En mettant `0.0.0.0:8000` après `runserver`, vous rendez votre instance Django
accessible depuis l'ensemble de votre réseau, pratique pour tester le rendu
de la note sur un téléphone !
## Cahier des Charges
Il est disponible [ici](https://wiki.crans.org/NoteKfet/NoteKfet2018/CdC).
- "traefik.http.routers.nk20.rule=Host(`ndd.example.com`)"
- "traefik.http.services.nk20.loadbalancer.server.port=8080"
```
## Documentation
La documentation est générée par django et son module admindocs.
Le cahier des charges initial est disponible [sur le Wiki Crans](https://wiki.crans.org/NoteKfet/NoteKfet2018/CdC).
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 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.
De plus, il faut aussi extraire les variables des fichiers JavaScript.
```bash
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
python3 manage.py compilemessages
python3 manage.py compilejsmessages
```

14
ansible/ansible.cfg Normal file
View File

@ -0,0 +1,14 @@
[defaults]
inventory = ./hosts
timeout = 42
[privilege_escalation]
become = True
become_ask_pass = True
[ssh_connection]
pipelining = True
retries = 3
[diff]
always = yes

19
ansible/base.yml Executable file
View File

@ -0,0 +1,19 @@
#!/usr/bin/env ansible-playbook
---
- hosts: all
vars_prompt:
- name: DB_PASSWORD
prompt: "Password of the database (leave it blank to skip database init)"
private: yes
vars:
mirror: eclats.crans.org
roles:
- 1-apt-basic
- 2-nk20
- 3-pip
- 4-certbot
- 5-nginx
- 6-psql
- 7-postinstall
- 8-docs

View File

@ -0,0 +1,7 @@
---
note:
server_name: note-dev.crans.org
git_branch: beta
serve_static: false
cron_enabled: false
email: notekfet2020@lists.crans.org

View File

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

8
ansible/hosts_example Normal file
View File

@ -0,0 +1,8 @@
[dev]
bde-note-dev.adh.crans.org
[prod]
bde-note.adh.crans.org
[all:vars]
ansible_python_interpreter=/usr/bin/python3

View File

@ -0,0 +1,54 @@
---
- 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
install_recommends: false
name:
# Common tools
- gettext
- git
- ipython3
# Front-end dependencies
- fonts-font-awesome
- libjs-bootstrap4
# Python dependencies
- python3-babel
- python3-bs4
- python3-django
- 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
- python3-psycopg2
- python3-setuptools
- python3-venv
# LaTeX (PDF generation)
- texlive-xetex
# Cache server
- memcached
# WSGI server
- uwsgi
- uwsgi-plugin-python3
register: pkg_result
retries: 3
until: pkg_result is succeeded

View File

@ -0,0 +1,48 @@
---
- name: Create note_kfet dir with good permissions
file:
path: /var/www/note_kfet
state: directory
owner: www-data
group: www-data
mode: u=rwx,g=rwxs,o=rx
- name: Clone Note Kfet
git:
repo: https://gitlab.crans.org/bde/nk20.git
dest: /var/www/note_kfet
version: "{{ note.git_branch }}"
force: true
- name: Use default env vars (should be updated!)
template:
src: "env.j2"
dest: "/var/www/note_kfet/.env"
mode: 0644
force: false
- name: Update permissions for note_kfet dir
file:
path: /var/www/note_kfet
state: directory
recurse: yes
owner: www-data
group: www-data
- name: Setup cron jobs
when: "note.cron_enabled"
template:
src: note.cron.j2
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

@ -0,0 +1 @@
../../../../.env_example

View File

@ -0,0 +1 @@
../../../../note.cron

View File

@ -0,0 +1,8 @@
---
- name: Install PIP basic dependencies
pip:
requirements: /var/www/note_kfet/requirements.txt
virtualenv: /var/www/note_kfet/env
virtualenv_command: /usr/bin/python3 -m venv
virtualenv_site_packages: true
become_user: www-data

View File

@ -0,0 +1,40 @@
---
- name: Install basic APT packages
apt:
update_cache: true
name:
- certbot
- python3-certbot-nginx
register: pkg_result
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
state: directory
- name: Add Certbot configuration
template:
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

@ -0,0 +1,20 @@
{{ ansible_managed | comment }}
# To generate the certificate, please use the following command
# certbot --config /etc/letsencrypt/conf.d/nk20.ini certonly
# Use a 4096 bit RSA key instead of 2048
rsa-key-size = 4096
# Always use the staging/testing server
# server = https://acme-staging.api.letsencrypt.org/directory
# Uncomment and update to register with the specified e-mail address
email = {{ note.email }}
# Uncomment to use a text interface instead of ncurses
text = True
# Use DNS-01 challenge
authenticator = nginx

View File

@ -0,0 +1,44 @@
---
- name: Install NGINX
apt:
name: nginx
register: pkg_result
retries: 3
until: pkg_result is succeeded
- name: Copy conf of Nginx
template:
src: "nginx_note.conf"
dest: /etc/nginx/sites-available/nginx_note.conf
mode: 0644
owner: www-data
group: www-data
- name: Enable Nginx site
file:
src: /etc/nginx/sites-available/nginx_note.conf
dest: /etc/nginx/sites-enabled/nginx_note.conf
owner: www-data
group: www-data
state: link
- name: Disable default Nginx site
file:
dest: /etc/nginx/sites-enabled/default
state: absent
- name: Copy conf of UWSGI
file:
src: /var/www/note_kfet/uwsgi_note.ini
dest: /etc/uwsgi/apps-enabled/uwsgi_note.ini
state: link
- name: Reload Nginx
systemd:
name: nginx
state: reloaded
- name: Restart UWSGI
systemd:
name: uwsgi
state: restarted

View File

@ -0,0 +1,69 @@
# the upstream component nginx needs to connect to
upstream note {
server unix:///var/www/note_kfet/note_kfet.sock; # file socket
}
# Redirect HTTP to nk20 HTTPS
server {
listen 80 default_server;
listen [::]:80 default_server;
location / {
return 301 https://{{ note.server_name }}$request_uri;
}
}
# Redirect all HTTPS to nk20 HTTPS
server {
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
location / {
return 301 https://{{ note.server_name }}$request_uri;
}
ssl_certificate /etc/letsencrypt/live/{{ note.server_name }}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/{{ note.server_name }}/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}
# configuration of the server
server {
listen 443 ssl;
listen [::]:443 ssl;
# the port your site will be served on
# the domain name it will serve for
server_name {{ note.server_name }}; # substitute your machine's IP address or FQDN
charset utf-8;
# 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
}
location /static {
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;
include /etc/nginx/uwsgi_params;
}
ssl_certificate /etc/letsencrypt/live/{{ note.server_name }}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/{{ note.server_name }}/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}

View File

@ -0,0 +1,25 @@
---
- name: Install PostgreSQL APT packages
apt:
update_cache: true
name:
- postgresql
- postgresql-contrib
- libpq-dev
register: pkg_result
retries: 3
until: pkg_result is succeeded
- name: Create role note
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|length >0
postgresql_db:
name: note_db
owner: note
become_user: postgres

View File

@ -0,0 +1,30 @@
---
- 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:
chdir: /var/www/note_kfet
become_user: postgres
- name: Compile messages
command: /var/www/note_kfet/env/bin/python manage.py compilemessages
args:
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

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,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
from django.contrib import admin
from note_kfet.admin import admin_site
from .models import Activity, ActivityType, Guest
from .forms import GuestForm
from .models import Activity, ActivityType, Entry, Guest, Opener
@admin.register(Activity, site=admin_site)
class ActivityAdmin(admin.ModelAdmin):
"""
Admin customisation for Activity
@ -19,6 +22,7 @@ class ActivityAdmin(admin.ModelAdmin):
ordering = ['-date_start']
@admin.register(ActivityType, site=admin_site)
class ActivityTypeAdmin(admin.ModelAdmin):
"""
Admin customisation for ActivityType
@ -26,7 +30,26 @@ class ActivityTypeAdmin(admin.ModelAdmin):
list_display = ('name', 'can_invite', 'guest_entry_fee')
# Register your models here.
admin.site.register(Activity, ActivityAdmin)
admin.site.register(ActivityType, ActivityTypeAdmin)
admin.site.register(Guest)
@admin.register(Guest, site=admin_site)
class GuestAdmin(admin.ModelAdmin):
"""
Admin customisation for Guest
"""
list_display = ('last_name', 'first_name', 'school', 'activity', 'inviter')
form = GuestForm
@admin.register(Entry, site=admin_site)
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 ActivityType, Activity, Guest, Entry, 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, GuestViewSet, EntryViewSet
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 django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import SearchFilter
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.response import Response
from rest_framework import status
from .serializers import ActivityTypeSerializer, ActivitySerializer, GuestSerializer, EntrySerializer
from ..models import ActivityType, Activity, Guest, Entry
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

@ -4,8 +4,9 @@
"pk": 1,
"fields": {
"name": "Pot",
"manage_entries": true,
"can_invite": true,
"guest_entry_fee": 500
"guest_entry_fee": 1000
}
},
{
@ -13,8 +14,39 @@
"pk": 2,
"fields": {
"name": "Soir\u00e9e de club",
"manage_entries": false,
"can_invite": false,
"guest_entry_fee": 0
}
},
{
"model": "activity.activitytype",
"pk": 3,
"fields": {
"name": "Autre",
"manage_entries": false,
"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,21 +1,50 @@
# 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, datetime
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 NoteUser, Note
from note_kfet.inputs import DateTimePickerInput, Autocomplete
from note.models import Note, NoteUser
from note_kfet.inputs import Autocomplete
from note_kfet.middlewares import get_current_request
from permission.backends import PermissionBackend
from .models import Activity, Guest
class ActivityForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# By default, the Kfet club is attended
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_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"]
if date_end < date_start:
self.add_error("date_end", _("The end date must be after the start date."))
return date_end
class Meta:
model = Activity
exclude = ('creater', 'valid', 'open', )
exclude = ('creater', 'valid', 'open', 'opener', )
widgets = {
"organizer": Autocomplete(
model=Club,
@ -39,9 +68,18 @@ class ActivityForm(forms.ModelForm):
class GuestForm(forms.ModelForm):
def clean(self):
"""
Someone can be invited as a Guest to an Activity if:
- the activity has not already started.
- the activity is validated.
- the Guest has not already been invited more than 5 times.
- the Guest is already invited.
- the inviter already invited 3 peoples.
"""
cleaned_data = super().clean()
if self.activity.date_start > datetime.now():
if timezone.now() > timezone.localtime(self.activity.date_start):
self.add_error("inviter", _("You can't invite someone once the activity is started."))
if not self.activity.valid:
@ -50,26 +88,26 @@ class GuestForm(forms.ModelForm):
one_year = timedelta(days=365)
qs = Guest.objects.filter(
first_name=cleaned_data["first_name"],
last_name=cleaned_data["last_name"],
first_name__iexact=cleaned_data["first_name"],
last_name__iexact=cleaned_data["last_name"],
activity__date_start__gte=self.activity.date_start - one_year,
)
if len(qs) >= 5:
if qs.filter(entry__isnull=False).count() >= 5:
self.add_error("last_name", _("This person has been already invited 5 times this year."))
qs = qs.filter(activity=self.activity)
if qs.exists():
self.add_error("last_name", _("This person is already invited."))
qs = Guest.objects.filter(inviter=cleaned_data["inviter"], activity=self.activity)
if len(qs) >= 3:
if "inviter" in cleaned_data:
if Guest.objects.filter(inviter=cleaned_data["inviter"], activity=self.activity).count() >= 3:
self.add_error("inviter", _("You can't invite more than 3 people to this activity."))
return cleaned_data
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,69 @@
# Generated by Django 2.2.16 on 2020-09-04 21:41
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Activity',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='name')),
('description', models.TextField(verbose_name='description')),
('location', models.CharField(blank=True, default='', help_text='Place where the activity is organized, eg. Kfet.', max_length=255, verbose_name='location')),
('date_start', models.DateTimeField(verbose_name='start date')),
('date_end', models.DateTimeField(verbose_name='end date')),
('valid', models.BooleanField(default=False, verbose_name='valid')),
('open', models.BooleanField(default=False, verbose_name='open')),
],
options={
'verbose_name': 'activity',
'verbose_name_plural': 'activities',
},
),
migrations.CreateModel(
name='ActivityType',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='name')),
('manage_entries', models.BooleanField(default=False, help_text='Enable the support of entries for this activity.', verbose_name='manage entries')),
('can_invite', models.BooleanField(default=False, verbose_name='can invite')),
('guest_entry_fee', models.PositiveIntegerField(default=0, verbose_name='guest entry fee')),
],
options={
'verbose_name': 'activity type',
'verbose_name_plural': 'activity types',
},
),
migrations.CreateModel(
name='Entry',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='entry time')),
],
options={
'verbose_name': 'entry',
'verbose_name_plural': 'entries',
},
),
migrations.CreateModel(
name='Guest',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('last_name', models.CharField(max_length=255, verbose_name='last name')),
('first_name', models.CharField(max_length=255, verbose_name='first name')),
],
options={
'verbose_name': 'guest',
'verbose_name_plural': 'guests',
},
),
]

View File

@ -0,0 +1,89 @@
# Generated by Django 2.2.16 on 2020-09-04 21:41
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('activity', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('member', '0001_initial'),
('note', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='GuestTransaction',
fields=[
('transaction_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='note.Transaction')),
('entry', models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, to='activity.Entry')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('note.transaction',),
),
migrations.AddField(
model_name='guest',
name='activity',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='activity.Activity'),
),
migrations.AddField(
model_name='guest',
name='inviter',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='guests', to='note.NoteUser', verbose_name='inviter'),
),
migrations.AddField(
model_name='entry',
name='activity',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='entries', to='activity.Activity', verbose_name='activity'),
),
migrations.AddField(
model_name='entry',
name='guest',
field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.PROTECT, to='activity.Guest'),
),
migrations.AddField(
model_name='entry',
name='note',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='note.NoteUser', verbose_name='note'),
),
migrations.AddField(
model_name='activity',
name='activity_type',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='activity.ActivityType', verbose_name='type'),
),
migrations.AddField(
model_name='activity',
name='attendees_club',
field=models.ForeignKey(help_text='Club that is authorized to join the activity. Mostly the Kfet club.', on_delete=django.db.models.deletion.PROTECT, related_name='+', to='member.Club', verbose_name='attendees club'),
),
migrations.AddField(
model_name='activity',
name='creater',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='user'),
),
migrations.AddField(
model_name='activity',
name='organizer',
field=models.ForeignKey(help_text='Club that organizes the activity. The entry fees will go to this club.', on_delete=django.db.models.deletion.PROTECT, related_name='+', to='member.Club', verbose_name='organizer'),
),
migrations.AlterUniqueTogether(
name='guest',
unique_together={('activity', 'last_name', 'first_name')},
),
migrations.AlterUniqueTogether(
name='entry',
unique_together={('activity', 'note', 'guest')},
),
migrations.AlterUniqueTogether(
name='activity',
unique_together={('name', 'date_start', 'date_end')},
),
]

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,13 +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, datetime
import os
from datetime import timedelta
from threading import Thread
from django.conf import settings
from django.contrib.auth.models import User
from django.db import models
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, Note
from rest_framework.exceptions import ValidationError
from note.models import NoteUser, Transaction
class ActivityType(models.Model):
@ -23,11 +28,21 @@ class ActivityType(models.Model):
verbose_name=_('name'),
max_length=255,
)
manage_entries = models.BooleanField(
verbose_name=_('manage entries'),
help_text=_('Enable the support of entries for this activity.'),
default=False,
)
can_invite = models.BooleanField(
verbose_name=_('can invite'),
default=False,
)
guest_entry_fee = models.PositiveIntegerField(
verbose_name=_('guest entry fee'),
default=0,
)
class Meta:
@ -51,6 +66,16 @@ class Activity(models.Model):
description = models.TextField(
verbose_name=_('description'),
blank=True,
default="",
)
location = models.CharField(
verbose_name=_('location'),
max_length=255,
blank=True,
default="",
help_text=_("Place where the activity is organized, eg. Kfet."),
)
activity_type = models.ForeignKey(
@ -71,6 +96,7 @@ class Activity(models.Model):
on_delete=models.PROTECT,
related_name='+',
verbose_name=_('organizer'),
help_text=_("Club that organizes the activity. The entry fees will go to this club."),
)
attendees_club = models.ForeignKey(
@ -78,6 +104,7 @@ class Activity(models.Model):
on_delete=models.PROTECT,
related_name='+',
verbose_name=_('attendees club'),
help_text=_("Club that is authorized to join the activity. Mostly the Kfet club."),
)
date_start = models.DateTimeField(
@ -101,6 +128,31 @@ class Activity(models.Model):
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):
"""
Update the activity wiki page each time the activity is updated (validation, change description, ...)
"""
if self.date_end < self.date_start:
raise ValidationError(_("The end date must be after the start date."))
ret = super().save(*args, **kwargs)
if not settings.DEBUG and self.pk and "scripts" in settings.INSTALLED_APPS:
def refresh_activities():
from scripts.management.commands.refresh_activities import Command as RefreshActivitiesCommand
# Consider that we can update the wiki iff the WIKI_PASSWORD env var is not empty
RefreshActivitiesCommand.refresh_human_readable_wiki_page("Modification de l'activité " + self.name,
False, os.getenv("WIKI_PASSWORD"))
RefreshActivitiesCommand.refresh_raw_wiki_page("Modification de l'activité " + self.name,
False, os.getenv("WIKI_PASSWORD"))
Thread(daemon=True, target=refresh_activities).start()\
if settings.DATABASES["default"]["ENGINE"] == 'django.db.backends.postgresql' else refresh_activities()
return ret
class Entry(models.Model):
@ -118,7 +170,7 @@ class Entry(models.Model):
)
time = models.DateTimeField(
auto_now_add=True,
default=timezone.now,
verbose_name=_("entry time"),
)
@ -139,11 +191,18 @@ class Entry(models.Model):
verbose_name = _("entry")
verbose_name_plural = _("entries")
def save(self, *args, **kwargs):
def __str__(self):
return _("Entry for {guest}, invited by {note} to the activity {activity}").format(
guest=str(self.guest), note=str(self.note), activity=str(self.activity)) if self.guest \
else _("Entry for {note} to the activity {activity}").format(
guest=str(self.guest), note=str(self.note), activity=str(self.activity))
@transaction.atomic
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
@ -163,7 +222,7 @@ class Entry(models.Model):
amount=self.activity.activity_type.guest_entry_fee,
reason="Invitation " + self.activity.name + " " + self.guest.first_name + " " + self.guest.last_name,
valid=True,
guest=self.guest,
entry=self,
).save()
return ret
@ -189,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,
@ -196,6 +260,43 @@ class Guest(models.Model):
verbose_name=_("inviter"),
)
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):
one_year = timedelta(days=365)
if not force_insert:
if timezone.now() > timezone.localtime(self.activity.date_start):
raise ValidationError(_("You can't invite someone once the activity is started."))
if not self.activity.valid:
raise ValidationError(_("This activity is not validated yet."))
qs = Guest.objects.filter(
first_name__iexact=self.first_name,
last_name__iexact=self.last_name,
activity__date_start__gte=self.activity.date_start - one_year,
)
if qs.filter(entry__isnull=False).count() >= 5:
raise ValidationError(_("This person has been already invited 5 times this year."))
qs = qs.filter(activity=self.activity)
if qs.exists():
raise ValidationError(_("This person is already invited."))
qs = Guest.objects.filter(inviter=self.inviter, activity=self.activity)
if qs.count() >= 3:
raise ValidationError(_("You can't invite more than 3 people to this activity."))
return super().save(force_insert, force_update, using, update_fields)
@property
def has_entry(self):
try:
@ -205,46 +306,41 @@ class Guest(models.Model):
except AttributeError:
return False
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
one_year = timedelta(days=365)
if not force_insert:
if self.activity.date_start > datetime.now():
raise ValidationError(_("You can't invite someone once the activity is started."))
if not self.activity.valid:
raise ValidationError(_("This activity is not validated yet."))
qs = Guest.objects.filter(
first_name=self.first_name,
last_name=self.last_name,
activity__date_start__gte=self.activity.date_start - one_year,
)
if len(qs) >= 5:
raise ValidationError(_("This person has been already invited 5 times this year."))
qs = qs.filter(activity=self.activity)
if qs.exists():
raise ValidationError(_("This person is already invited."))
qs = Guest.objects.filter(inviter=self.inviter, activity=self.activity)
if len(qs) >= 3:
raise ValidationError(_("You can't invite more than 3 people to this activity."))
return super().save(force_insert, force_update, using, update_fields)
class Meta:
verbose_name = _("guest")
verbose_name_plural = _("guests")
unique_together = ("activity", "last_name", "first_name", )
class GuestTransaction(Transaction):
guest = models.OneToOneField(
Guest,
entry = models.OneToOneField(
Entry,
on_delete=models.PROTECT,
)
@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,19 @@
# 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.html import format_html
import datetime
from django.utils import timezone
from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
from note_kfet.middlewares import get_current_request
import django_tables2 as tables
from django_tables2 import A
from permission.backends import PermissionBackend
from note.templatetags.pretty_money import pretty_money
from .models import Activity, Guest, Entry
from .models import Activity, Entry, Guest, Opener
class ActivityTable(tables.Table):
@ -20,6 +26,11 @@ class ActivityTable(tables.Table):
attrs = {
'class': 'table table-condensed table-striped table-hover'
}
row_attrs = {
'class': lambda record: 'bg-success' if record.open else ('' if record.valid else 'bg-warning'),
'title': lambda record: _("The activity is currently open.") if record.open else
('' if record.valid else _("The validation of the activity is pending.")),
}
model = Activity
template_name = 'django_tables2/bootstrap4.html'
fields = ('name', 'activity_type', 'organizer', 'attendees_club', 'date_start', 'date_end', )
@ -28,31 +39,27 @@ class ActivityTable(tables.Table):
class GuestTable(tables.Table):
inviter = tables.LinkColumn(
'member:user_detail',
args=[A('inviter.user.pk'), ],
args=[A('inviter__user__pk'), ],
)
entry = tables.Column(
empty_values=(),
attrs={
"td": {
"class": lambda record: "" if record.has_entry else "validate btn btn-danger",
"onclick": lambda record: "" if record.has_entry else "remove_guest(" + str(record.pk) + ")"
}
}
verbose_name=_("Remove"),
)
class Meta:
attrs = {
'class': 'table table-condensed table-striped table-hover'
'class': 'table table-condensed table-striped'
}
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 _("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):
@ -66,8 +73,15 @@ def get_row_class(record):
qs = Entry.objects.filter(note=record.note, activity=record.activity, guest=None)
if qs.exists():
c += " table-success"
elif not record.note.user.memberships.filter(club=record.activity.attendees_club,
date_start__lte=timezone.now(),
date_end__gte=timezone.now()).exists():
c += " table-info"
elif record.note.balance < 0:
c += " table-danger"
# MODE VIEUXCON=ON
if (datetime.datetime.utcnow().timestamp() - record.note.created_at.timestamp()) > 3600 * 24 * 365 * 2.5:
c += " font-weight-bold underline"
return c
@ -86,7 +100,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):
@ -106,3 +120,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

@ -0,0 +1,99 @@
{% extends "base.html" %}
{% comment %}
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">
{% trans "Guests list" %}
</h3>
<div id="guests_table">
{% render_table guests %}
</div>
</div>
{% endif %}
{% 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({
url:"/api/activity/guest/" + guest_id + "/",
method:"DELETE",
headers: {"X-CSRFTOKEN": CSRF_TOKEN}
})
.done(function() {
addMsg('{% trans "Guest deleted" %}', 'success');
$("#guests_table").load(location.pathname + " #guests_table");
})
.fail(function(xhr, textStatus, error) {
errMsg(xhr.responseJSON);
});
}
$("#open_activity").click(function() {
$.ajax({
url: "/api/activity/activity/{{ activity.pk }}/",
type: "PATCH",
dataType: "json",
headers: {
"X-CSRFTOKEN": CSRF_TOKEN
},
data: {
open: {{ activity.open|yesno:'false,true' }}
}
}).done(function () {
reloadWithTurbolinks();
}).fail(function (xhr) {
errMsg(xhr.responseJSON);
});
});
$("#validate_activity").click(function () {
$.ajax({
url: "/api/activity/activity/{{ activity.pk }}/",
type: "PATCH",
dataType: "json",
headers: {
"X-CSRFTOKEN": CSRF_TOKEN
},
data: {
valid: {{ activity.valid|yesno:'false,true' }}
}
}).done(function () {
reloadWithTurbolinks();
}).fail(function (xhr) {
errMsg(xhr.responseJSON);
});
});
</script>
{% endblock %}

View File

@ -0,0 +1,171 @@
{% extends "base.html" %}
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load static i18n pretty_money perms %}
{% load render_table from django_tables2 %}
{% block content %}
<h1 class="text-white">{{ title }}</h1>
<div class="row">
<div class="col-xl-12">
<div class="btn-group btn-group-toggle bg-light" style="width: 100%">
<a href="{% url "note:transfer" %}#transfer" class="btn btn-sm btn-outline-primary">
{% trans "Transfer" %}
</a>
{% if "note.notespecial"|not_empty_model_list %}
<a href="{% url "note:transfer" %}#credit" class="btn btn-sm btn-outline-primary">
{% trans "Credit" %}
</a>
<a href="{% url "note:transfer" %}#debit" class="btn btn-sm btn-outline-primary">
{% trans "Debit" %}
</a>
{% endif %}
{% for a in activities_open %}
<a href="{% url "activity:activity_entry" pk=a.pk %}"
class="btn btn-sm btn-outline-primary{% if a.pk == activity.pk %} active{% endif %}">
{% trans "Entries" %} {{ a.name }}
</a>
{% endfor %}
</div>
</div>
</div>
<hr>
<a href="{% url "activity:activity_detail" pk=activity.pk %}">
<button class="btn btn-light">{% trans "Return to activity page" %}</button>
</a>
<input id="alias" type="text" class="form-control" placeholder="Nom/note ...">
<hr>
<div class="card" id="entry_table">
<h2 class="text-center">{{ entries.count }}
{% if entries.count >= 2 %}{% trans "entries" %}{% else %}{% trans "entry" %}{% endif %}</h2>
{% render_table table %}
</div>
{% endblock %}
{% block extrajavascript %}
<script>
old_pattern = null;
alias_obj = $("#alias");
function reloadTable(force = false) {
let pattern = alias_obj.val();
if ((pattern === old_pattern || pattern === "") && !force)
return;
$("#entry_table").load(location.pathname + "?search=" + pattern.replace(" ", "%20") + " #entry_table", init);
refreshBalance();
}
alias_obj.keyup(function(event) {
let code = event.originalEvent.keyCode
if (65 <= code <= 122 || code === 13) {
debounce(reloadTable)()
}
});
$(document).ready(init);
function init() {
$(".table-row").click(function (e) {
let target = e.target.parentElement;
target = $("#" + target.id);
let type = target.attr("data-type");
let id = target.attr("data-id");
let last_name = target.attr("data-last-name");
let first_name = target.attr("data-first-name");
if (type === "membership") {
$.post("/api/activity/entry/?format=json", {
csrfmiddlewaretoken: CSRF_TOKEN,
activity: {{ activity.id }},
note: id,
guest: null
}).done(function () {
if (target.hasClass("table-info"))
addMsg(
"{% trans "Entry done, but caution: the user is not a Kfet member." %}",
"warning", 10000);
else
addMsg("Entry made!", "success", 4000);
reloadTable(true);
}).fail(function (xhr) {
errMsg(xhr.responseJSON, 4000);
});
} else {
let line_obj = $("#buttons_guest_" + id);
if (line_obj.length || target.attr('class').includes("table-success")) {
line_obj.remove();
return;
}
let tr = "<tr class='text-center'>" +
"<td id='buttons_guest_" + id + "' style='table-danger center' colspan='5'>" +
"<button id='transaction_guest_" + id +
"' class='btn btn-secondary'>Payer avec la note de l'hôte</button> " +
"<button id='transaction_guest_" + id +
"_especes' class='btn btn-secondary'>Payer en espèces</button> " +
"<button id='transaction_guest_" + id +
"_cb' class='btn btn-secondary'>Payer en CB</button></td>" +
"<tr>";
$(tr).insertAfter(target);
let makeTransaction = function () {
$.post("/api/activity/entry/?format=json", {
csrfmiddlewaretoken: CSRF_TOKEN,
activity: {{ activity.id }},
note: target.attr("data-inviter"),
guest: id
}).done(function () {
if (target.hasClass("table-info"))
addMsg(
"{% trans "Entry done, but caution: the user is not a Kfet member." %}",
"warning", 10000);
else
addMsg("{% trans "Entry done!" %}", "success", 4000);
reloadTable(true);
}).fail(function (xhr) {
errMsg(xhr.responseJSON, 4000);
});
};
let credit = function (credit_id, credit_name) {
return function () {
$.post("/api/note/transaction/transaction/", {
"csrfmiddlewaretoken": CSRF_TOKEN,
"quantity": 1,
"amount": {{ activity.activity_type.guest_entry_fee }},
"reason": "Crédit " + credit_name +
" (invitation {{ activity.name }})",
"valid": true,
"polymorphic_ctype": {{ notespecial_ctype }},
"resourcetype": "SpecialTransaction",
"source": credit_id,
"destination": target.attr('data-inviter'),
"last_name": last_name,
"first_name": first_name,
"bank": ""
}).done(function () {
makeTransaction();
reset();
}).fail(function (xhr) {
errMsg(xhr.responseJSON, 4000);
});
};
};
$("#transaction_guest_" + id).click(makeTransaction);
$("#transaction_guest_" + id + "_especes").click(credit(1, "espèces"));
$("#transaction_guest_" + id + "_cb").click(credit(2, "carte bancaire"));
}
});
}
</script>
{% endblock %}

View File

@ -0,0 +1,43 @@
{% extends "base.html" %}
{% comment %}
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">
<form method="post">
{% csrf_token %}
{{ form|crispy }}
<button class="btn btn-primary" type="submit">{% trans "Submit" %}</button>
</form>
</div>
</div>
{% 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

@ -0,0 +1,49 @@
{% extends "base.html" %}
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load render_table from django_tables2 %}
{% load i18n %}
{% block content %}
{% if started_activities %}
<div class="card bg-secondary text-white mb-3">
<h3 class="card-header text-center">
{% trans "Current activity" %}
</h3>
<div class="card-body text-dark">
{% for activity in started_activities %}
{% include "activity/includes/activity_info.html" %}
{% endfor %}
</div>
</div>
{% endif %}
<div class="card bg-light mb-3">
<h3 class="card-header text-center">
{% trans "Upcoming activities" %}
</h3>
{% if upcoming.data %}
{% render_table upcoming %}
{% else %}
<div class="card-body">
<div class="alert alert-warning">
{% trans "There is no planned activity." %}
</div>
</div>
{% endif %}
<div class="card-footer">
<a class="btn btn-sm btn-success" href="{% url 'activity:activity_create' %}" data-turbolinks="false">
<i class="fa fa-calendar-plus-o" aria-hidden="true"></i>
{% trans 'New activity' %}
</a>
</div>
</div>
<div class="card bg-light mb-3">
<h3 class="card-header text-center">
{% trans "All activities" %}
</h3>
{% render_table table %}
</div>
{% endblock %}

View File

@ -0,0 +1,78 @@
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load i18n perms pretty_money %}
{% url 'activity:activity_detail' activity.pk as activity_detail_url %}
<div id="activity_info" class="card bg-light shadow mb-3">
<div class="card-header text-center">
<h4>
{% if request.path_info != activity_detail_url %}
<a href="{{ activity_detail_url }}">{{ activity.name }}</a>
{% else %}
{{ activity.name }}
{% endif %}
</h4>
</div>
<div class="card-body" id="profile_infos">
<dl class="row">
<dt class="col-xl-6">{% trans 'description'|capfirst %}</dt>
<dd class="col-xl-6"> {{ activity.description|linebreaks }}</dd>
<dt class="col-xl-6">{% trans 'type'|capfirst %}</dt>
<dd class="col-xl-6"> {{ activity.activity_type }}</dd>
<dt class="col-xl-6">{% trans 'start date'|capfirst %}</dt>
<dd class="col-xl-6">{{ activity.date_start }}</dd>
<dt class="col-xl-6">{% trans 'end date'|capfirst %}</dt>
<dd class="col-xl-6">{{ activity.date_end }}</dd>
{% if "activity.change_activity_valid"|has_perm:activity %}
<dt class="col-xl-6">{% trans 'creater'|capfirst %}</dt>
<dd class="col-xl-6"><a href="{% url "member:user_detail" pk=activity.creater.pk %}">{{ activity.creater }}</a></dd>
{% endif %}
<dt class="col-xl-6">{% trans 'organizer'|capfirst %}</dt>
<dd class="col-xl-6"><a href="{% url "member:club_detail" pk=activity.organizer.pk %}">{{ activity.organizer }}</a></dd>
<dt class="col-xl-6">{% trans 'attendees club'|capfirst %}</dt>
<dd class="col-xl-6"><a href="{% url "member:club_detail" pk=activity.attendees_club.pk %}">{{ activity.attendees_club }}</a></dd>
<dt class="col-xl-6">{% trans 'can invite'|capfirst %}</dt>
<dd class="col-xl-6">{{ activity.activity_type.can_invite|yesno }}</dd>
{% if activity.activity_type.can_invite %}
<dt class="col-xl-6">{% trans 'guest entry fee'|capfirst %}</dt>
<dd class="col-xl-6">{{ activity.activity_type.guest_entry_fee|pretty_money }}</dd>
{% endif %}
<dt class="col-xl-6">{% trans 'valid'|capfirst %}</dt>
<dd class="col-xl-6">{{ activity.valid|yesno }}</dd>
<dt class="col-xl-6">{% trans 'opened'|capfirst %}</dt>
<dd class="col-xl-6">{{ activity.open|yesno }}</dd>
</dl>
</div>
<div class="card-footer text-center">
{% if activity.open and activity.activity_type.manage_entries and ".change__open"|has_perm:activity %}
<a class="btn btn-warning btn-sm my-1" href="{% url 'activity:activity_entry' pk=activity.pk %}"> {% trans "Entry page" %}</a>
{% endif %}
{% if request.path_info == activity_detail_url %}
{% if activity.valid and ".change__open"|has_perm:activity %}
<a class="btn btn-warning btn-sm my-1" id="open_activity"> {% if activity.open %}{% trans "close"|capfirst %}{% else %}{% trans "open"|capfirst %}{% endif %}</a>
{% endif %}
{% if not activity.open and ".change__valid"|has_perm:activity %}
<a class="btn btn-success btn-sm my-1" id="validate_activity"> {% if activity.valid %}{% trans "invalidate"|capfirst %}{% else %}{% trans "validate"|capfirst %}{% endif %}</a>
{% endif %}
{% if ".change_"|has_perm:activity %}
<a class="btn btn-primary btn-sm my-1" href="{% url 'activity:activity_update' pk=activity.pk %}" data-turbolinks="false"> {% trans "edit"|capfirst %}</a>
{% endif %}
{% if activity.activity_type.can_invite and not activity_started %}
<a class="btn btn-primary btn-sm my-1" href="{% url 'activity:activity_invite' pk=activity.pk %}" data-turbolinks="false"> {% trans "Invite" %}</a>
{% endif %}
{% endif %}
</div>
</div>

View File

@ -0,0 +1,237 @@
# 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 member.models import Club
from ..api.views import ActivityTypeViewSet, ActivityViewSet, EntryViewSet, GuestViewSet
from ..models import Activity, ActivityType, Guest, Entry
class TestActivities(TestCase):
"""
Test activities
"""
fixtures = ('initial',)
def setUp(self):
self.user = User.objects.create_superuser(
username="admintoto",
password="tototototo",
email="toto@example.com"
)
self.client.force_login(self.user)
sess = self.client.session
sess["permission_mask"] = 42
sess.save()
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",
)
def test_activity_list(self):
"""
Display the list of all activities
"""
response = self.client.get(reverse("activity:activity_list"))
self.assertEqual(response.status_code, 200)
def test_activity_create(self):
"""
Create a new activity
"""
response = self.client.get(reverse("activity:activity_create"))
self.assertEqual(response.status_code, 200)
response = self.client.post(reverse("activity:activity_create"), data=dict(
name="Activity created",
description="This activity was successfully created.",
location="Earth",
activity_type=ActivityType.objects.get(name="Soirée de club").id,
creater=self.user.id,
organizer=Club.objects.get(name="Kfet").id,
attendees_club=Club.objects.get(name="Kfet").id,
date_start="{:%Y-%m-%d %H:%M}".format(timezone.now()),
date_end="{:%Y-%m-%d %H:%M}".format(timezone.now() + timedelta(days=2)),
valid=True,
))
self.assertTrue(Activity.objects.filter(name="Activity created").exists())
activity = Activity.objects.get(name="Activity created")
self.assertRedirects(response, reverse("activity:activity_detail", args=(activity.pk,)), 302, 200)
def test_activity_detail(self):
"""
Display the detail of an activity
"""
response = self.client.get(reverse("activity:activity_detail", args=(self.activity.pk,)))
self.assertEqual(response.status_code, 200)
def test_activity_update(self):
"""
Update an activity
"""
response = self.client.get(reverse("activity:activity_update", args=(self.activity.pk,)))
self.assertEqual(response.status_code, 200)
response = self.client.post(reverse("activity:activity_update", args=(self.activity.pk,)), data=dict(
name=str(self.activity) + " updated",
description="This activity was successfully updated.",
location="Earth",
activity_type=ActivityType.objects.get(name="Autre").id,
creater=self.user.id,
organizer=Club.objects.get(name="Kfet").id,
attendees_club=Club.objects.get(name="Kfet").id,
date_start="{:%Y-%m-%d %H:%M}".format(timezone.now()),
date_end="{:%Y-%m-%d %H:%M}".format(timezone.now() + timedelta(days=2)),
valid=True,
))
self.assertTrue(Activity.objects.filter(name="Activity updated").exists())
self.assertRedirects(response, reverse("activity:activity_detail", args=(self.activity.pk,)), 302, 200)
def test_activity_entry(self):
"""
Create some entries
"""
self.activity.open = True
self.activity.save()
response = self.client.get(reverse("activity:activity_entry", args=(self.activity.pk,)))
self.assertEqual(response.status_code, 200)
response = self.client.get(reverse("activity:activity_entry", args=(self.activity.pk,)) + "?search=guest")
self.assertEqual(response.status_code, 200)
response = self.client.get(reverse("activity:activity_entry", args=(self.activity.pk,)) + "?search=admin")
self.assertEqual(response.status_code, 200)
# User entry
response = self.client.post("/api/activity/entry/", data=dict(
activity=self.activity.id,
note=self.user.note.id,
guest="",
))
self.assertEqual(response.status_code, 201) # 201 = Created
self.assertTrue(Entry.objects.filter(note=self.user.note, guest=None, activity=self.activity).exists())
# Guest entry
response = self.client.post("/api/activity/entry/", data=dict(
activity=self.activity.id,
note=self.user.note.id,
guest=self.guest.id,
))
self.assertEqual(response.status_code, 201) # 201 = Created
self.assertTrue(Entry.objects.filter(note=self.user.note, guest=self.guest.id, activity=self.activity).exists())
def test_activity_invite(self):
"""
Try to invite people to an activity
"""
response = self.client.get(reverse("activity:activity_invite", args=(self.activity.pk,)))
self.assertEqual(response.status_code, 200)
# The activity is started, can't invite
response = self.client.post(reverse("activity:activity_invite", args=(self.activity.pk,)), data=dict(
activity=self.activity.id,
inviter=self.user.note.id,
last_name="GUEST2",
first_name="Guest",
school="School",
))
self.assertEqual(response.status_code, 200)
self.activity.date_start += timedelta(days=1)
self.activity.save()
response = self.client.post(reverse("activity:activity_invite", args=(self.activity.pk,)), data=dict(
activity=self.activity.id,
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)
def test_activity_ics(self):
"""
Render the ICS calendar
"""
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
@ -14,4 +14,5 @@ urlpatterns = [
path('<int:pk>/entry/', views.ActivityEntryView.as_view(), name='activity_entry'),
path('<int:pk>/update/', views.ActivityUpdateView.as_view(), name='activity_update'),
path('new/', views.ActivityCreateView.as_view(), name='activity_create'),
path('calendar.ics', views.CalendarView.as_view(), name='calendar_ics'),
]

View File

@ -1,29 +1,55 @@
# 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 datetime, timezone
from hashlib import md5
from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import PermissionDenied
from django.db import transaction
from django.db.models import F, Q
from django.http import HttpResponse
from django.urls import reverse_lazy
from django.views.generic import CreateView, DetailView, UpdateView, TemplateView
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.utils.translation import gettext_lazy as _
from django_tables2.views import SingleTableView
from note.models import NoteUser, Alias, NoteSpecial
from django.views import View
from django.views.decorators.cache import cache_page
from django.views.generic import DetailView, TemplateView, UpdateView
from django.views.generic.list import ListView
from django_tables2.views import MultiTableMixin, SingleTableMixin
from api.viewsets import is_regex
from note.models import Alias, NoteSpecial, NoteUser
from permission.backends import PermissionBackend
from permission.views import ProtectQuerysetMixin
from permission.views import ProtectQuerysetMixin, ProtectedCreateView
from .forms import ActivityForm, GuestForm
from .models import Activity, Guest, Entry
from .tables import ActivityTable, GuestTable, EntryTable
from .models import Activity, Entry, Guest, Opener
from .tables import ActivityTable, EntryTable, GuestTable, OpenerTable
class ActivityCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
class ActivityCreateView(ProtectQuerysetMixin, ProtectedCreateView):
"""
View to create a new Activity
"""
model = Activity
form_class = ActivityForm
extra_context = {"title": _("Create new activity")}
def get_sample_object(self):
return Activity(
name="",
description="",
creater=self.request.user,
activity_type_id=1,
organizer_id=1,
attendees_club_id=1,
date_start=timezone.now(),
date_end=timezone.now(),
)
@transaction.atomic
def form_valid(self, form):
form.instance.creater = self.request.user
return super().form_valid(form)
@ -33,127 +59,279 @@ class ActivityCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
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
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().reverse()
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)
context['title'] = _("Activities")
tables = context["tables"]
for name, table in zip(["table", "upcoming"], tables):
context[name] = table
upcoming_activities = Activity.objects.filter(date_end__gt=datetime.now())
context['upcoming'] = ActivityTable(
data=upcoming_activities.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view")))
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
"""
model = Activity
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"] = datetime.now(timezone.utc) > self.object.date_start
context["activity_started"] = timezone.now() > timezone.localtime(self.object.date_start)
context["widget"] = {
"name": "opener",
"resetable": True,
"attrs": {
"class": "autocomplete form-control",
"id": "opener",
"api_url": "/api/note/alias/?note__polymorphic_ctype__model=noteuser",
"name_field": "name",
"placeholder": ""
}
}
return context
class ActivityUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
"""
Updates one Activity
"""
model = Activity
form_class = ActivityForm
extra_context = {"title": _("Update activity")}
def get_success_url(self, **kwargs):
return reverse_lazy('activity:activity_detail', kwargs={"pk": self.kwargs["pk"]})
class ActivityInviteView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
class ActivityInviteView(ProtectQuerysetMixin, ProtectedCreateView):
"""
Invite a Guest, The rules to invites someone are defined in `forms:activity.GuestForm`
"""
model = Guest
form_class = GuestForm
template_name = "activity/activity_invite.html"
template_name = "activity/activity_form.html"
def get_sample_object(self):
""" Creates a standart Guest binds to the Activity"""
activity = Activity.objects.get(pk=self.kwargs["pk"])
return Guest(
activity=activity,
first_name="",
last_name="",
school="",
inviter=self.request.user.note,
)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
activity = context["form"].activity
context["title"] = _('Invite guest to the activity "{}"').format(activity.name)
return context
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"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
table_class = EntryTable
activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view"))\
.get(pk=self.kwargs["pk"])
context["activity"] = activity
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()
matched = []
activity = Activity.objects.get(pk=self.kwargs["pk"])
pattern = "^$"
if "search" in self.request.GET:
pattern = self.request.GET["search"]
sample_entry = Entry(activity=activity, note=self.request.user.note)
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 pattern:
pattern = "^$"
if not activity.activity_type.manage_entries:
raise PermissionDenied(_("This activity does not support activity entries."))
if pattern[0] != "^":
pattern = "^" + pattern
if not activity.open:
raise PermissionDenied(_("This activity is closed."))
return super().dispatch(request, *args, **kwargs)
def get_invited_guest(self, activity):
"""
Retrieves all Guests to the activity
"""
guest_qs = Guest.objects\
.annotate(balance=F("inviter__balance"), note_name=F("inviter__user__username"))\
.filter(Q(first_name__regex=pattern) | Q(last_name__regex=pattern)
| Q(inviter__alias__name__regex=pattern)
| Q(inviter__alias__normalized_name__regex=Alias.normalize(pattern))) \
.filter(PermissionBackend.filter_queryset(self.request.user, Guest, "view"))\
.distinct()[:20]
for guest in guest_qs:
guest.type = "Invité"
matched.append(guest)
.filter(activity=activity)\
.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"]
# 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(**{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.distinct()
def get_invited_note(self, activity):
"""
Retrieves all Note that can attend the activity,
they need to have an up-to-date membership in the attendees_club.
"""
note_qs = Alias.objects.annotate(last_name=F("note__noteuser__user__last_name"),
first_name=F("note__noteuser__user__first_name"),
username=F("note__noteuser__user__username"),
note_name=F("name"),
balance=F("note__balance"))\
.filter(Q(note__polymorphic_ctype__model="noteuser")
& (Q(note__noteuser__user__first_name__regex=pattern)
| Q(note__noteuser__user__last_name__regex=pattern)
| Q(name__regex=pattern)
| Q(normalized_name__regex=Alias.normalize(pattern)))) \
.filter(PermissionBackend.filter_queryset(self.request.user, Alias, "view"))
if settings.DATABASES[note_qs.db]["ENGINE"] == 'django.db.backends.postgresql_psycopg2':
note_qs = note_qs.distinct('note__pk')[:20]
balance=F("note__balance"))
# Keep only users that have a note
note_qs = note_qs.filter(note__noteuser__isnull=False).exclude(note__inactivity_reason='forced')
if activity.activity_type.name != "Pot Vieux":
# Keep only 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(),
)
# 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(),
# )
# Filter with permission backend
note_qs = note_qs.filter(PermissionBackend.filter_queryset(self.request, Alias, "view"))
if "search" in self.request.GET and self.request.GET["search"]:
pattern = self.request.GET["search"]
# Check if this is a valid regex. If not, we won't check regex
valid_regex = is_regex(pattern)
suffix = "__iregex" if valid_regex else "__icontains"
note_qs = note_qs.filter(
Q(**{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()
# SQLite doesn't support distinct fields. For compatibility reason (in dev mode), the note list will only
# have distinct aliases rather than distinct notes with a SQLite DB, but it can fill the result page.
# In production mode, please use PostgreSQL.
note_qs = note_qs.distinct()[:20]
for note in note_qs:
note_qs = note_qs.distinct('note__pk')[:20]\
if settings.DATABASES[note_qs.db]["ENGINE"] == 'django.db.backends.postgresql' else note_qs.distinct()[:20]
return note_qs
def get_table_data(self):
activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request, Activity, "view"))\
.distinct().get(pk=self.kwargs["pk"])
matched = []
for guest in self.get_invited_guest(activity):
guest.type = "Invité"
matched.append(guest)
for note in self.get_invited_note(activity):
note.type = "Adhérent"
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)
@ -161,8 +339,70 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
context["noteuser_ctype"] = ContentType.objects.get_for_model(NoteUser).pk
context["notespecial_ctype"] = ContentType.objects.get_for_model(NoteSpecial).pk
context["activities_open"] = Activity.objects.filter(open=True).filter(
PermissionBackend.filter_queryset(self.request.user, Activity, "view")).filter(
PermissionBackend.filter_queryset(self.request.user, Activity, "change")).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,
"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.
"""
def multilines(self, string, maxlength, offset=0):
newstring = string[:maxlength - offset]
string = string[maxlength - offset:]
while string:
newstring += "\r\n "
newstring += string[:maxlength - 1]
string = string[maxlength - 1:]
return newstring
def get(self, request, *args, **kwargs):
ics = """BEGIN:VCALENDAR
VERSION: 2.0
PRODID:Note Kfet 2020
X-WR-CALNAME:Kfet Calendar
NAME:Kfet Calendar
CALSCALE:GREGORIAN
BEGIN:VTIMEZONE
TZID:Europe/Paris
X-LIC-LOCATION:Europe/Paris
BEGIN:DAYLIGHT
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
TZNAME:CEST
DTSTART:19700329T020000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
TZNAME:CET
DTSTART:19701025T030000
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
END:STANDARD
END:VTIMEZONE
"""
for activity in Activity.objects.filter(valid=True).order_by("-date_start").all():
ics += f"""BEGIN:VEVENT
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:{"{:%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) + f"""
-- {activity.organizer.name}
END:VEVENT
"""
ics += "END:VCALENDAR"
ics = ics.replace("\r", "").replace("\n", "\r\n")
return HttpResponse(ics, content_type="text/calendar; charset=UTF-8")

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'

91
apps/api/serializers.py Normal file
View File

@ -0,0 +1,91 @@
# 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 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(serializers.ModelSerializer):
"""
REST API Serializer for Users.
The djangorestframework plugin will analyse the model `User` and parse all fields in the API.
"""
class Meta:
model = User
exclude = (
'password',
'groups',
'user_permissions',
)
class ContentTypeSerializer(serializers.ModelSerializer):
"""
REST API Serializer for Users.
The djangorestframework plugin will analyse the model `User` and parse all fields in the API.
"""
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,91 +1,62 @@
# 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.urls import url, include
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import routers, serializers
from rest_framework.filters import SearchFilter
from rest_framework.viewsets import ReadOnlyModelViewSet
from activity.api.urls import register_activity_urls
from api.viewsets import ReadProtectedModelViewSet
from member.api.urls import register_members_urls
from note.api.urls import register_note_urls
from treasury.api.urls import register_treasury_urls
from logs.api.urls import register_logs_urls
from permission.api.urls import register_permission_urls
from wei.api.urls import register_wei_urls
class UserSerializer(serializers.ModelSerializer):
"""
REST API Serializer for Users.
The djangorestframework plugin will analyse the model `User` and parse all fields in the API.
"""
class Meta:
model = User
exclude = (
'password',
'groups',
'user_permissions',
)
class ContentTypeSerializer(serializers.ModelSerializer):
"""
REST API Serializer for Users.
The djangorestframework plugin will analyse the model `User` and parse all fields in the API.
"""
class Meta:
model = ContentType
fields = '__all__'
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/
"""
queryset = User.objects.all()
serializer_class = UserSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['id', 'username', 'first_name', 'last_name', 'email', 'is_superuser', 'is_staff', 'is_active', ]
search_fields = ['$username', '$first_name', '$last_name', ]
# This ViewSet is the only one that is accessible from all authenticated users!
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/
"""
queryset = ContentType.objects.all()
serializer_class = ContentTypeSerializer
from django.conf import settings
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.
# Register each app API router and user viewset
router = routers.DefaultRouter()
router.register('models', ContentTypeViewSet)
router.register('user', UserViewSet)
register_members_urls(router, 'members')
if "activity" in settings.INSTALLED_APPS:
from activity.api.urls import register_activity_urls
register_activity_urls(router, 'activity')
register_note_urls(router, 'note')
register_treasury_urls(router, 'treasury')
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,31 +1,126 @@
# 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
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 rest_framework import viewsets
from note_kfet.middlewares import get_current_authenticated_user
from note.models import Alias
from .filters import RegexSafeSearchFilter
from .serializers import UserSerializer, ContentTypeSerializer
class ReadProtectedModelViewSet(viewsets.ModelViewSet):
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.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
model = ContentType.objects.get_for_model(self.serializer_class.Meta.model).model_class()
user = get_current_authenticated_user()
self.queryset = model.objects.filter(PermissionBackend.filter_queryset(user, model, "view"))
self.model = ContentType.objects.get_for_model(self.serializer_class.Meta.model).model_class()
def get_queryset(self):
return self.queryset.filter(PermissionBackend.filter_queryset(self.request, self.model, "view")).distinct()
class ReadOnlyProtectedModelViewSet(viewsets.ReadOnlyModelViewSet):
class ReadOnlyProtectedModelViewSet(ReadOnlyModelViewSet):
"""
Protect a ReadOnlyModelViewSet by filtering the objects that the user cannot see.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
model = ContentType.objects.get_for_model(self.serializer_class.Meta.model).model_class()
user = get_current_authenticated_user()
self.queryset = model.objects.filter(PermissionBackend.filter_queryset(user, model, "view"))
self.model = ContentType.objects.get_for_model(self.serializer_class.Meta.model).model_class()
def get_queryset(self):
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/user/
"""
queryset = User.objects
serializer_class = UserSerializer
filter_backends = [DjangoFilterBackend]
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()
# Sqlite doesn't support ORDER BY in subqueries
queryset = queryset.order_by("username") \
if settings.DATABASES[queryset.db]["ENGINE"] == 'django.db.backends.postgresql' else queryset
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
Q(**{f"note__alias__name{suffix}": prefix + pattern})
).union(
queryset.filter(
# Match with normalization
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(**{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(**{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,
)
queryset = queryset if settings.DATABASES[queryset.db]["ENGINE"] == 'django.db.backends.postgresql' \
else queryset.order_by("username")
return queryset
# This ViewSet is the only one that is accessible from all authenticated users!
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/models/
"""
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

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

@ -0,0 +1,37 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib import admin
from django.db import transaction
from note_kfet.admin import admin_site
from .models import Allergen, BasicFood, QRCode, TransformedFood
@admin.register(QRCode, site=admin_site)
class QRCodeAdmin(admin.ModelAdmin):
pass
@admin.register(BasicFood, site=admin_site)
class BasicFoodAdmin(admin.ModelAdmin):
@transaction.atomic
def save_related(self, *args, **kwargs):
ans = super().save_related(*args, **kwargs)
args[1].instance.update()
return ans
@admin.register(TransformedFood, site=admin_site)
class TransformedFoodAdmin(admin.ModelAdmin):
exclude = ["allergens", "expiry_date"]
@transaction.atomic
def save_related(self, request, form, *args, **kwargs):
super().save_related(request, form, *args, **kwargs)
form.instance.update()
@admin.register(Allergen, site=admin_site)
class AllergenAdmin(admin.ModelAdmin):
pass

View File

View File

@ -0,0 +1,50 @@
# 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, QRCode, TransformedFood
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 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__'
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__'

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, QRCodeViewSet, TransformedFoodViewSet
def register_food_urls(router, path):
"""
Configure router for Food REST API.
"""
router.register(path + '/allergen', AllergenViewSet)
router.register(path + '/basic_food', BasicFoodViewSet)
router.register(path + '/qrcode', QRCodeViewSet)
router.register(path + '/transformed_food', TransformedFoodViewSet)

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, QRCodeSerializer, TransformedFoodSerializer
from ..models import Allergen, BasicFood, QRCode, TransformedFood
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/basic_food/
"""
queryset = BasicFood.objects.order_by('id')
serializer_class = BasicFoodSerializer
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', ]
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/transformed_food/
"""
queryset = TransformedFood.objects.order_by('id')
serializer_class = TransformedFoodSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['name', ]
search_fields = ['$name', ]

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')

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

@ -0,0 +1,114 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from random import shuffle
from django import forms
from django.utils.translation import gettext_lazy as _
from django.utils import timezone
from member.models import Club
from bootstrap_datepicker_plus.widgets import DateTimePickerInput
from note_kfet.inputs import Autocomplete
from note_kfet.middlewares import get_current_request
from permission.backends import PermissionBackend
from .models import BasicFood, QRCode, TransformedFood
class AddIngredientForms(forms.ModelForm):
"""
Form for add an ingredient
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['ingredient'].queryset = self.fields['ingredient'].queryset.filter(
polymorphic_ctype__model='transformedfood',
is_ready=False,
is_active=True,
was_eaten=False,
)
# Caution, the logic is inverted here, we flip the logic on saving in AddIngredientView
self.fields['is_active'].initial = True
self.fields['is_active'].label = _("Fully used")
class Meta:
model = TransformedFood
fields = ('ingredient', 'is_active')
class BasicFoodForms(forms.ModelForm):
"""
Form for add non-transformed food
"""
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]) + ", ..."
class Meta:
model = BasicFood
fields = ('name', 'owner', 'date_type', 'expiry_date', 'is_active', 'was_eaten', 'allergens',)
widgets = {
"owner": Autocomplete(
model=Club,
attrs={"api_url": "/api/members/club/"},
),
'expiry_date': DateTimePickerInput(),
}
class QRCodeForms(forms.ModelForm):
"""
Form for create QRCode
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['food_container'].queryset = self.fields['food_container'].queryset.filter(
is_active=True,
was_eaten=False,
polymorphic_ctype__model='transformedfood',
)
class Meta:
model = QRCode
fields = ('food_container',)
class TransformedFoodForms(forms.ModelForm):
"""
Form for add transformed food
"""
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
self.fields['creation_date'].required = True
self.fields['creation_date'].initial = timezone.now
self.fields['is_active'].initial = True
self.fields['is_ready'].initial = False
self.fields['was_eaten'].initial = False
# 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]) + ", ..."
class Meta:
model = TransformedFood
fields = ('name', 'creation_date', 'owner', 'is_active', 'is_ready', 'was_eaten', 'shelf_life')
widgets = {
"owner": Autocomplete(
model=Club,
attrs={"api_url": "/api/members/club/"},
),
'creation_date': DateTimePickerInput(),
}

View File

@ -0,0 +1,84 @@
# Generated by Django 2.2.28 on 2024-07-05 08:57
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', '0011_profile_vss_charter_read'),
]
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')),
('was_eaten', models.BooleanField(default=False, verbose_name='was eaten')),
('is_ready', models.BooleanField(default=False, verbose_name='is ready')),
('allergens', models.ManyToManyField(blank=True, to='food.Allergen', verbose_name='allergen')),
('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_food.food_set+', to='contenttypes.ContentType')),
],
options={
'verbose_name': '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')),
('date_type', models.CharField(choices=[('DLC', 'DLC'), ('DDM', 'DDM')], max_length=255)),
('arrival_date', models.DateTimeField(default=django.utils.timezone.now, verbose_name='arrival date')),
],
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.OneToOneField(on_delete=django.db.models.deletion.PROTECT, 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(verbose_name='creation date')),
('is_active', models.BooleanField(default=True, verbose_name='is active')),
('ingredient', 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

@ -0,0 +1,19 @@
# Generated by Django 2.2.28 on 2024-07-06 20:37
import datetime
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('food', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='transformedfood',
name='shelf_life',
field=models.DurationField(default=datetime.timedelta(days=3), verbose_name='shelf life'),
),
]

View File

@ -0,0 +1,62 @@
from django.db import migrations
def create_14_mandatory_allergens(apps, schema_editor):
"""
There are 14 mandatory allergens, they are pre-injected
"""
Allergen = apps.get_model("food", "allergen")
Allergen.objects.get_or_create(
name="Gluten",
)
Allergen.objects.get_or_create(
name="Fruits à coques",
)
Allergen.objects.get_or_create(
name="Crustacés",
)
Allergen.objects.get_or_create(
name="Céléri",
)
Allergen.objects.get_or_create(
name="Oeufs",
)
Allergen.objects.get_or_create(
name="Moutarde",
)
Allergen.objects.get_or_create(
name="Poissons",
)
Allergen.objects.get_or_create(
name="Soja",
)
Allergen.objects.get_or_create(
name="Lait",
)
Allergen.objects.get_or_create(
name="Sulfites",
)
Allergen.objects.get_or_create(
name="Sésame",
)
Allergen.objects.get_or_create(
name="Lupin",
)
Allergen.objects.get_or_create(
name="Arachides",
)
Allergen.objects.get_or_create(
name="Mollusques",
)
class Migration(migrations.Migration):
dependencies = [
('food', '0002_transformedfood_shelf_life'),
]
operations = [
migrations.RunPython(create_14_mandatory_allergens),
]

View File

@ -0,0 +1,28 @@
# Generated by Django 2.2.28 on 2024-08-13 21:58
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('food', '0003_create_14_allergens_mandatory'),
]
operations = [
migrations.RemoveField(
model_name='transformedfood',
name='is_active',
),
migrations.AddField(
model_name='food',
name='is_active',
field=models.BooleanField(default=True, verbose_name='is active'),
),
migrations.AlterField(
model_name='qrcode',
name='food_container',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='QR_code', to='food.Food', verbose_name='food container'),
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 4.2.15 on 2024-08-28 08:00
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('food', '0004_auto_20240813_2358'),
]
operations = [
migrations.AlterField(
model_name='food',
name='polymorphic_ctype',
field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype'),
),
]

View File

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

@ -0,0 +1,226 @@
# 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 member.models import Club
from polymorphic.models import PolymorphicModel
class QRCode(models.Model):
"""
An QRCode model
"""
qr_code_number = models.PositiveIntegerField(
verbose_name=_("QR-code number"),
unique=True,
)
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 {qr_code_number}").format(qr_code_number=self.qr_code_number)
class Allergen(models.Model):
"""
A list of 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):
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=_('allergen'),
)
expiry_date = models.DateTimeField(
verbose_name=_('expiry date'),
null=False,
)
was_eaten = models.BooleanField(
default=False,
verbose_name=_('was eaten'),
)
# is_ready != is_active : is_ready signifie que la nourriture est prête à être manger,
# is_active signifie que la nourriture n'est pas encore archivé
# il sert dans les cas où il est plus intéressant que de l'open soit conservé (confiture par ex)
is_ready = models.BooleanField(
default=False,
verbose_name=_('is ready'),
)
is_active = models.BooleanField(
default=True,
verbose_name=_('is active'),
)
def __str__(self):
return self.name
@transaction.atomic
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
return super().save(force_insert, force_update, using, update_fields)
class Meta:
verbose_name = _('food')
verbose_name = _('foods')
class BasicFood(Food):
"""
Food which has been directly buy on supermarket
"""
date_type = models.CharField(
max_length=255,
choices=(
("DLC", "DLC"),
("DDM", "DDM"),
)
)
arrival_date = models.DateTimeField(
verbose_name=_('arrival date'),
default=timezone.now,
)
# label = models.ImageField(
# verbose_name=_('food label'),
# max_length=255,
# blank=False,
# null=False,
# upload_to='label/',
# )
@transaction.atomic
def update_allergens(self):
# update parents
for parent in self.transformed_ingredient_inv.iterator():
parent.update_allergens()
@transaction.atomic
def update_expiry_date(self):
# update parents
for parent in self.transformed_ingredient_inv.iterator():
parent.update_expiry_date()
@transaction.atomic
def update(self):
self.update_allergens()
self.update_expiry_date()
class Meta:
verbose_name = _('Basic food')
verbose_name_plural = _('Basic foods')
class TransformedFood(Food):
"""
Transformed food are a mix between basic food and meal
"""
creation_date = models.DateTimeField(
verbose_name=_('creation date'),
)
ingredient = models.ManyToManyField(
Food,
blank=True,
symmetrical=False,
related_name='transformed_ingredient_inv',
verbose_name=_('transformed ingredient'),
)
# Without microbiological analyzes, the storage time is 3 days
shelf_life = models.DurationField(
verbose_name=_("shelf life"),
default=timedelta(days=3),
)
@transaction.atomic
def archive(self):
# When a meal are archived, if it was eaten, update ingredient fully used for this meal
raise NotImplementedError
@transaction.atomic
def update_allergens(self):
# When allergens are changed, simply update the parents' allergens
old_allergens = list(self.allergens.all())
self.allergens.clear()
for ingredient in self.ingredient.iterator():
self.allergens.set(self.allergens.union(ingredient.allergens.all()))
if old_allergens == list(self.allergens.all()):
return
super().save()
# update parents
for parent in self.transformed_ingredient_inv.iterator():
parent.update_allergens()
@transaction.atomic
def update_expiry_date(self):
# When expiry_date is changed, simply update the parents' expiry_date
old_expiry_date = self.expiry_date
self.expiry_date = self.creation_date + self.shelf_life
for ingredient in self.ingredient.iterator():
self.expiry_date = min(self.expiry_date, ingredient.expiry_date)
if old_expiry_date == self.expiry_date:
return
super().save()
# update parents
for parent in self.transformed_ingredient_inv.iterator():
parent.update_expiry_date()
@transaction.atomic
def update(self):
self.update_allergens()
self.update_expiry_date()
@transaction.atomic
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
class Meta:
verbose_name = _('Transformed food')
verbose_name_plural = _('Transformed foods')

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

@ -0,0 +1,19 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import django_tables2 as tables
from django_tables2 import A
from .models import TransformedFood
class TransformedFoodTable(tables.Table):
name = tables.LinkColumn(
'food:food_view',
args=[A('pk'), ],
)
class Meta:
model = TransformedFood
template_name = 'django_tables2/bootstrap4.html'
fields = ('name', "owner", "allergens", "expiry_date")

View File

@ -0,0 +1,20 @@
{% extends "base.html" %}
{% comment %}
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,37 @@
{% extends "base.html" %}
{% comment %}
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>
<li><p>{% trans 'Owner' %} : {{ food.owner }}</p></li>
<li><p>{% trans 'Arrival date' %} : {{ food.arrival_date }}</p></li>
<li><p>{% trans 'Expiry date' %} : {{ food.expiry_date }} ({{ food.date_type }})</p></li>
<li>{% trans 'Allergens' %} :</li>
<ul>
{% for allergen in food.allergens.iterator %}
<li>{{ allergen.name }}</li>
{% endfor %}
</ul>
<p>
<li><p>{% trans 'Active' %} : {{ food.is_active }}<p></li>
<li><p>{% trans 'Eaten' %} : {{ food.was_eaten }}<p></li>
</ul>
{% if can_update %}
<a class="btn btn-sm btn-warning" href="{% url "food:basic_update" pk=food.pk %}">{% trans 'Update' %}</a>
{% endif %}
{% if can_add_ingredient %}
<a class="btn btn-sm btn-success" href="{% url "food:add_ingredient" pk=food.pk %}">
{% trans 'Add to a meal' %}
</a>
{% endif %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,20 @@
{% extends "base.html" %}
{% comment %}
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,55 @@
{% extends "base.html" %}
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load render_table from django_tables2 %}
{% load i18n crispy_forms_tags %}
{% block content %}
<div class="card bg-white mb-3">
<h3 class="card-header text-center">
{{ title }}
</h3>
<div class="card-body" id="form">
<a class="btn btn-sm btn-success" href="{% url "food:qrcode_basic_create" slug=slug %}">
{% trans 'New basic food' %}
</a>
<form method="post">
{% csrf_token %}
{{ form|crispy }}
<button class="btn btn-primary" type="submit">{% trans "Submit" %}</button>
</form>
<div class="card-body" id="profile_infos">
<h4>{% trans "Copy constructor" %}</h4>
<table class="table">
<thead>
<tr>
<th class="orderable">
{% trans "Name" %}
</th>
<th class="orderable">
{% trans "Owner" %}
</th>
<th class="orderable">
{% trans "Arrival date" %}
</th>
<th class="orderable">
{% trans "Expiry date" %}
</th>
</tr>
</thead>
<tbody>
{% for basic in last_basic %}
<tr>
<td><a href="{% url "food:qrcode_basic_create" slug=slug %}?copy={{ basic.pk }}">{{ basic.name }}</a></td>
<td>{{ basic.owner }}</td>
<td>{{ basic.arrival_date }}</td>
<td>{{ basic.expiry_date }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,39 @@
{% extends "base.html" %}
{% comment %}
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 }} {% trans 'number' %} {{ qrcode.qr_code_number }}
</h3>
<div class="card-body">
<ul>
<li><p>{% trans 'Name' %} : {{ qrcode.food_container.name }}</p></li>
<li><p>{% trans 'Owner' %} : {{ qrcode.food_container.owner }}</p></li>
<li><p>{% trans 'Expiry date' %} : {{ qrcode.food_container.expiry_date }}</p></li>
</ul>
{% if qrcode.food_container.polymorphic_ctype.model == 'basicfood' and can_update_basic %}
<a class="btn btn-sm btn-warning" href="{% url "food:basic_update" pk=qrcode.food_container.pk %}" data-turbolinks="false">
{% trans 'Update' %}
</a>
{% elif can_update_transformed %}
<a class="btn btn-sm btn-warning" href="{% url "food:transformed_update" pk=qrcode.food_container.pk %}">
{% trans 'Update' %}
</a>
{% endif %}
{% if can_view_detail %}
<a class="btn btn-sm btn-primary" href="{% url "food:food_view" pk=qrcode.food_container.pk %}">
{% trans 'View details' %}
</a>
{% endif %}
{% if can_add_ingredient %}
<a class="btn btn-sm btn-success" href="{% url "food:add_ingredient" pk=qrcode.food_container.pk %}">
{% trans 'Add to a meal' %}
</a>
{% endif %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,51 @@
{% extends "base.html" %}
{% comment %}
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>
<li><p>{% trans 'Owner' %} : {{ food.owner }}</p></li>
{% if can_see_ready %}
<li><p>{% trans 'Ready' %} : {{ food.is_ready }}</p></li>
{% endif %}
<li><p>{% trans 'Creation date' %} : {{ food.creation_date }}</p></li>
<li><p>{% trans 'Expiry date' %} : {{ food.expiry_date }}</p></li>
<li>{% trans 'Allergens' %} :</li>
<ul>
{% for allergen in food.allergens.iterator %}
<li>{{ allergen.name }}</li>
{% endfor %}
</ul>
<p>
<li>{% trans 'Ingredients' %} :</li>
<ul>
{% for ingredient in food.ingredient.iterator %}
<li><a href="{% url "food:food_view" pk=ingredient.pk %}">{{ ingredient.name }}</a></li>
{% endfor %}
</ul>
<p>
<li><p>{% trans 'Shelf life' %} : {{ food.shelf_life }}</p></li>
<li><p>{% trans 'Ready' %} : {{ food.is_ready }}</p></li>
<li><p>{% trans 'Active' %} : {{ food.is_active }}</p></li>
<li><p>{% trans 'Eaten' %} : {{ food.was_eaten }}</p></li>
</ul>
{% if can_update %}
<a class="btn btn-sm btn-warning" href="{% url "food:transformed_update" pk=food.pk %}">
{% trans 'Update' %}
</a>
{% endif %}
{% if can_add_ingredient %}
<a class="btn btn-sm btn-success" href="{% url "food:add_ingredient" pk=food.pk %}">
{% trans 'Add to a meal' %}
</a>
{% endif %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,20 @@
{% extends "base.html" %}
{% comment %}
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,60 @@
{% extends "base.html" %}
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load render_table from django_tables2 %}
{% load i18n %}
{% block content %}
<div class="card bg-light mb-3">
<h3 class="card-header text-center">
{% trans "Meal served" %}
</h3>
{% if can_create_meal %}
<div class="card-footer">
<a class="btn btn-sm btn-success" href="{% url 'food:transformed_create' %}" data-turbolinks="false">
{% 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>
{% endif %}
</div>
<div class="card bg-light mb-3">
<h3 class="card-header text-center">
{% trans "Open" %}
</h3>
{% if open.data %}
{% render_table open %}
{% else %}
<div class="card-body">
<div class="alert alert-warning">
{% trans "There is no free meal." %}
</div>
</div>
{% endif %}
</div>
<div class="card bg-light mb-3">
<h3 class="card-header text-center">
{% trans "All meals" %}
</h3>
{% if table.data %}
{% render_table table %}
{% else %}
<div class="card-body">
<div class="alert alert-warning">
{% trans "There is no meal." %}
</div>
</div>
{% endif %}
</div>
{% endblock %}

3
apps/food/tests.py Normal file
View File

@ -0,0 +1,3 @@
# from django.test import TestCase
# Create your tests here.

21
apps/food/urls.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
from django.urls import path
from . import views
app_name = 'food'
urlpatterns = [
path('', views.TransformedListView.as_view(), name='food_list'),
path('<int:slug>', views.QRCodeView.as_view(), name='qrcode_view'),
path('detail/<int:pk>', views.FoodView.as_view(), name='food_view'),
path('<int:slug>/create_qrcode', views.QRCodeCreateView.as_view(), name='qrcode_create'),
path('<int:slug>/create_qrcode/basic', views.QRCodeBasicFoodCreateView.as_view(), name='qrcode_basic_create'),
path('create/transformed', views.TransformedFoodCreateView.as_view(), name='transformed_create'),
path('update/basic/<int:pk>', views.BasicFoodUpdateView.as_view(), name='basic_update'),
path('update/transformed/<int:pk>', views.TransformedFoodUpdateView.as_view(), name='transformed_update'),
path('add/<int:pk>', views.AddIngredientView.as_view(), name='add_ingredient'),
]

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

@ -0,0 +1,421 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.db import transaction
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpResponseRedirect
from django_tables2.views import MultiTableMixin
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from django.utils import timezone
from django.views.generic import DetailView, UpdateView
from django.views.generic.list import ListView
from django.forms import HiddenInput
from permission.backends import PermissionBackend
from permission.views import ProtectQuerysetMixin, ProtectedCreateView
from .forms import AddIngredientForms, BasicFoodForms, QRCodeForms, TransformedFoodForms
from .models import BasicFood, Food, QRCode, TransformedFood
from .tables import TransformedFoodTable
class AddIngredientView(ProtectQuerysetMixin, UpdateView):
"""
A view to add an ingredient
"""
model = Food
template_name = 'food/add_ingredient_form.html'
extra_context = {"title": _("Add the ingredient")}
form_class = AddIngredientForms
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["pk"] = self.kwargs["pk"]
return context
@transaction.atomic
def form_valid(self, form):
form.instance.creater = self.request.user
food = Food.objects.get(pk=self.kwargs['pk'])
add_ingredient_form = AddIngredientForms(data=self.request.POST)
if food.is_ready:
form.add_error(None, _("The product is already prepared"))
return self.form_invalid(form)
if not add_ingredient_form.is_valid():
return self.form_invalid(form)
# We flip logic ""fully used = not is_active""
food.is_active = not food.is_active
# Save the aliment and the allergens associed
for transformed_pk in self.request.POST.getlist('ingredient'):
transformed = TransformedFood.objects.get(pk=transformed_pk)
if not transformed.is_ready:
transformed.ingredient.add(food)
transformed.update()
food.save()
return HttpResponseRedirect(self.get_success_url())
def get_success_url(self, **kwargs):
return reverse('food:food_list')
class BasicFoodUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
"""
A view to update a basic food
"""
model = BasicFood
form_class = BasicFoodForms
template_name = 'food/basicfood_form.html'
extra_context = {"title": _("Update an aliment")}
@transaction.atomic
def form_valid(self, form):
form.instance.creater = self.request.user
basic_food_form = BasicFoodForms(data=self.request.POST)
if not basic_food_form.is_valid():
return self.form_invalid(form)
ans = super().form_valid(form)
form.instance.update()
return ans
def get_success_url(self, **kwargs):
self.object.refresh_from_db()
return reverse('food:food_view', kwargs={"pk": self.object.pk})
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
return context
class FoodView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
"""
A view to see a food
"""
model = Food
extra_context = {"title": _("Details of:")}
context_object_name = "food"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["can_update"] = PermissionBackend.check_perm(self.request, "food.change_food")
context["can_add_ingredient"] = PermissionBackend.check_perm(self.request, "food.change_transformedfood")
return context
class QRCodeBasicFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView):
#####################################################################
# TO DO
# - this feature is very pratical for meat or fish, nevertheless we can implement this later
# - fix picture save
# - implement solution crop and convert image (reuse or recode ImageForm from members apps)
#####################################################################
"""
A view to add a basic food with a qrcode
"""
model = BasicFood
form_class = BasicFoodForms
template_name = 'food/basicfood_form.html'
extra_context = {"title": _("Add a new basic food with QRCode")}
@transaction.atomic
def form_valid(self, form):
form.instance.creater = self.request.user
basic_food_form = BasicFoodForms(data=self.request.POST)
if not basic_food_form.is_valid():
return self.form_invalid(form)
# Save the aliment and the allergens associed
basic_food = form.save(commit=False)
# We assume the date of labeling and the same as the date of arrival
basic_food.arrival_date = timezone.now()
basic_food.is_ready = False
basic_food.is_active = True
basic_food.was_eaten = False
basic_food._force_save = True
basic_food.save()
basic_food.refresh_from_db()
qrcode = QRCode()
qrcode.qr_code_number = self.kwargs['slug']
qrcode.food_container = basic_food
qrcode.save()
return super().form_valid(form)
def get_success_url(self, **kwargs):
self.object.refresh_from_db()
return reverse('food:qrcode_view', kwargs={"slug": self.kwargs['slug']})
def get_sample_object(self):
# We choose a club which may work or BDE else
owner_id = 1
for membership in self.request.user.memberships.all():
club_id = membership.club.id
food = BasicFood(name="", expiry_date=timezone.now(), owner_id=club_id)
if PermissionBackend.check_perm(self.request, "food.add_basicfood", food):
owner_id = club_id
return BasicFood(
name="",
expiry_date=timezone.now(),
owner_id=owner_id,
)
def get_context_data(self, **kwargs):
# Some field are hidden on create
context = super().get_context_data(**kwargs)
form = context['form']
form.fields['is_active'].widget = HiddenInput()
form.fields['was_eaten'].widget = HiddenInput()
copy = self.request.GET.get('copy', None)
if copy is not None:
basic = BasicFood.objects.get(pk=copy)
for field in ['date_type', 'expiry_date', 'name', 'owner']:
form.fields[field].initial = getattr(basic, field)
for field in ['allergens']:
form.fields[field].initial = getattr(basic, field).all()
return context
class QRCodeCreateView(ProtectQuerysetMixin, ProtectedCreateView):
"""
A view to add a new qrcode
"""
model = QRCode
template_name = 'food/create_qrcode_form.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:
return HttpResponseRedirect(reverse("food:qrcode_view", kwargs=kwargs))
else:
return super().get(*args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["slug"] = self.kwargs["slug"]
context["last_basic"] = BasicFood.objects.order_by('-pk').all()[:10]
return context
@transaction.atomic
def form_valid(self, form):
form.instance.creater = self.request.user
qrcode_food_form = QRCodeForms(data=self.request.POST)
if not qrcode_food_form.is_valid():
return self.form_invalid(form)
# Save the qrcode
qrcode = form.save(commit=False)
qrcode.qr_code_number = self.kwargs["slug"]
qrcode._force_save = True
qrcode.save()
qrcode.refresh_from_db()
qrcode.food_container.save()
return super().form_valid(form)
def get_success_url(self, **kwargs):
self.object.refresh_from_db()
return reverse('food:qrcode_view', kwargs={"slug": self.kwargs['slug']})
def get_sample_object(self):
return QRCode(
qr_code_number=self.kwargs["slug"],
food_container_id=1
)
class QRCodeView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
"""
A view to see a qrcode
"""
model = QRCode
extra_context = {"title": _("QRCode")}
context_object_name = "qrcode"
slug_field = "qr_code_number"
def get(self, *args, **kwargs):
qrcode = kwargs["slug"]
if self.model.objects.filter(qr_code_number=qrcode).count() > 0:
return super().get(*args, **kwargs)
else:
return HttpResponseRedirect(reverse("food:qrcode_create", kwargs=kwargs))
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
qr_code_number = self.kwargs['slug']
qrcode = self.model.objects.get(qr_code_number=qr_code_number)
model = qrcode.food_container.polymorphic_ctype.model
if model == "basicfood":
context["can_update_basic"] = PermissionBackend.check_perm(self.request, "food.change_basicfood")
context["can_view_detail"] = PermissionBackend.check_perm(self.request, "food.view_basicfood")
if model == "transformedfood":
context["can_update_transformed"] = PermissionBackend.check_perm(self.request, "food.change_transformedfood")
context["can_view_detail"] = PermissionBackend.check_perm(self.request, "food.view_transformedfood")
context["can_add_ingredient"] = PermissionBackend.check_perm(self.request, "food.change_transformedfood")
return context
class TransformedFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView):
"""
A view to add a tranformed food
"""
model = TransformedFood
template_name = 'food/transformedfood_form.html'
form_class = TransformedFoodForms
extra_context = {"title": _("Add a new meal")}
@transaction.atomic
def form_valid(self, form):
form.instance.creater = self.request.user
transformed_food_form = TransformedFoodForms(data=self.request.POST)
if not transformed_food_form.is_valid():
return self.form_invalid(form)
# Save the aliment and allergens associated
transformed_food = form.save(commit=False)
transformed_food.expiry_date = transformed_food.creation_date
transformed_food.is_active = True
transformed_food.is_ready = False
transformed_food.was_eaten = False
transformed_food._force_save = True
transformed_food.save()
transformed_food.refresh_from_db()
ans = super().form_valid(form)
transformed_food.update()
return ans
def get_success_url(self, **kwargs):
self.object.refresh_from_db()
return reverse('food:food_view', kwargs={"pk": self.object.pk})
def get_sample_object(self):
# We choose a club which may work or BDE else
owner_id = 1
for membership in self.request.user.memberships.all():
club_id = membership.club.id
food = TransformedFood(name="",
creation_date=timezone.now(),
expiry_date=timezone.now(),
owner_id=club_id)
if PermissionBackend.check_perm(self.request, "food.add_transformedfood", food):
owner_id = club_id
break
return TransformedFood(
name="",
owner_id=owner_id,
creation_date=timezone.now(),
expiry_date=timezone.now(),
)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# Some field are hidden on create
form = context['form']
form.fields['is_active'].widget = HiddenInput()
form.fields['is_ready'].widget = HiddenInput()
form.fields['was_eaten'].widget = HiddenInput()
form.fields['shelf_life'].widget = HiddenInput()
return context
class TransformedFoodUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
"""
A view to update transformed product
"""
model = TransformedFood
template_name = 'food/transformedfood_form.html'
form_class = TransformedFoodForms
extra_context = {'title': _('Update a meal')}
@transaction.atomic
def form_valid(self, form):
form.instance.creater = self.request.user
transformedfood_form = TransformedFoodForms(data=self.request.POST)
if not transformedfood_form.is_valid():
return self.form_invalid(form)
ans = super().form_valid(form)
form.instance.update()
return ans
def get_success_url(self, **kwargs):
self.object.refresh_from_db()
return reverse('food:food_view', kwargs={"pk": self.object.pk})
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
return context
class TransformedListView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, ListView):
"""
Displays ready TransformedFood
"""
model = TransformedFood
tables = [TransformedFoodTable, TransformedFoodTable, TransformedFoodTable]
extra_context = {"title": _("Transformed food")}
def get_queryset(self, **kwargs):
return super().get_queryset(**kwargs).distinct()
def get_tables(self):
tables = super().get_tables()
tables[0].prefix = "all-"
tables[1].prefix = "open-"
tables[2].prefix = "served-"
return tables
def get_tables_data(self):
# first table = all transformed food, second table = free, third = served
return [
self.get_queryset().order_by("-creation_date"),
TransformedFood.objects.filter(is_ready=True, is_active=True, was_eaten=False, expiry_date__lt=timezone.now())
.filter(PermissionBackend.filter_queryset(self.request, TransformedFood, "view"))
.distinct()
.order_by("-creation_date"),
TransformedFood.objects.filter(is_ready=True, is_active=True, was_eaten=False, expiry_date__gte=timezone.now())
.filter(PermissionBackend.filter_queryset(self.request, TransformedFood, "view"))
.distinct()
.order_by("-creation_date")
]
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# We choose a club which should work
for membership in self.request.user.memberships.all():
club_id = membership.club.id
food = TransformedFood(
name="",
owner_id=club_id,
creation_date=timezone.now(),
expiry_date=timezone.now(),
)
if PermissionBackend.check_perm(self.request, "food.add_transformedfood", food):
context['can_create_meal'] = True
break
tables = context["tables"]
for name, table in zip(["table", "open", "served"], tables):
context[name] = table
return context

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,9 +15,9 @@ 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', ]
ordering_fields = ['timestamp', ]
ordering = ['-timestamp', ]
ordering_fields = ['timestamp', 'id', ]
ordering = ['-id', ]

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

@ -0,0 +1,37 @@
# Generated by Django 2.2.16 on 2020-09-04 21:41
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('contenttypes', '0002_remove_content_type_name'),
]
operations = [
migrations.CreateModel(
name='Changelog',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('ip', models.GenericIPAddressField(blank=True, null=True, verbose_name='IP Address')),
('instance_pk', models.CharField(max_length=255, verbose_name='identifier')),
('previous', models.TextField(null=True, verbose_name='previous data')),
('data', models.TextField(null=True, verbose_name='new data')),
('action', models.CharField(choices=[('create', 'create'), ('edit', 'edit'), ('delete', 'delete')], default='edit', max_length=16, verbose_name='action')),
('timestamp', models.DateTimeField(default=django.utils.timezone.now, verbose_name='timestamp')),
('model', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='contenttypes.ContentType', verbose_name='model')),
('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='user')),
],
options={
'verbose_name': 'changelog',
'verbose_name_plural': 'changelogs',
},
),
]

View File

@ -0,0 +1,17 @@
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('logs', '0001_initial'),
]
operations = [
migrations.RunSQL(
"UPDATE logs_changelog SET previous = '' WHERE previous IS NULL;"
),
migrations.RunSQL(
"UPDATE logs_changelog SET data = '' WHERE data IS NULL;"
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 2.2.16 on 2020-09-06 19:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('logs', '0002_replace_null_by_blank'),
]
operations = [
migrations.AlterField(
model_name='changelog',
name='data',
field=models.TextField(blank=True, default='', verbose_name='new data'),
),
migrations.AlterField(
model_name='changelog',
name='previous',
field=models.TextField(blank=True, default='', verbose_name='previous data'),
),
]

View File

@ -1,10 +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.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.db import models
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
@ -43,12 +44,14 @@ class Changelog(models.Model):
)
previous = models.TextField(
null=True,
blank=True,
default="",
verbose_name=_('previous data'),
)
data = models.TextField(
null=True,
blank=True,
default="",
verbose_name=_('new data'),
)
@ -68,14 +71,18 @@ class Changelog(models.Model):
timestamp = models.DateTimeField(
null=False,
blank=False,
auto_now_add=True,
default=timezone.now,
name='timestamp',
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")
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
@ -23,6 +23,9 @@ EXCLUDED = [
'cas_server.userattributes',
'contenttypes.contenttype',
'logs.changelog', # Never remove this line
'mailer.dontsendentry',
'mailer.message',
'mailer.messagelog',
'migrations.migration',
'note.note' # We only store the subclasses
'note.transaction',
@ -47,22 +50,19 @@ def save_object(sender, instance, **kwargs):
in order to store each modification made
"""
# noinspection PyProtectedMember
if instance._meta.label_lower in EXCLUDED:
return
if hasattr(instance, "_no_log"):
if instance._meta.label_lower in EXCLUDED or hasattr(instance, "_no_signal"):
return
# 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,26 +71,51 @@ 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
# On crée notre propre sérialiseur JSON pour pouvoir sauvegarder les modèles
changed_fields = '__all__'
if previous:
# On ne garde que les champs modifiés
changed_fields = []
for field in instance._meta.fields:
if field.name.endswith("_ptr"):
# A field ending with _ptr is a OneToOneRel with a subclass, e.g. NoteClub.note_ptr -> Note
continue
if getattr(instance, field.name) != getattr(previous, field.name):
changed_fields.append(field.name)
if len(changed_fields) == 0:
# Pas de log s'il n'y a pas de modification
return
# On crée notre propre sérialiseur JSON pour pouvoir sauvegarder les modèles avec uniquement les champs modifiés
class CustomSerializer(ModelSerializer):
class Meta:
model = instance.__class__
fields = '__all__'
fields = changed_fields
previous_json = JSONRenderer().render(CustomSerializer(previous).data).decode("UTF-8") if previous else None
previous_json = JSONRenderer().render(CustomSerializer(previous).data).decode("UTF-8") if previous else ""
instance_json = JSONRenderer().render(CustomSerializer(instance).data).decode("UTF-8")
if previous_json == instance_json:
# Pas de log s'il n'y a pas de modification
return
Changelog.objects.create(user=user,
ip=ip,
model=ContentType.objects.get_for_model(instance),
@ -106,19 +131,16 @@ def delete_object(sender, instance, **kwargs):
Each time a model is deleted, an entry in the table `Changelog` is added in the database
"""
# noinspection PyProtectedMember
if instance._meta.label_lower in EXCLUDED:
if instance._meta.label_lower in EXCLUDED or hasattr(instance, "_no_signal"):
return
if hasattr(instance, "_no_log"):
return
# 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()
# 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()
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)
@ -127,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):
@ -141,6 +177,6 @@ def delete_object(sender, instance, **kwargs):
model=ContentType.objects.get_for_model(instance),
instance_pk=instance.pk,
previous=instance_json,
data=None,
data="",
action="delete"
).save()

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,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 django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.models import User
from django.utils.translation import gettext_lazy as _
from note.templatetags.pretty_money import pretty_money
from note_kfet.admin import admin_site
from .forms import ProfileForm
from .models import Club, Membership, Profile, Role
from .models import Club, Membership, Profile
class ProfileInline(admin.StackedInline):
@ -14,29 +17,50 @@ class ProfileInline(admin.StackedInline):
Inline user profile in user admin
"""
model = Profile
form = ProfileForm
can_delete = False
@admin.register(User, site=admin_site)
class CustomUserAdmin(UserAdmin):
inlines = (ProfileInline,)
list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff')
list_select_related = ('profile',)
form = ProfileForm
def get_inline_instances(self, request, obj=None):
"""
When creating a new user don't show profile one the first step
"""
if not obj:
return list()
return super().get_inline_instances(request, obj)
return super().get_inline_instances(request, obj) if obj else []
# Update Django User with profile
admin.site.unregister(User)
admin.site.register(User, CustomUserAdmin)
@admin.register(Club, site=admin_site)
class ClubAdmin(admin.ModelAdmin):
list_display = ('name', 'parent_club', 'email', 'require_memberships', 'pretty_fee_paid',
'pretty_fee_unpaid', 'membership_start', 'membership_end',)
ordering = ('name',)
search_fields = ('name', 'email',)
# Add other models
admin.site.register(Club)
admin.site.register(Membership)
admin.site.register(Role)
def pretty_fee_paid(self, obj):
return pretty_money(obj.membership_fee_paid)
def pretty_fee_unpaid(self, obj):
return pretty_money(obj.membership_fee_unpaid)
pretty_fee_paid.short_description = _("membership fee (paid students)")
pretty_fee_unpaid.short_description = _("membership fee (unpaid students)")
@admin.register(Membership, site=admin_site)
class MembershipAdmin(admin.ModelAdmin):
list_display = ('user', 'club', 'date_start', 'date_end', 'view_roles', 'pretty_fee',)
ordering = ('-date_start', 'club')
def view_roles(self, obj):
return ", ".join(role.name for role in obj.roles.all())
def pretty_fee(self, obj):
return pretty_money(obj.fee)
view_roles.short_description = _("roles")
pretty_fee.short_description = _("fee")

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
from rest_framework import serializers
from ..models import Profile, Club, Role, Membership
from ..models import Profile, Club, Membership
class ProfileSerializer(serializers.ModelSerializer):
@ -29,17 +29,6 @@ class ClubSerializer(serializers.ModelSerializer):
fields = '__all__'
class RoleSerializer(serializers.ModelSerializer):
"""
REST API Serializer for Roles.
The djangorestframework plugin will analyse the model `Role` and parse all fields in the API.
"""
class Meta:
model = Role
fields = '__all__'
class MembershipSerializer(serializers.ModelSerializer):
"""
REST API Serializer for Memberships.

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