746 Commits

Author SHA1 Message Date
Sli
1d03fcf6ea Fix windows path and shell calls issues 2025-01-07 17:10:06 +01:00
Sli
a6ba65a494 Auto download gcc for windows 2025-01-06 23:07:20 +01:00
Sli
c90fcc838e Migrate xapian install script to python 2025-01-06 23:07:20 +01:00
bc9cb9b36c Merge pull request #992 from ae-utbm/uv
Fix install documentation
2025-01-06 22:30:32 +01:00
Sli
edafc06c3f Fix install documentation 2025-01-06 22:26:46 +01:00
134f8a7989 Merge pull request #991 from ae-utbm/uv
Switch from poetry to uv
2025-01-06 22:19:19 +01:00
Sli
771cbdbd77 More explicit uv install steps 2025-01-06 21:59:36 +01:00
Sli
a491baddb9 Apply review comments 2025-01-06 20:13:41 +01:00
Sli
8d10a5e0ab Update deploy scripts to uv 2025-01-06 16:17:56 +01:00
Sli
cbe42d3a60 Add caching for virtualenv 2025-01-06 16:17:56 +01:00
Sli
0c4d72e17a Switch from poetry to uv 2025-01-06 16:17:54 +01:00
429df81ec9 Merge pull request #989 from ae-utbm/counter
Make code matching rank first in counter click
2025-01-05 19:05:10 +01:00
Sli
bb24516474 Make code matching rank first in counter click 2025-01-05 18:54:54 +01:00
8e339c3d4b Merge pull request #988 from ae-utbm/news
fix: wrong link for ae dev discord
2025-01-05 17:29:57 +01:00
Sli
25298518bc fix: wrong link for ae dev discord 2025-01-05 17:25:23 +01:00
2e26ff2cde Merge pull request #986 from ae-utbm/news
Improve welcome page
2025-01-05 17:18:55 +01:00
Sli
a8702d4f5e Improve welcome page
* Improve code readability of calendar details
* Add link to AE Dev discord in useful links
* Add link to github at the bottom
2025-01-05 16:42:26 +01:00
7f4cc5fb0f Merge pull request #980 from ae-utbm/ban-groups
Ban groups
2025-01-05 15:54:19 +01:00
e7215be00e translations 2025-01-05 15:49:30 +01:00
4f35cc00bc Add UserBan management views 2025-01-05 15:49:08 +01:00
af47587116 Split groups and ban groups 2025-01-05 15:49:08 +01:00
3c4daeadb0 Merge pull request #985 from ae-utbm/form-fixes
small form fixes
2025-01-05 15:47:44 +01:00
348ab19ac6 small form fixes
le `display:block` avait disparu des helptext, ce qui rendait leur affichage bizarre. Et il manquait quelques détails sur le `ProductForm`
2025-01-05 15:40:41 +01:00
ada74a3e42 Merge pull request #984 from ae-utbm/lock-poetry
Pin poetry version
2025-01-05 15:02:45 +01:00
785ac9bdab pin poetry version 2025-01-05 14:48:40 +01:00
d1e604e7a5 Merge pull request #975 from ae-utbm/unified-calendar
Unified calendar widget on main com page with external and internal events
2025-01-05 01:46:38 +01:00
Sli
2749a88704 Basic test for internal calendar 2025-01-05 01:36:41 +01:00
Sli
eb3db134f8 Test external calendar caching 2025-01-05 01:32:54 +01:00
Sli
fa7f5d24b0 Test external calendar api 2025-01-05 01:04:11 +01:00
Sli
ba76015c71 Use a newer ical library 2025-01-04 23:12:34 +01:00
Sli
1887a2790f Move IcsCalendar to it's own file 2025-01-04 23:08:09 +01:00
Sli
5d0fc38107 Make social icons links pretty 2025-01-04 23:08:09 +01:00
Sli
65df55a635 Use signals to update internal ics 2025-01-04 23:08:09 +01:00
Sli
a60e1f1fdc Create dedicated class to manage ics calendar files 2025-01-04 23:08:09 +01:00
Sli
0a0f44607e Return calendars as real files 2025-01-04 23:08:09 +01:00
Sli
007080ee48 Extract send_file response creation logic to a dedicated function 2025-01-04 23:08:09 +01:00
Sli
a13e3e95b7 Harmonize titles on front page 2025-01-04 23:08:09 +01:00
Sli
169938e1da Replace old agenda of event with links to services and change permission to see birthdays 2025-01-04 23:08:09 +01:00
Sli
e5fb875968 Add support for event location and more detail link 2025-01-04 22:52:17 +01:00
Sli
9bd14f1b4e Refactor popup creation 2025-01-04 22:51:45 +01:00
Sli
fd2295119d nice looking popup with well aligned icon 2025-01-04 22:51:45 +01:00
Sli
eac2709e86 Create basic (ugly) event detail popup 2025-01-04 22:51:45 +01:00
Sli
48f6d134bf Fix news page layout 2025-01-04 22:51:45 +01:00
Sli
6d7467e746 Make new calendar look like the iframe one 2025-01-04 22:51:44 +01:00
Sli
0d1629495b Refactor com scss and add basic unified event calendar 2025-01-04 22:51:44 +01:00
Sli
63839dc22b Fix poster edition and display bug 2025-01-04 22:51:44 +01:00
c627944bd1 Merge pull request #983 from ae-utbm/gettext
Remove line numbers from locale files
2025-01-04 22:50:10 +01:00
f0be4b270b remove line numbers from locale files 2025-01-04 22:03:37 +01:00
728065e771 Merge pull request #982 from ae-utbm/groups
fix get_or_create in club group migration
2025-01-04 19:01:16 +01:00
849fac490d fix get_or_create in club group migration 2025-01-04 18:49:00 +01:00
5752229312 Merge pull request #981 from ae-utbm/groups
split migrations
2025-01-04 18:14:09 +01:00
6eb860579a split migrations 2025-01-04 18:05:02 +01:00
d08d54b4c9 Merge pull request #935 from ae-utbm/groups
Remove `RealGroup` and `MetaGroup`
2025-01-04 17:13:48 +01:00
bb210f8d47 change club group names when the club name changes 2025-01-04 16:43:38 +01:00
efca10e252 remove Club.view_groups, Club.edit_groups and Club.owner_group 2025-01-03 17:30:24 +01:00
b8f851b009 translations 2025-01-03 01:18:28 +01:00
1e29ae4171 fixes on club group attribution 2025-01-03 01:18:28 +01:00
0ae1e850f4 improve admin 2025-01-03 01:18:28 +01:00
d380668c0f Move users to the club groups in the migration 2025-01-03 01:18:28 +01:00
9a72c5eb72 fix galaxy tests 2025-01-03 01:18:28 +01:00
407cfbe02b update docs 2025-01-03 01:18:28 +01:00
6400b2c2c2 replace MetaGroups by proper group management 2025-01-03 01:18:28 +01:00
cce7ecbe73 Merge pull request #974 from ae-utbm/fix-page
fix 500 error when accessing history of non-existing page
2024-12-29 15:47:38 +01:00
d200c1e381 fix 500 error when accessing history of non-existing page 2024-12-28 13:25:42 +01:00
2f9e5bfee1 Merge pull request #965 from ae-utbm/form-style
rework form style
2024-12-27 22:24:09 +01:00
11702d3d7c Merge pull request #959 from ae-utbm/counter-click-step-4
Make counter click client side first
2024-12-27 22:06:35 +01:00
Sli
43f47e2087 Improve product card display on counter click 2024-12-27 01:59:54 +01:00
4b881903f0 Merge pull request #972 from ae-utbm/fix-product-fetch
Fix product fetch
2024-12-26 23:43:41 +01:00
761e37ade6 fix product fetch 2024-12-26 17:26:06 +01:00
10ed2f7404 Merge pull request #963 from ae-utbm/fix-group-edit
Fix error when submitting group form without any group checked
2024-12-26 17:02:02 +01:00
Sli
43768f1691 Refactor counter-click css 2024-12-26 11:52:30 +01:00
Sli
280d27343d Put error popup inside the basket 2024-12-25 20:44:52 +01:00
Sli
138e1662c7 Add popup css class and display basket error messages with it on counter click 2024-12-24 00:29:23 +01:00
Sli
c80fe094a2 Remove useless form elements in counters and improve alignment 2024-12-23 20:44:49 +01:00
Sli
139221dd22 Apply review comments 2024-12-23 15:15:24 +01:00
72c2981d66 rework form style 2024-12-23 15:11:15 +01:00
Sli
6f003ffa53 Add translations 2024-12-23 02:41:41 +01:00
Sli
7f6fd7dc47 Fix wrong tests/permissions 2024-12-23 02:37:41 +01:00
Sli
ccf5118c9d Add invalid form tests 2024-12-23 02:26:39 +01:00
Sli
022c19c020 Fix counter permissions issues 2024-12-23 02:17:28 +01:00
Sli
2e5e217842 Disable eboutic in counter click/main 2024-12-23 01:35:44 +01:00
Sli
9c93c004ec Add more counter click tests 2024-12-23 01:18:01 +01:00
Sli
472800eff6 Add nice snackbar message on counter interface and fix not enough money protection on frontend 2024-12-23 00:56:57 +01:00
Sli
b8d43a629b Increase selling label size and add more counter click tests 2024-12-23 00:00:40 +01:00
Sli
f6693e12cf Basic counter click tests 2024-12-22 19:24:07 +01:00
Sli
38f491cf57 Properly test annotations in counter click 2024-12-22 16:43:07 +01:00
Sli
3464d5d860 Add proper tests for refilling view 2024-12-22 16:16:28 +01:00
81773dc800 Merge pull request #964 from ae-utbm/fix-backend
Fix custom auth backend
2024-12-22 15:07:46 +01:00
da400155eb fix SithModelBackend._get_group_permissions 2024-12-22 15:01:58 +01:00
Sli
5079938a5b Fix get_operator on non bar counters and better display of counter with no products 2024-12-22 13:36:50 +01:00
Sli
b8430adc50 Split counter-click-index.ts 2024-12-22 13:01:37 +01:00
Sli
eed434aeb2 Improve age management for getting products and make get_product a part of counter model 2024-12-22 12:27:58 +01:00
Sli
372470b44b Improve empty basket and tray price management 2024-12-22 12:06:15 +01:00
Sli
7071553c3b Optimize product id validation on counter click 2024-12-22 12:06:15 +01:00
Sli
eea237b813 Pre-filter allowed products in backend for counter click 2024-12-22 12:06:15 +01:00
Sli
c37288c285 Display nice product cards on counter click interface 2024-12-22 12:06:15 +01:00
Sli
ccf5767a01 Fix customerBalance not init and submit/cancel buttons visuals 2024-12-22 12:06:15 +01:00
Sli
ffe6fc8c2a Redirect when cancelling instead of submitting a form 2024-12-22 12:06:15 +01:00
Sli
5f0b4d2050 Properly display form errors in counter 2024-12-22 12:06:15 +01:00
Sli
f9d7dc7d3a Restore form when form submit fails due to error 2024-12-22 12:06:15 +01:00
Sli
8ebea00896 Fix crash during validation 2024-12-22 12:06:15 +01:00
Sli
a548f4744e Fix counter main
* Fix crash when submitting nothing
* Fix code field not being autofocus
2024-12-22 12:06:15 +01:00
Sli
a383f3e717 Don't use codes as a primary key in counter click 2024-12-22 12:06:15 +01:00
Sli
60f18669c8 Make counter click client side first 2024-12-22 12:06:14 +01:00
Sli
a36946529b Fix error when submitting group form without any group checked 2024-12-22 12:04:51 +01:00
eaac0c728f Merge pull request #961 from ae-utbm/auth-backend
Custom auth backend
2024-12-22 06:38:34 +01:00
9ca95774a3 Merge pull request #962 from ae-utbm/query-news
Fix N+1 queries on birthdays
2024-12-22 06:32:58 +01:00
fa66851889 fix n+1 queries on birthdays 2024-12-21 21:09:08 +01:00
ab81f11199 Manage subscribers group permissions 2024-12-21 18:52:16 +01:00
bea7741d35 populate group permissions 2024-12-21 18:48:30 +01:00
81e163812e custom auth backend 2024-12-21 17:34:20 +01:00
4f233538e0 Merge pull request #955 from ae-utbm/counter-click-step-3
Use TomSelect for product selection on counter
2024-12-21 16:00:06 +01:00
Sli
4ac09ac08b Use tomselect instead of jquery autoselect for counter clicks 2024-12-21 15:56:18 +01:00
6d02970676 Merge pull request #946 from ae-utbm/product-csv
Rework the product admin page
2024-12-21 15:50:34 +01:00
accf1befce Make products filterable by product type 2024-12-21 02:15:51 +01:00
6953eaa9d0 fix sanitization of the csv content 2024-12-21 02:14:38 +01:00
180bae59c8 Add translations 2024-12-21 02:14:38 +01:00
9cafc163e8 fix frontend archived products filter 2024-12-21 02:14:38 +01:00
8f8eef4107 display products as cards 2024-12-21 02:14:38 +01:00
7af745087e create a card css component 2024-12-21 02:14:38 +01:00
aab093200b slightly improve style 2024-12-21 02:14:38 +01:00
1a9556f811 add a button to download products as csv 2024-12-21 02:14:38 +01:00
39b36aa509 ajaxify the product admin page 2024-12-21 02:14:38 +01:00
3fc260a12c add csv converter 2024-12-21 02:14:38 +01:00
1696a2f579 Add NestedKeyOf Type 2024-12-21 02:14:38 +01:00
baebc0b690 Merge pull request #958 from ae-utbm/fix-group-form
fix user groups form
2024-12-20 11:07:13 +01:00
9f3a10ca71 fix user groups form 2024-12-20 11:00:57 +01:00
38ceaf3106 Merge pull request #957 from ae-utbm/user-model
Fix groups displayed on user profile group edition
2024-12-19 20:32:39 +01:00
Sli
87b619794d Fix groups displayed on user profile group edition 2024-12-19 18:57:50 +01:00
29c4a36479 Merge pull request #956 from ae-utbm/query-page-hist
Fix N+1 queries on page history
2024-12-19 15:09:11 +01:00
ddeb12f08c Merge pull request #929 from ae-utbm/user-model
Migrate User parent class from AbstractBaseUser to AbstractUser
2024-12-19 14:27:16 +01:00
a7b1406e06 post-rebase fix 2024-12-19 10:53:11 +01:00
871ef60cf6 remove obsolete RunPython operations 2024-12-19 10:39:07 +01:00
7e9071a533 optimize User.is_subscribed and User.was_subscribed 2024-12-19 10:39:07 +01:00
8c660e9856 Make core.User inherit from AbstractUser instead of AbstractBaseUser 2024-12-19 10:39:04 +01:00
6ca641ab7f fix: N+1 queries on page version list page 2024-12-19 10:32:02 +01:00
8d6609566f Merge pull request #951 from ae-utbm/refactor-news
refactor news model and creation form
2024-12-18 16:09:41 +01:00
17e4c63737 refactor news model and creation form 2024-12-18 15:54:10 +01:00
fad470b670 Merge pull request #952 from ae-utbm/sort-producttypes
Sort product types
2024-12-18 15:45:50 +01:00
c5646b1e59 Merge pull request #954 from ae-utbm/fix-subscription
fix access to the subscription page
2024-12-18 15:45:17 +01:00
5da27bb266 rename producttype to product_type 2024-12-18 14:48:59 +01:00
be6a077c8e fix access to the subscription page 2024-12-18 14:13:39 +01:00
8d643fc6b4 Apply review comments 2024-12-17 17:23:13 +01:00
47876e3971 Make product types dynamically orderable. 2024-12-17 13:35:29 +01:00
c79c251ba7 Add ProductTypeController 2024-12-17 13:35:29 +01:00
483670e798 Make ProductType an OrderedModel 2024-12-17 13:35:29 +01:00
6c8a6008d5 api route to search products with detailed infos. 2024-12-17 12:38:59 +01:00
e680124d7b fix makemessages command in docs 2024-12-17 12:38:59 +01:00
b06a06f50c feat: add restore on backspace plugin for tom select 2024-12-17 12:38:59 +01:00
6416de237f Merge pull request #923 from ae-utbm/counter-click-step-2
Casser counter click step 2 : separate refilling from counter clicks with fragments
2024-12-17 10:58:34 +01:00
Sli
ad44fd52a4 Apply review comments 2024-12-17 10:54:41 +01:00
Sli
03c27b10e5 Fix refill permissions
* Remove ability to refill from counters
* Fix bug where you could refill without any board member on a BAR
* Add a warning message explaining why refilling are disabled
2024-12-17 02:42:07 +01:00
Sli
fc0ef29738 Remove GetCustomer API endpoint 2024-12-17 01:42:10 +01:00
Sli
a0eb53a607 Apply review comments 2024-12-17 01:41:45 +01:00
Sli
66e5ef64fd Don't use API to update amount after a refilling query 2024-12-17 00:47:43 +01:00
f5d5cc18a8 Merge pull request #949 from ae-utbm/trombi
Fix crash when admin gets to preferences of an user subscribed to a trombinoscope
2024-12-16 10:06:17 +01:00
Sli
4c65939bbe Fix crash when admin gets to preferences of an user subscribed to a trombinoscope 2024-12-16 09:31:43 +01:00
Sli
379527cd58 Add a nice animation on successful refilling 2024-12-16 00:58:23 +01:00
Sli
f63fb59cbf Allow filtering of refilling options
* Move settings.SITH_COUNTER_PAYMENT_METHOD to counter.apps.PAYMENT_METHOD
* Move student cards to an accordion on counter click
* Make cash default refilling option
* Disable bank selection option in refilling if CHECK are not allowed
* Disable refilling with CHECK from the frontend
2024-12-16 00:15:21 +01:00
Sli
cde864fdc7 Apply review comments 2024-12-15 22:47:59 +01:00
Sli
e9361697f7 Convert customer refill to a fragment view 2024-12-15 21:33:19 +01:00
830c752971 Merge pull request #948 from ae-utbm/sentry
Enable sentry workflow again
2024-12-15 18:36:46 +01:00
Sli
6bdc1b73ae Enable sentry workflow again 2024-12-15 17:31:41 +01:00
0f003870bb Merge pull request #924 from ae-utbm/unique-student-card
Make student card unique per user
2024-12-15 17:06:35 +01:00
Sli
0631c77a1c Apply review comments 2024-12-15 17:02:44 +01:00
Sli
2cc4308a58 Fix tooltip shadow and position and improve unittests 2024-12-15 16:49:24 +01:00
Sli
4975475e85 Add tooltip on current registered card, allow barmen to delete cards and make card deletion a fragment 2024-12-15 16:49:24 +01:00
466fe58763 feat: make student card unique per user 2024-12-15 16:49:24 +01:00
3b7e338808 fix 500 when accessing preferences
Quand on tente d'accéder aux préférences d'un utilisateur relié à un trombi, sans être soi-même dans un trombi, on a une erreur.
2024-12-15 16:49:24 +01:00
53b13e7aef Merge pull request #947 from ae-utbm/dependencies
Upgrade dependencies
2024-12-15 13:53:28 +01:00
Sli
fa60ecb25a Upgrade dependencies 2024-12-15 00:59:55 +01:00
a975824481 Merge pull request #945 from ae-utbm/refactor-product
Remove `Product.parent_product`
2024-12-09 20:20:11 +01:00
c51e5eb6cb remove parent_product column in the Product table 2024-12-09 12:59:33 +01:00
f0bc502ec9 fix translation in subscription creation success fragment 2024-12-09 12:31:58 +01:00
902cafc5e4 Merge pull request #921 from ae-utbm/counter-click
Casser counter click étape 1 : introduire des fragments
2024-12-08 13:49:08 +01:00
b2f54aa23e Merge pull request #943 from ae-utbm/update-deps
Update deps
2024-12-08 13:46:53 +01:00
Sli
29a5425259 Add spinner to student card form 2024-12-08 13:17:56 +01:00
e2a34c75ea deps: update dependencies 2024-12-08 11:54:58 +01:00
Sli
de7aa6f6a6 Create a generic form fragment renderer 2024-12-08 11:45:16 +01:00
9acb421b2e deps: update ruff 2024-12-08 11:17:27 +01:00
Sli
66d2dc74e7 Pre-fetch forms for student card 2024-12-08 00:32:28 +01:00
Sli
2f613607af Update number of queries in test_num_queries 2024-12-07 23:35:35 +01:00
Sli
d4b9c3afb1 Make StudentCardFormView fragment only 2024-12-07 22:36:15 +01:00
Sli
b81cf49d0a Remove student card creation from CounterClick view and use fragment instead
Intercept htmx on submit requests, this allows auto submit from nfc fields

Fix super call with parameters

Add loading wheel on student card form for counter_click.jinja
2024-12-07 12:57:10 +01:00
1da45fdffc Merge pull request #934 from ae-utbm/split-counter
Split counter views into multiple files
2024-12-07 11:53:14 +01:00
10dde3f002 fix imports 2024-12-07 00:18:17 +01:00
c2d6af12ab Merge branches 'split-home' and 'split-studentcard' into split-counter 2024-12-07 00:13:50 +01:00
6e48f88c06 extract counter auth views 2024-12-07 00:12:10 +01:00
7a91a71565 extract counter auth views 2024-12-07 00:11:18 +01:00
c4764110d8 extract counter home views 2024-12-07 00:10:46 +01:00
ff68e65250 extract counter home views 2024-12-07 00:07:37 +01:00
c9d83e5916 extract student card views 2024-12-07 00:06:33 +01:00
5dc99dbfcb extract student card views 2024-12-07 00:05:45 +01:00
8dbec85c8e Merge pull request #941 from ae-utbm/optimize-search
Optimize search
2024-12-06 21:00:06 +01:00
84d7e40e66 feat: client-side cache for ajax-select inputs 2024-12-06 18:38:30 +01:00
0b509f2200 fix N+1 queries on user search 2024-12-06 18:38:30 +01:00
9591162cc9 Merge pull request #940 from ae-utbm/fix-dump
Fix the account dump command.
2024-12-05 19:52:07 +01:00
007e17fd8b Fix the account dump command.
- a missing `fail_silently` flag made the whole command fail if an invalid recipient is used (like closed utbm mail address)
- Not specifying the seller make the account detail pages crash.
2024-12-05 12:50:40 +01:00
95f8e7517c Merge pull request #932 from ae-utbm/fix-subscriptions
Rework the subscription page
2024-12-03 19:45:26 +01:00
9667c79162 remove htmx-ext-response-targets 2024-12-03 19:41:10 +01:00
1c79c25262 better tab style 2024-12-03 19:41:09 +01:00
04b4b34bfe add back user profiles on subscription form 2024-12-03 19:41:09 +01:00
fc0e689d4e add initial values to forms 2024-12-03 19:41:09 +01:00
83bb4b3b12 add translation 2024-12-03 19:41:09 +01:00
8dcfc604a0 write tests 2024-12-03 19:41:09 +01:00
d2d639e5f6 Split SubscriptionForm into SubscriptionNewUserForm and SubscriptionExistingUserForm 2024-12-03 19:41:09 +01:00
b3eb7693e3 Merge pull request #933 from ae-utbm/remove-stock
delete stock application
2024-11-28 23:20:35 +01:00
10f42b1522 fix imports 2024-11-27 19:03:34 +01:00
76e9f3b1dc Merge branches 'split-cash', 'split-click', 'split-main', 'split-admin', 'split-mixins', 'split-eticket' and 'split-invoices' into split-clean 2024-11-27 18:49:40 +01:00
d0ff9bc16c extract mixins views 2024-11-27 18:48:06 +01:00
5e4ebd16f9 extract mixins views 2024-11-27 18:47:55 +01:00
d2b19424ff extract eticket views 2024-11-27 18:47:18 +01:00
08286254cd extract eticket views 2024-11-27 18:47:03 +01:00
4805c39b45 extract cash views 2024-11-27 18:46:24 +01:00
f845bbf20a extract cash views 2024-11-27 18:45:27 +01:00
71c7158124 extract invoice views 2024-11-27 18:43:26 +01:00
c4643ee52c extract invoice views 2024-11-27 18:42:50 +01:00
b46b0882f3 extract admin views 2024-11-27 18:42:26 +01:00
1c4efc9431 extract admin views 2024-11-27 18:41:47 +01:00
4133e0ccdd extract click views 2024-11-27 18:41:12 +01:00
de415e7e75 split click views 2024-11-27 18:40:38 +01:00
9d17524f45 extract main views 2024-11-27 18:00:48 +01:00
68ad9650af extract main views 2024-11-27 17:56:44 +01:00
8d4d8a3abc create views package 2024-11-27 17:07:08 +01:00
9617e29ed5 delete stock application 2024-11-26 17:35:10 +01:00
75406f7b58 Tabs jinja component 2024-11-26 16:17:44 +01:00
70f5ae4f9c Move subscription forms to subscription/forms.py 2024-11-26 16:17:44 +01:00
ff307f1d65 Merge pull request #928 from ae-utbm/vite
Integrate vite manifests in django
2024-11-22 18:34:49 +01:00
d7ae601c52 Merge pull request #911 from ae-utbm/skia/fix_user_profile_picture
core: fix user profile picture size
2024-11-21 19:13:36 +01:00
33b9ff78bb Merge pull request #913 from ae-utbm/dump-accounts
Dump accounts
2024-11-21 18:39:49 +01:00
Sli
0739ce2fb4 Improve readability and usability 2024-11-21 00:33:40 +01:00
Sli
8fc1a754de Integrates vite manifests to django 2024-11-20 18:24:28 +01:00
Sli
ca8c1c9d92 Mirror -index.css generation with their import location in -index.js/ts files 2024-11-19 21:22:14 +01:00
Sli
0485ab1120 Remove defer from script where type=module is used 2024-11-19 21:22:14 +01:00
Sli
8a8851847c Passage de webpack à vite.dev 2024-11-19 21:22:14 +01:00
Sli
7b41051d0d Go for a more generic js bundling architecture
* Don't tie the output name to webpack itself
* Don't call js bundling webpack in python code
* Make the doc more generic about js bundling
2024-11-19 21:22:14 +01:00
3db1f592e2 Merge pull request #927 from ae-utbm/password-and-username
Improve password and username generation
2024-11-19 17:39:54 +01:00
6853ec0b69 make random password generation safe 2024-11-19 13:21:08 +01:00
3b39049c20 Make User.generate_username less stupid 2024-11-19 13:07:59 +01:00
37d1669a72 typo in docstrings
Co-authored-by: NaNoMelo <56289688+NaNoMelo@users.noreply.github.com>
2024-11-19 00:48:35 +01:00
ee9f36d883 implement the dump_accounts command 2024-11-19 00:48:35 +01:00
e712f9fdb8 improve counter dump admin 2024-11-19 00:43:17 +01:00
Sli
9991f5dc64 Create nice animation when scanning nfc cards 2024-11-15 14:51:45 +01:00
Sli
fce6c3d29c Convert nfc input to a web component 2024-11-15 14:51:45 +01:00
346439076e Merge pull request #922 from ae-utbm/ci
Fix CI
2024-11-15 14:46:05 +01:00
Sli
5e8d8b8d5d Revert back curl install of poetry in pipelines 2024-11-15 14:41:25 +01:00
db9f86c41e Merge pull request #919 from ae-utbm/ts-eboutic
Migrate eboutic to Typescript
2024-11-14 11:07:37 +01:00
c7adde62eb reset poetry cache in github CI 2024-11-13 23:50:43 +01:00
34559dda08 migrate eboutic to typescript 2024-11-13 23:26:05 +01:00
37c4621e9e Merge pull request #912 from ae-utbm/refactor_populate
Refactor populate
2024-11-13 15:43:18 +01:00
dd7ed290f5 Merge pull request #883 from ae-utbm/htmx
Introduce htmx in sith files
2024-11-13 15:35:24 +01:00
Sli
dc1e1fc897 Fix typos 2024-11-12 21:38:38 +01:00
Sli
37abde04d7 Improve fragment doc 2024-11-11 13:56:34 +01:00
Sli
40f2f7033e Add test for AllowFragment mixin 2024-11-11 13:49:38 +01:00
Sli
aebf909dc6 Apply review comments 2024-11-11 13:49:38 +01:00
Sli
ec7d45fd91 Add documentation for htmx 2024-11-11 13:49:38 +01:00
Sli
3af5d96bf5 Introduce htmx in sith files
* Convert FileModerationView into ListView and add pagination with htmx
* Don't allow sas moderation in file moderation view
* Split up base.jinja and introduce base_fragment.jinja
* Improve FileModerationView performances and make it root only
* Add permissions tests for file modération
2024-11-11 13:49:38 +01:00
c7a8a1a91c refactor CI 2024-11-11 13:28:44 +01:00
2dd434d987 Merge pull request #917 from ae-utbm/doc
Fix some doc typos
2024-11-11 12:47:51 +01:00
Sli
5e954bae6a Fix some doc typos 2024-11-11 00:32:04 +01:00
a97dba18c2 Reduce width of non-multiple ajax selects 2024-11-11 00:26:16 +01:00
26770de40e Make selected option more visible 2024-11-11 00:26:16 +01:00
583d4ddfb8 Use less requests in GetUserForm.clean 2024-11-11 00:26:16 +01:00
486047b929 remove the honeypot from the login page
Des utilisateurs humains se font régulièrement "éclairer" par le honeypot. Les mesures anti-bot ne devraient pas bloquer des humains.
2024-11-11 00:22:07 +01:00
b65ec6463b fix picture display in profile page 2024-11-10 16:18:56 +01:00
7cc13ea669 Merge pull request #899 from ae-utbm/ajax-select
Improve ajax select
2024-11-10 13:37:57 +01:00
c2efc969d0 refactor populate.py 2024-11-10 02:59:43 +01:00
b091fee035 custom queryset method to bulk update customer balance 2024-11-10 02:59:43 +01:00
2a0f2454f4 core: fix user profile picture size
Since 28f397574f and the removal of the
`flex-basis: 50px` property from `user_profile_pictures_thumbnails`,
the main picture was always displayed small-ish, at least on Firefox.
Setting back a flex-basis helps getting more consistent behavior once
again.
2024-11-07 15:51:43 +01:00
97ea1763f1 Merge pull request #910 from ae-utbm/logo-25
Add promo 25 logo
2024-11-07 15:25:06 +01:00
b9f51596e9 Add promo 25 logo 2024-11-07 13:39:24 +01:00
Sli
0610794dbe Fix ajax-select visual 2024-10-28 18:18:56 +01:00
Sli
a6b32fcad1 Fix readability and avoid instantiating too many TypeAdapter 2024-10-28 18:08:13 +01:00
Sli
e583e78a4e Convert the whole request to json at once on select widget 2024-10-21 17:11:07 +02:00
Sli
3eb3feea49 Fix deprecated usage of schema json method and avoid multiple inheritance on select widgets 2024-10-21 16:14:00 +02:00
Sli
935914428b Remove ajax_select completely 2024-10-21 13:30:12 +02:00
Sli
ab63ba1c54 Remove ajax_select from accounting 2024-10-21 13:26:11 +02:00
Sli
afdc6b69df Remove ajax_select from sas 2024-10-21 10:30:35 +02:00
Sli
8b419dcee6 Remove ajax_select from core 2024-10-20 23:25:56 +02:00
Sli
e7181257e3 Remove ajax_select from core/views/forms.py 2024-10-20 23:04:54 +02:00
Sli
8e7c09332f Remove ajax_select from core/views/group.py 2024-10-20 22:58:39 +02:00
Sli
d9ea5e5538 Remove ajax_select from trombi 2024-10-20 22:41:35 +02:00
Sli
a21460a1b8 Remove ajax_select from subscriptions 2024-10-20 22:36:55 +02:00
Sli
b6a480ff61 Remove ajax_select from forum 2024-10-20 22:29:07 +02:00
Sli
84ee6dd2f5 Remove ajax_select from clubs 2024-10-20 21:28:25 +02:00
Sli
a950585a02 Remove ajax_select from rootplace 2024-10-20 20:55:07 +02:00
Sli
7f8a2c1eaf Remove ajax_select from counters 2024-10-20 20:55:05 +02:00
Sli
125157fdf4 Move gettext to the top 2024-10-20 18:35:55 +02:00
Sli
517263dd58 Automatically move inner html in created node when inheriting from HTMLElement 2024-10-20 18:29:48 +02:00
Sli
301fc73687 Fix markdown input initial value and crash when alpine is not loaded 2024-10-20 18:13:48 +02:00
Sli
45441c351d Improve ajax-select style 2024-10-20 17:37:51 +02:00
Sli
be5ce414ba Add proper delete button and fix item ordering 2024-10-20 16:57:38 +02:00
Sli
bb3f277ba5 Extract js and css from select widgets to editable class attributes 2024-10-20 13:40:59 +02:00
23049a8ae2 Merge pull request #901 from ae-utbm/improve-warning-dump
Improve warning dump
2024-10-20 13:35:08 +02:00
Sli
8bbebfdb13 Add AutoCompleteSelectGroup 2024-10-20 13:33:44 +02:00
662b4b5c53 precise that dumped users can still subscribe 2024-10-20 12:45:37 +02:00
9675b6372c add flags to the dump warning mail command 2024-10-20 12:32:28 +02:00
03afd49115 make the mail text only 2024-10-20 12:32:28 +02:00
Sli
0af3505c2a Make a generic AjaxSelect abstract class 2024-10-20 02:26:32 +02:00
Sli
f78b968075 Move markdown input and select widgets to a widget folder 2024-10-20 01:05:34 +02:00
7d40316044 Merge pull request #900 from ae-utbm/optimize-again
optimize product pages again
2024-10-20 00:55:27 +02:00
Sli
e3dcad62cc Migrates lookups
* products
* files
* Groups
* Clubs
* Accounting
2024-10-20 00:47:31 +02:00
db6a871854 optimize product pages again 2024-10-20 00:27:25 +02:00
Sli
ce4f57bd8f Add ajax user widget and remove ajax_select from elections 2024-10-19 22:06:34 +02:00
Sli
8be8328830 Create select widget based on tomselect on django backend
Replace make_ajax in elections by the new widget
2024-10-19 21:32:58 +02:00
Sli
0a0092e189 Add link-once and script-once web components 2024-10-19 18:55:32 +02:00
Sli
c50f0a2ac5 Simplify ajax-select inheritance and make simple auto complete 2024-10-19 16:02:54 +02:00
Sli
6b3012d21c Fix broken sas ui in webkit based browsers 2024-10-18 23:50:04 +02:00
Sli
729f848c14 Add min-characters-for-search attribute for user-ajax-select 2024-10-18 23:34:37 +02:00
Sli
56cc4776a6 Create base class for ajax-select 2024-10-18 23:26:04 +02:00
b9cbba2309 Merge pull request #896 from ae-utbm/relpace-select2
Replace selec2 with tom-select
2024-10-18 00:24:09 +02:00
Sli
4165f8d4af Add register decorator for web components and a better inheriting system for html elements 2024-10-17 23:14:54 +02:00
Sli
cac185634d Avoid keeping text after selecting item 2024-10-17 18:21:51 +02:00
Sli
66dceefcf0 Fix bad constructor when adding attrs that are not part of the parent and fix tom-select on safari 2024-10-17 18:15:55 +02:00
Sli
677ff51ea5 Create web component util 2024-10-17 18:15:55 +02:00
Sli
645b8a543e Make easymde compatible with safari 2024-10-17 18:15:55 +02:00
Sli
74a506c48b Add missing features
* Fix display
* Add internationalization
* Avoid querying under a certain amount of characters
* Update docs for translations with typescript
* Add interpolate to typescript globals
2024-10-17 18:15:55 +02:00
Sli
deda2b4055 Replace selec2 with tom-select 2024-10-17 18:15:55 +02:00
67ebb90ffa Merge pull request #897 from ae-utbm/fix-xss
Fix xss on select2 results
2024-10-17 12:10:08 +02:00
5d16ba135a fix: xss on select2 results 2024-10-17 08:15:34 +02:00
150d08dc45 Merge pull request #894 from ae-utbm/sentry
Test sentry-debug endpoint
2024-10-15 20:48:01 +02:00
Sli
c1a85486cc Add test for sentry-debug endpoint 2024-10-15 14:09:51 +02:00
d16a207a83 Add more Ruff rules (#891)
* ruff: apply rule F

* ruff: apply rule E

* ruff: apply rule SIM

* ruff: apply rule TCH

* ruff: apply rule ERA

* ruff: apply rule PLW

* ruff: apply rule FLY

* ruff: apply rule PERF

* ruff: apply rules FURB & RUF
2024-10-15 11:36:26 +02:00
Sli
d114b01bcc Make sure Alpine is always loaded when using markdown-input component 2024-10-15 00:28:43 +02:00
Sli
dee54c3b41 Use manifest storage and correct webpack conversion on easymde form 2024-10-15 00:28:43 +02:00
Sli
670d2fa12e Use a web component for easymde
* Bump tsconfig output to es6
* Fix wrong import behavior on typescript according to webpack's doc
* Create an easymde component
2024-10-15 00:28:43 +02:00
a68e47ce8c Merge pull request #890 from ae-utbm/update-python-deps
Upgrade python dependencies
2024-10-14 15:45:18 +02:00
Sli
0314aa6733 Upgrade python dependencies
* Upgrade pre-commit
* Upgrade model-bakery
* Uprgade mkdocstrings
* Upgrade mkdocstrings-python
* Upgrade mkdocs-material
2024-10-14 15:16:07 +02:00
496ad7ce9b Merge pull request #868 from ae-utbm/delete-picture-confirm-button
Delete picture confirm button
2024-10-14 14:12:50 +02:00
efdd4a6b16 fix ruff breakpoint 2024-10-14 01:59:24 +02:00
Sli
0b31b215f6 Remove check_front command 2024-10-14 00:55:48 +02:00
Sli
7e1734aed5 Migrate chartjs to npm 2024-10-14 00:55:48 +02:00
19cd51043a feat: display moderation requests to moderators 2024-10-14 00:47:07 +02:00
5348a451e9 feat: picture moderation requests 2024-10-14 00:45:52 +02:00
83ae21140d move SAS forms to their own file 2024-10-14 00:45:52 +02:00
Sli
cdf9519a9f Port galaxy to webpack 2024-10-13 20:09:55 +02:00
d77358eaac Merge pull request #879 from ae-utbm/optimize-products-page
optimize: product list views
2024-10-13 19:20:01 +02:00
Sli
9609a7615b Don't apply js minification to webpack generated files 2024-10-13 17:14:21 +02:00
Sli
361a06e5b3 Migrate sentry to webpack 2024-10-13 17:14:21 +02:00
Sli
1720307c21 Add biome for typescript in vscode config 2024-10-13 17:00:09 +02:00
15ae24f0bd optimize: product list views 2024-10-13 12:32:50 +02:00
143713fac1 Merge pull request #878 from ae-utbm/fix-invoices
fix: InvoiceQuerySet.annotate_total() (but this time good)
2024-10-13 11:57:59 +02:00
e4845b580b fix: invoices month grouping 2024-10-13 11:47:22 +02:00
Sli
40c623b202 Optimize select2 import 2024-10-13 10:55:15 +02:00
Sli
092ace8432 Add commands to easily analyze webpack outputs 2024-10-13 10:55:15 +02:00
Sli
00cf619c68 Remove hand crafted urls on viewer-index.ts 2024-10-13 10:55:15 +02:00
Sli
b6e1c3bc88 Add helper function to export ts functions to html 2024-10-13 10:55:15 +02:00
Sli
3b1d06a71d Update select2 documentation 2024-10-13 10:55:15 +02:00
Sli
a5d8c96bab Remove select2 from vendored
* Make core/utils/select2.ts
* Convert viewer-index.js to typescript
2024-10-13 10:55:15 +02:00
564d95f701 fix: InvoiceQuerySet.annotate_total() (but for real this time) 2024-10-13 10:37:48 +02:00
Sli
768e2867b5 Fix wrong formatter doc on vscode 2024-10-12 19:43:07 +02:00
Sli
f07a855e7e Remove history management from script.js and migrate sas albums to webpack 2024-10-12 19:19:23 +02:00
2fa9daf627 Merge pull request #872 from ae-utbm/invoices-bug
fix: InvoiceQuerySet.annotate_total()
2024-10-12 19:18:37 +02:00
a1bae7ced3 fix empty options in paginated with typescript 2024-10-12 18:59:06 +02:00
7312580a8d fix: InvoiceQuerySet.annotate_total() 2024-10-12 15:52:40 +02:00
1c774aa4a0 Merge pull request #861 from ae-utbm/mail-inactives
Send mail to inactive users
2024-10-12 15:33:23 +02:00
29b32f6cbf Tell the customer balance in the warning mail 2024-10-11 09:59:03 +02:00
465e0f31d9 write command test 2024-10-11 09:57:46 +02:00
5a8052ae47 send mail to inactive users 2024-10-11 09:57:41 +02:00
6a64e05247 select inactive users 2024-10-11 09:45:54 +02:00
81a64eed08 Merge pull request #867 from ae-utbm/barmen-link
Better UX and performance for counter state display
2024-10-11 09:31:12 +02:00
29b27dc626 Merge pull request #866 from ae-utbm/openapi
Typescript support and auto generated typescript client API
2024-10-11 09:30:35 +02:00
ca25a12be0 Increase the barmen timeout limit
La limite actuelle est trop faible. En soirée, on s'en fout. Mais en journée, c'est terriblement chiant. Certains barmens passent leur temps à rafraichir la la page, certains mettent un rechargement auto à intervalles réguliers (ce qui tue le concept du timeout), et d'autres encore ont juste arrêté d'y prêter attention (mais le comptoir apparait alors comme fermé, et des étudiants qui auraient pu venir au Foyer ne viennent finalement pas)
2024-10-10 19:38:49 +02:00
c0a6f5eb30 Optimize barmen timeout and counter state fetch
Le timeout se fait en une seule requête et la récupération de l'état des comptoirs en une seule requête aussi. Grâce à ça, on peut en grande partie retirer le cache pour l'affichage de l'état des comptoirs, ce qui a des implications excellentes en termes d'UX (comme le fait que la redirection vers la page de comptoir ou d'activité aura plus une apparence de truc aléatoire)
2024-10-10 19:38:49 +02:00
4bc4d266c2 Remove the question mark from the counter state
En raison de la manière dont le timeout marche et de l'activité des comptoirs, la notion de "comptoir inactif" n'est pas intuitive. Un comptoir est ouvert ou fermé. Point.
2024-10-10 19:37:00 +02:00
8f0ee4df6d Merge pull request #828 from ae-utbm/remove-stocks
remove stock application
2024-10-10 19:00:38 +02:00
Sli
579d077b35 Fix docstring 2024-10-10 15:45:43 +02:00
Sli
32444fac90 Apply review comments 2024-10-10 15:42:11 +02:00
Sli
849177562d Add a way to get the base url of an endpoint 2024-10-10 02:57:54 +02:00
Sli
86bbc4cf6e Migrate uv guide to webpack 2024-10-10 02:04:49 +02:00
Sli
46e58bb49e Remove fetchPaginated and migrate viewer.js to viewer-index.js in webpack 2024-10-09 21:46:56 +02:00
Sli
9199f91151 Use typescript api for user pictures and allow imports across js files
* Add imports paths for js files in node
* Add a ts version of fetchPaginated
* Update documentation
2024-10-09 20:59:12 +02:00
Sli
9247696c1c Don't collect .ts files in statics 2024-10-09 17:30:44 +02:00
Sli
37f62e15cf Use new typescript api for user graphs 2024-10-09 17:21:05 +02:00
Sli
a98c924b24 Use auto generated api for markdown input 2024-10-09 16:56:53 +02:00
Sli
a71ca60270 Add typescript support and automatic openapi client generation from ninja 2024-10-09 16:28:54 +02:00
76cc730d8f Merge pull request #865 from ae-utbm/deps
Update Deps
2024-10-09 15:54:09 +02:00
12bb7e9294 remove stock application 2024-10-09 14:50:41 +02:00
1dca0ea003 update ruff 2024-10-09 14:28:13 +02:00
b340a6568f update dependencies 2024-10-09 14:26:39 +02:00
Sli
6f4e93bb76 Use configuration object for load builders 2024-10-09 12:14:10 +02:00
Sli
93eb09887e Fix translations 2024-10-09 12:14:10 +02:00
Sli
09081b03b6 Move family_graph.js to webpack
* Remove cytoscape dependencies
2024-10-09 12:14:10 +02:00
Sli
ceee393bd8 Move user_picture.js to webpack
* Fix relative path generation in webpack
* remove vendored/native-file-system-adapter
* remove vendored/zip.js
2024-10-09 12:14:10 +02:00
b969513d94 Merge pull request #858 from ae-utbm/jsstandard
Add biome to format js files
2024-10-08 23:45:20 +02:00
2111a2c67e Merge pull request #859 from ae-utbm/account-pages
Optimize user account pages
2024-10-08 19:55:45 +02:00
Sli
7405241b82 Apply all biomejs fixes 2024-10-08 17:14:22 +02:00
b0884c6b04 return 404 when accessing not existing account 2024-10-08 15:30:35 +02:00
20bea62542 use spaces for indentation 2024-10-08 13:54:44 +02:00
Sli
24925f7726 Add Biome to documentation 2024-10-08 01:49:29 +02:00
Sli
d0c18d4538 Format with biome instead of standard 2024-10-08 01:49:15 +02:00
Sli
37eaa4b912 Add Standard to documentation 2024-10-07 01:36:13 +02:00
Sli
a3cca056ae Apply standard to easymde 2024-10-07 00:10:24 +02:00
Sli
ee965008d1 Properly fix no-unused-vars warning 2024-10-07 00:04:48 +02:00
Sli
c57d2ece9c Apply standard formater and linter on js files 2024-10-07 00:04:48 +02:00
Sli
e5aa7aa866 Move easymde widget to easymde-index.js 2024-10-07 00:04:05 +02:00
cacdf600f4 Merge pull request #860 from ae-utbm/fix-sas-owner
Fix sas owner
2024-10-05 21:44:21 +02:00
5ee0ee8efb tests for picture ownership 2024-10-05 21:02:19 +02:00
08f20796a7 access rights fix 2024-10-05 20:53:52 +02:00
58d3a7ee2c Optimize user account pages 2024-10-04 13:41:39 +02:00
f6be360eab Merge pull request #857 from ae-utbm/fix-slideshow
Fix slideshow
2024-10-03 22:51:25 +02:00
543a48b4ab reminder to use wsl when cloning the project 2024-10-03 18:32:27 +02:00
Sli
0f657b934d Fix makemessage doc 2024-10-03 18:32:27 +02:00
c4e42212aa Better install doc 2024-10-03 18:32:27 +02:00
a4fe4996aa Merge pull request #834 from ae-utbm/real-name
Restore real name
2024-10-03 10:12:23 +02:00
Sli
fbcacb24f8 Fix broken screen slideshow 2024-10-03 01:05:56 +02:00
0eaa20e09d fix localdate issues 2024-10-03 00:25:22 +02:00
Sli
271d57051e Upgrade to fontawesome 6
* Adapt fontawesome usage when needed
* Fix uv guide not importing css
* Remove utf8 usage for fontawesome
2024-10-03 00:06:03 +02:00
3d6c260e53 Merge pull request #854 from ae-utbm/img-resizing
faster image resizing and smaller results
2024-10-02 23:50:33 +02:00
d0f17bd41a faster image resizing and smaller results 2024-10-02 23:16:47 +02:00
Sli
655d72a2b1 Completely integrate wepack in django
* Migrate alpine
* Migrate jquery and jquery-ui
* Migrate shorten
* Add babel for javascript
* Introduce staticfiles django app
* Only bundle -index.js files in static/webpack
* Unify scss and webpack generated files
* Convert scss calls to static
* Add --clear-generated option to collectstatic
* Fix docs warnings
2024-10-02 16:11:02 +02:00
71c96fdf62 Merge pull request #852 from ae-utbm/master
Merge back
2024-10-01 10:39:42 +02:00
3f2327dee4 Merge pull request #851 from ae-utbm/841-sales-selection-performance
841 sales selection performance
2024-09-30 16:07:19 +02:00
06eecfce40 Optimized last operations on counters 2024-09-30 16:02:17 +02:00
67af1485b3 Merge pull request #850 from ae-utbm/sas-history
Fix history navigation bug in picture viewer in sas
2024-09-30 15:06:10 +02:00
Sli
a00a85a56a Fix recursive link history updates for picture viewer 2024-09-30 15:02:11 +02:00
bb953a6139 Merge pull request #831 from ae-utbm/forum-css-rework
Forum css rework
2024-09-30 12:13:52 +02:00
140dc26dc6 Merge pull request #846 from ae-utbm/fix-select2-img
fix profile pictures layout in Select2 results
2024-09-30 12:13:21 +02:00
3548deebf6 Merge pull request #849 from ae-utbm/taiste
New 3DSv2 fields and Bugfixes
2024-09-30 11:33:32 +02:00
c67155f02c Merge pull request #845 from ae-utbm/fix-search
Fix 500 whean searching users
2024-09-30 10:51:14 +02:00
c10e1e8cbf fix profile pictures layout in Select2 results 2024-09-29 23:31:33 +02:00
c5f5ad3f75 fix 500 when searching users 2024-09-29 23:01:55 +02:00
8ec3074488 Merge pull request #842 from ae-utbm/3dsv2-again
Add the new 3DSv2 fields
2024-09-28 17:59:37 +02:00
1b1284d3d0 Better validation for phone number in billing info 2024-09-28 17:25:34 +02:00
f71518ed6f Move deprecated paginate macro to a lower scope 2024-09-27 11:21:33 +02:00
1800785b80 generalize usage of the paginate_jinja macro 2024-09-27 11:21:33 +02:00
6449724ed5 fix pagination macro and add ellision 2024-09-27 11:21:33 +02:00
6179c3e7d4 Better style for forum messages 2024-09-27 11:21:33 +02:00
3e5d4c5fbb add fixtures for the forum 2024-09-27 11:21:33 +02:00
3f2b63aaa5 move forum style into its own file 2024-09-27 11:21:33 +02:00
d29a5cdb44 Add the new 3DSv2 fields 2024-09-27 11:10:38 +02:00
bbcc7ffeaa Merge pull request #839 from ae-utbm/user-ordering
User ordering
2024-09-25 17:51:25 +02:00
93f4dede3e Put users that never logged in at the end 2024-09-25 14:36:22 +02:00
683f8235b1 Merge pull request #840 from ae-utbm/faster-album-rights
Optimize SithFile recursive rights
2024-09-25 14:35:45 +02:00
43917317b4 optimize file recursive rights 2024-09-25 12:31:51 +02:00
f182de5929 restore user ordering 2024-09-24 12:52:40 +02:00
c6657bffd2 fix: profile picture deletion by board members 2024-09-23 23:35:14 +02:00
3d138d404f move webcam JS to its own file 2024-09-23 23:35:14 +02:00
Sli
9c93162741 Add missing files 2024-09-23 10:25:27 +02:00
Sli
6068c6048a Use real name of the website once again 2024-09-23 01:37:25 +02:00
d47461ba40 Merge pull request #830 from ae-utbm/repair-pagination
fix: `fetch_paginated`
2024-09-20 00:03:34 +02:00
66e88ac6fb Merge pull request #832 from ae-utbm/image-deletion-fix
Fixes after last deployment
2024-09-19 23:57:12 +02:00
d3cada4c95 fix family graph image exension 2024-09-19 20:52:10 +02:00
27443bcd21 fix image deletion. again. 2024-09-19 20:35:08 +02:00
b246e171b7 fix: fetch_paginated 2024-09-18 22:03:39 +02:00
ec434bec56 Merge pull request #829 from ae-utbm/taiste
Family tree and blazingly fast SAS
2024-09-18 16:06:01 +02:00
7458f622f5 Merge pull request #809 from ae-utbm/ajax-image-sas
Ajax image sas
2024-09-18 15:03:54 +02:00
ab72e01707 lower the number of characters to trigger a fulltext search 2024-09-17 17:52:39 +02:00
acad74528d fix: sale creation in populate_more 2024-09-17 16:05:42 +02:00
813bbbb94a preload images and identifications 2024-09-17 12:23:13 +02:00
a2a858262a apply review comments 2024-09-17 12:23:13 +02:00
Sli
727e5cb199 Dummy data on default current_picture to avoid javascript errors 2024-09-17 12:23:13 +02:00
Sli
71602b43bd implement back feature on sas ajax view 2024-09-17 12:23:13 +02:00
bc40b92744 completely ajaxify the picture page 2024-09-17 12:23:13 +02:00
d545becf24 add spinner during loading 2024-09-17 12:17:21 +02:00
48f605dbe0 Use select2 for user picture identification 2024-09-17 12:17:17 +02:00
b0d7bbbb79 select 2 builder 2024-09-17 12:14:20 +02:00
f624b7c66d Graph de famille en frontend (#820)
* Remove graphviz and use cytoscape.js instead

* Frontend generated graphs
* Make installation easier and faster
* Better user experience
* Family api and improved interface
* Fix url history when using 0, improve button selection and reset reverse with reset button
* Use klay layout
* Add js translations and apply review comments
2024-09-17 12:10:06 +02:00
bf96d8a10c Merge pull request #824 from ae-utbm/compress-product-images
auto compress product icons
2024-09-15 18:26:56 +02:00
e8b496cfdc test: Product and ProductType icon resizing 2024-09-15 16:38:58 +02:00
79ef151ad3 auto compress product icons 2024-09-15 14:12:41 +02:00
8e48103fd2 Merge pull request #823 from ae-utbm/fix-image-extension
fix image extension
2024-09-14 19:48:30 +02:00
ed4c65600c fix image extension 2024-09-14 18:45:12 +02:00
ae16a1bd89 Merge pull request #821 from ae-utbm/taiste
Python upgrade and bugfixes
2024-09-12 11:37:27 +02:00
e2b42145e1 Merge pull request #819 from ae-utbm/fix-delete-picture
fix undeletable SAS pictures
2024-09-10 23:12:59 +02:00
55ad1f99fd fix undeletable SAS pictures 2024-09-10 21:38:13 +02:00
5b427bee35 Merge pull request #817 from ae-utbm/skia/faster_install_xapian
Faster install xapian
2024-09-09 15:37:12 +02:00
d1c88a5cef core: commands: make 'install_xapian' way faster 2024-09-09 15:17:09 +02:00
99a25d5e9b Merge pull request #811 from ae-utbm/sas-form-length
unify album name length
2024-09-08 14:32:26 +02:00
d148d6b3a5 unify album name length 2024-09-08 13:30:23 +02:00
66189d3ab2 Merge pull request #810 from ae-utbm/fix-membership-end
fix memberships ending today
2024-09-04 16:27:00 +02:00
f1afa3b436 fix memberships ending today 2024-09-04 16:21:42 +02:00
6380fb193c Merge pull request #808 from ae-utbm/update
Update Python and dependencies
2024-09-02 13:59:23 +02:00
341ffc9a55 update CI 2024-09-02 12:49:11 +02:00
6962b39fc9 use typing.Self for custom queryset methods 2024-09-02 01:03:46 +02:00
d04b4c77c6 update dependencies 2024-09-02 01:03:46 +02:00
453b7df0be bump Python to 3.12 2024-09-02 01:03:46 +02:00
878ee99fe4 Merge pull request #806 from ae-utbm/taiste
Bugfixes
2024-09-02 00:03:40 +02:00
6918e3044f Merge pull request #801 from ae-utbm/remove-version
remove sith version from the footer
2024-09-01 23:50:53 +02:00
cf46c3800f remove sith version from the footer 2024-09-01 23:47:25 +02:00
7c0c132f40 Merge pull request #804 from ae-utbm/repair-subscription-translation
fix subscription form translation
2024-09-01 23:42:18 +02:00
e0bf797876 Merge pull request #805 from ae-utbm/images-format
Better images format
2024-09-01 23:33:57 +02:00
dd07c374d7 convert uploaded images to webp 2024-09-01 19:05:54 +02:00
b3e59b3829 remove unused view GET user/<user_id>/profile_upload 2024-09-01 18:49:50 +02:00
352b09d9cd fix subscription form translation 2024-09-01 15:20:07 +02:00
93cc6d99f8 Merge pull request #803 from ae-utbm/fix-promo-image
fix promo img on clicks
2024-09-01 12:49:38 +02:00
85a99fc8fa fix promo img on clicks 2024-09-01 12:33:49 +02:00
a4d801bed4 Merge pull request #798 from ae-utbm/fix-content-disposition
repair name of protected files
2024-08-30 10:44:49 +02:00
fbff38c5c3 repair name of protected files
Depuis l'implémentation de l'envoi des fichiers par le reverse-proxy, le nom des fichiers n'était plus envoyé.
2024-08-30 10:27:03 +02:00
14402f7537 Merge pull request #800 from ae-utbm/forgotten-migrations
add forgotten migration
2024-08-29 12:53:21 +02:00
88d24f8067 Merge pull request #799 from ae-utbm/remove-sentry-ci
remove sentry deployment CI (until Sentry is repaired)
2024-08-29 12:53:06 +02:00
cc1d700f7d add forgotten migration 2024-08-29 11:57:09 +02:00
e82acdabb0 remove sentry deployment CI (until Sentry is repaired) 2024-08-29 11:48:26 +02:00
ea42c98571 Merge pull request #797 from ae-utbm/fix-image-injection
Better form for user submiting images
2024-08-27 22:46:01 +02:00
Sli
cc5df9b171 Better form for user submiting images, fix potential attack vector on bad file being resized and treated as an image 2024-08-27 17:05:37 +02:00
b4749f297b Merge pull request #795 from ae-utbm/taiste
Last update before Inté
2024-08-27 14:16:40 +02:00
e564c6604c Merge pull request #788 from ae-utbm/manifest-static-files
Manifest static files
2024-08-27 11:08:49 +02:00
712615a312 Merge pull request #794 from ae-utbm/user-pictures-ajax
Better browser compatibility for user picture page
2024-08-27 11:08:27 +02:00
Sli
d95d4901d2 Use reduce instead of groupBy for user picture sorting to support more browsers 2024-08-27 10:35:38 +02:00
9373654306 use rjsmin for js minification
Ca minifie moins bien le JS que Uglify, mais c'est intégrable directement dans les dépendances du projet
2024-08-26 23:16:13 +02:00
4a9d9f03a8 fix test workflow 2024-08-26 22:59:40 +02:00
b7261ec629 custom manifest static files storage that also minify scss and js files 2024-08-26 22:34:32 +02:00
2e1f16fa04 slim jquery-ui 2024-08-26 22:34:32 +02:00
d295cc5223 move vendored files into their own folder 2024-08-26 22:34:32 +02:00
ff088009d9 move static files in their respective application 2024-08-26 22:34:31 +02:00
52c19e9962 simplify scss management 2024-08-26 22:34:31 +02:00
68d0a16d1c Merge pull request #782 from ae-utbm/ajax-navigation-history
Ajax navigation history in uv guide
2024-08-26 22:29:19 +02:00
Sli
a422e8d39a Improve rendering of file input 2024-08-26 22:21:16 +02:00
Sli
ef80c1be61 Make camera error gray to be less aggressive on the eyes 2024-08-26 22:21:16 +02:00
Sli
85d9816aaa Improve delete button behavior 2024-08-26 22:21:16 +02:00
Sli
93b66d980d Directly display selected img of form on screen and convert to webp 2024-08-26 22:21:16 +02:00
Sli
07d617da91 Get video resolution from the camera settings for a better image quality 2024-08-26 22:21:16 +02:00
Sli
34aac40e65 Add translations 2024-08-26 22:21:16 +02:00
Sli
f54bf2b8af Adjust css 2024-08-26 22:21:16 +02:00
Sli
e7d04d9817 Unify user profile display with a nice macro and handle camera errors 2024-08-26 22:21:16 +02:00
Sli
ef1537ac2c Basic webcam setup with modern web api 2024-08-26 22:21:16 +02:00
d1f86fe3d9 Merge pull request #791 from ae-utbm/remove-bbcode
Remove to_markdown.jinja forgotten during bbcode convertion removal
2024-08-24 20:40:04 +02:00
Sli
d13b79552b Remove to_markdown.jinja forgotten during bbcode convertion removal 2024-08-21 15:35:43 +02:00
4036bfd703 Merge pull request #775 from ae-utbm/user-pictures-ajax
Render user picture page with ajax to improve performances
2024-08-18 12:40:07 +02:00
Sli
759e360a1d Don't use unnecessary promises 2024-08-17 10:15:13 +02:00
Sli
8865529b39 Use native alpine debounce 2024-08-17 02:58:53 +02:00
Sli
cdb73ee49c Don't rely on waiting for pedagogy history 2024-08-17 02:57:00 +02:00
Sli
9188c28ee7 Remove intersect 2024-08-16 22:52:20 +02:00
Sli
2a6c1f050d Create a paginate_alpine macro 2024-08-11 15:11:51 +02:00
Sli
2ec1f8cdc0 Fix back action in uv guide 2024-08-11 14:58:05 +02:00
121b388d85 Merge pull request #781 from ae-utbm/ajax-navigation-history
Fix back function in album pagination
2024-08-11 00:34:21 +02:00
Sli
589119c9ee Improve update_query_string with enum action 2024-08-10 23:32:50 +02:00
Sli
b35e1a476e Fix back function in album pagination 2024-08-10 18:38:04 +02:00
8174bce720 Merge pull request #780 from ae-utbm/remove-bbcode
remove doku/bbcode to markdown
2024-08-10 16:04:10 +02:00
d8a7d62b23 Merge pull request #779 from ae-utbm/fix-queryset
fix crash on album fetch & test
2024-08-10 16:03:56 +02:00
Sli
a75730d91f Fix unbalanced html 2024-08-10 15:16:37 +02:00
Sli
a2b5f929dd Apply review comments
* Add alpine intersect
* Move alpine and it's plugins to a folder
* Fix spinning wheel position
* Improve album title position
2024-08-10 14:49:02 +02:00
7a0fa9f1a0 remove doku/bbcode to markdown 2024-08-10 14:23:01 +02:00
28ff7f24c5 Merge pull request #774 from ae-utbm/fix-operation-logs
Fix operation logs
2024-08-10 10:33:39 +02:00
e6db25357b Merge pull request #778 from ae-utbm/user-search-privacy
User search privacy
2024-08-10 09:38:29 +02:00
72ea6b6fdd fix crash on album fetch & test 2024-08-10 00:46:40 +02:00
Sli
bf5f72fd9d Fix user search displaying results that shouldn't be viewed 2024-08-10 00:43:03 +02:00
af724a1e0e Merge pull request #777 from ae-utbm/taiste
SAS Hotfixes
2024-08-09 18:20:22 +02:00
Sli
0eeaf1ce21 Render user picture page with ajax to improve performances 2024-08-09 18:09:58 +02:00
57a8215c6b Merge pull request #776 from ae-utbm/fix-album-navigation
SAS fixes
2024-08-09 18:09:06 +02:00
9163e4dee6 fix SAS album display 2024-08-09 18:08:36 +02:00
c56d6e3f6b fix wrong page size when fetching pictures. 2024-08-09 17:35:33 +02:00
Sli
20e8854467 Fix operation logs 2024-08-09 17:35:26 +02:00
3ef38fabdb fix picture navigation 2024-08-09 17:34:35 +02:00
f5cee10761 Merge pull request #773 from ae-utbm/taiste
SAS, Eboutic, Antispam, psycopg
2024-08-09 13:35:26 +02:00
d1cbb765c0 Merge pull request #769 from ae-utbm/query-sas
Sas picture selection
2024-08-09 12:11:16 +02:00
7ea9a5ca2d improved feedback when loading ajax content 2024-08-09 11:58:26 +02:00
20c015c312 improved UX 2024-08-09 11:58:26 +02:00
ecb48ce663 fix error when uploading image with an alpha channel 2024-08-09 11:58:26 +02:00
00dc03a235 fix rights on albums and next/previous pictures 2024-08-08 13:35:48 +02:00
d3b203a4a1 change cache on picture download 2024-08-08 11:50:45 +02:00
4506440a62 add PictureQuerySet.viewable_by(user) method 2024-08-08 11:50:45 +02:00
da6bd84cdf restify album view 2024-08-08 11:50:45 +02:00
0b9ccf6a57 paginate GET /api/sas/picture 2024-08-08 11:50:45 +02:00
a056bd177f Merge pull request #772 from ae-utbm/master
Merge-back
2024-08-08 11:47:26 +02:00
d2ea8f2898 Merge pull request #742 from ae-utbm/refactor-eboutic
Eboutic big refactor
2024-08-07 20:36:50 +02:00
5cce4269bb remove fuzzy from translations 2024-08-07 20:33:26 +02:00
0a2ed6dd94 fix crash when basket contains not existing product 2024-08-07 20:15:46 +02:00
417f328206 fix billing infos not sending 2024-08-07 14:29:51 +02:00
cca9732925 eboutic big refactor 2024-08-06 16:49:20 +02:00
f02864b752 Merge pull request #768 from ae-utbm/ruff-print
T2 ruff rule
2024-08-06 16:45:20 +02:00
62bb15317c T2 ruff rule 2024-08-06 11:42:10 +02:00
b35751126f Merge pull request #762 from ae-utbm/dependabot/pip/taiste/sentry-sdk-2.12.0
[UPDATE] Bump sentry-sdk from 2.11.0 to 2.12.0
2024-08-06 11:14:47 +02:00
28d6d8ba96 Merge pull request #766 from ae-utbm/alpine
Alpine
2024-08-06 10:43:08 +02:00
6bdb16e293 [UPDATE] Bump sentry-sdk from 2.11.0 to 2.12.0
Bumps [sentry-sdk](https://github.com/getsentry/sentry-python) from 2.11.0 to 2.12.0.
- [Release notes](https://github.com/getsentry/sentry-python/releases)
- [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-python/compare/2.11.0...2.12.0)

---
updated-dependencies:
- dependency-name: sentry-sdk
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-05 21:41:48 +00:00
eb45cf6175 Merge pull request #761 from ae-utbm/dependabot/pip/taiste/django-honeypot-1.2.1
[UPDATE] Bump django-honeypot from 1.2.0 to 1.2.1
2024-08-05 23:39:42 +02:00
d6d8f56570 [UPDATE] Bump django-honeypot from 1.2.0 to 1.2.1
Bumps [django-honeypot](https://github.com/jamesturk/django-honeypot) from 1.2.0 to 1.2.1.
- [Changelog](https://github.com/jamesturk/django-honeypot/blob/main/CHANGELOG)
- [Commits](https://github.com/jamesturk/django-honeypot/commits)

---
updated-dependencies:
- dependency-name: django-honeypot
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-05 21:31:09 +00:00
5322dc1de8 Merge pull request #764 from ae-utbm/dependabot/pip/taiste/faker-26.1.0
[UPDATE] Bump faker from 26.0.0 to 26.1.0
2024-08-05 23:28:58 +02:00
51bb1a5c9d Merge pull request #765 from ae-utbm/fix-referer
fix-referer
2024-08-05 23:28:25 +02:00
996dadf6f5 update alpineJS to 3.14 2024-08-05 17:16:24 +02:00
29bb0f6712 promote AlpineJS to global dependency 2024-08-05 17:08:30 +02:00
f6fbad8403 fix missing HTTP_REFERER 2024-08-05 15:53:41 +02:00
e37ce4172e Merge pull request #759 from ae-utbm/accel-redirect
Accel redirect
2024-08-05 15:15:39 +02:00
1dfd871169 add doc for nginx configuration 2024-08-05 13:32:47 +02:00
a637742bb0 apply review comment 2024-08-05 10:52:15 +02:00
a5e4db99fb Use X-Accel-Redirect to send files in prod 2024-08-05 10:52:15 +02:00
a9f66e2cd9 extract sent_from_logged_counter(request) 2024-08-05 10:52:15 +02:00
7bc7af8245 [UPDATE] Bump faker from 26.0.0 to 26.1.0
Bumps [faker](https://github.com/joke2k/faker) from 26.0.0 to 26.1.0.
- [Release notes](https://github.com/joke2k/faker/releases)
- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md)
- [Commits](https://github.com/joke2k/faker/compare/v26.0.0...v26.1.0)

---
updated-dependencies:
- dependency-name: faker
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-05 08:51:26 +00:00
e5dfe1e638 Merge pull request #763 from ae-utbm/dependabot/pip/taiste/phonenumbers-8.13.42
[UPDATE] Bump phonenumbers from 8.13.40 to 8.13.42
2024-08-05 10:49:13 +02:00
284f064cbf [UPDATE] Bump phonenumbers from 8.13.40 to 8.13.42
Bumps [phonenumbers](https://github.com/daviddrysdale/python-phonenumbers) from 8.13.40 to 8.13.42.
- [Commits](https://github.com/daviddrysdale/python-phonenumbers/compare/v8.13.40...v8.13.42)

---
updated-dependencies:
- dependency-name: phonenumbers
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-05 08:34:32 +00:00
e5c6f00283 Merge pull request #758 from ae-utbm/psycopg
update psycopg v2 to psycopg v3
2024-08-04 23:19:56 +02:00
12d316ebe4 doc: advanced install 2024-08-04 23:12:24 +02:00
cbd8932075 update psycopg v2 to psycopg v3 2024-08-04 23:12:24 +02:00
feb6dcbc94 Merge pull request #756 from ae-utbm/antispam
Filter blocked emails
2024-08-04 22:36:03 +02:00
Sli
181e74b1d1 Add antispam app
* update_spam_database command to update suspicious domains from an
   external provider
* Add a AntiSpamEmailField that deny emails from suspicious domains
* Update documentation
2024-08-04 22:34:40 +02:00
eb04e26b22 Merge pull request #757 from ae-utbm/taiste
Taiste
2024-08-04 16:51:36 +02:00
7b97f0bf47 [UPDATE] Bump pre-commit from 3.7.1 to 3.8.0
Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 3.7.1 to 3.8.0.
- [Release notes](https://github.com/pre-commit/pre-commit/releases)
- [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md)
- [Commits](https://github.com/pre-commit/pre-commit/compare/v3.7.1...v3.8.0)

---
updated-dependencies:
- dependency-name: pre-commit
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-01 17:51:19 +02:00
19fdaf4c89 fix club counter click 2024-08-01 17:50:43 +02:00
7ca9c8dc42 Merge pull request #749 from ae-utbm/redis
Redis
2024-08-01 13:01:42 +02:00
946f35c601 Merge pull request #752 from ae-utbm/autocomplete-admin
use autocomplete_fields in admin
2024-08-01 13:01:27 +02:00
c7b47bdd02 use redis for the cache 2024-08-01 12:26:23 +02:00
eef15e05f4 use autocomplete_fields in admin 2024-08-01 11:27:54 +02:00
Sli
f265346a10 Sort pictures by album in zip file 2024-07-31 12:01:08 +02:00
a321bd79ed Merge pull request #745 from ae-utbm/picture-zip
Add image download progress bar and fix output name of pictures
2024-07-31 10:19:50 +02:00
819e2b5f9f better download button style 2024-07-30 19:58:58 +02:00
91344741a5 add some alpine to picture download 2024-07-30 19:23:48 +02:00
4d2b82235c downgrade ajax-select 2024-07-30 14:15:46 +02:00
Sli
ffa3936878 Improve zip download
* Remove jszip for zip.js which is better maintained
* Pictures keep their creation date
2024-07-30 11:11:31 +02:00
26c70aa071 Merge pull request #746 from ae-utbm/pedagogy
Use full text search in pedagogy uv search api
2024-07-29 18:12:43 +02:00
Sli
1bd887567e Use full text search in pedagogy uv search api 2024-07-29 13:01:20 +02:00
Sli
3304f32ef0 Add image download progress bar and fix output name of pictures 2024-07-28 23:53:18 +02:00
0790ae2298 Merge pull request #743 from ae-utbm/taiste
Taiste
2024-07-28 21:37:38 +02:00
39151b61e7 Merge pull request #741 from ae-utbm/better-pagination
improve pagination
2024-07-28 16:46:00 +02:00
3f49d70745 remove pedagogy style from style.scss 2024-07-28 16:39:15 +02:00
e5434961de Merge pull request #736 from ae-utbm/better-scss
Better scss
2024-07-28 16:35:12 +02:00
aab2d3a03f Merge pull request #740 from ae-utbm/deps
Update dependencies
2024-07-28 16:34:53 +02:00
b022ebb80e improve pagination 2024-07-27 10:46:57 +02:00
2737cae4ab update django-phonenumber-field 2024-07-26 21:45:18 +02:00
c4e6272535 various deps updates 2024-07-26 21:45:18 +02:00
aa0c98bf34 increase delay between dependabot alerts 2024-07-26 18:24:04 +02:00
63b6b262c6 repair BASE_DIR 2024-07-26 18:21:57 +02:00
424639ea80 better scss 2024-07-26 15:55:15 +02:00
594776f3a6 better scss compilation 2024-07-26 15:55:15 +02:00
918e93d211 Merge pull request #735 from ae-utbm/more-fixtures
Add a command to create more fixtures
2024-07-26 15:28:47 +02:00
b82f98c87f reorganize pyproject.toml 2024-07-26 15:16:54 +02:00
6c4251a91f populate more 2024-07-26 15:15:50 +02:00
2261782920 Merge pull request #738 from ae-utbm/fix-remove-from-picture
Fix button to remove a user from picture
2024-07-26 14:48:28 +02:00
043dcfb283 add tests 2024-07-26 14:25:26 +02:00
3c76c5e0f1 fix grouping 2024-07-26 00:39:29 +02:00
d348e6314a fix the pictures order (not just the album) 2024-07-26 00:39:29 +02:00
b3fa6f352b fix album order for user pictures 2024-07-26 00:39:29 +02:00
191b05c305 Fix button to remove a user from picture 2024-07-25 23:31:54 +02:00
215fdce411 Fix button to remove a user from picture 2024-07-25 23:29:12 +02:00
b25805e0a1 introduce djhtml as jinja+scss formater 2024-07-25 16:46:45 +02:00
13d0d2a300 Merge pull request #733 from ae-utbm/nfc
Add nfc widget
2024-07-25 15:56:51 +02:00
Sli
15f51fb03f Create an NFC button for browser supporting NFC API 2024-07-25 07:18:39 +02:00
a24b1f5c2a Merge pull request #730 from ae-utbm/picture-zip
Téléchargement des images dans un zip
2024-07-25 01:21:02 +02:00
04e7f65e8e Merge pull request #725 from ae-utbm/drop-jquery-calendar
Remove jquery datetimepicker
2024-07-25 01:20:41 +02:00
Sli
41b9318028 Download user pictures as a zip 2024-07-24 23:51:15 +02:00
378e8b53f2 Merge pull request #731 from ae-utbm/taiste
MkDocs, Ninja API, logo promo 24 et refactors
2024-07-24 17:56:57 +02:00
c832e8b1a7 Merge pull request #729 from ae-utbm/test-name-refactoring
Harmonize test names
2024-07-24 01:17:39 +02:00
Sli
fee7ade1a5 Harmonize test names 2024-07-24 00:50:48 +02:00
d51dbf8a53 Merge pull request #724 from ae-utbm/ninja
Use django-ninja for the API
2024-07-24 00:48:08 +02:00
c03a1b57c5 update doc 2024-07-24 00:44:09 +02:00
Sli
0c566cfbde Add picture size in sas api 2024-07-24 00:44:09 +02:00
9295325d21 remove jquery datetime picker 2024-07-23 23:26:48 +02:00
cb1aa8bef0 add tests 2024-07-23 20:36:57 +02:00
Sli
b9d19be183 Fix markdown api and add test for user picture page 2024-07-23 20:36:46 +02:00
Sli
293369f165 Pagination on UV guide 2024-07-23 19:58:11 +02:00
3046438cb1 replace drf by django-ninja 2024-07-23 19:57:33 +02:00
811e5a5ad1 Merge pull request #726 from ae-utbm/honeypot
better honeypot logging
2024-07-22 12:45:45 +02:00
Sli
2c8f18d7fc Add honeypot on forum 2024-07-22 11:40:11 +02:00
Sli
c7f8cdd098 Support field_name argument in honeypot jinja extension 2024-07-22 11:34:22 +02:00
Sli
58ff5b934a add get_client_ip util function 2024-07-22 09:49:08 +02:00
03d15ddded better honeypot logging 2024-07-21 22:31:05 +02:00
002d8f80a6 Merge pull request #720 from ae-utbm/counter-refactor
Refactor on counters
2024-07-21 15:39:07 +02:00
82d3791859 refactor counter 2024-07-21 10:51:08 +02:00
d9531838f2 Merge pull request #716 from ae-utbm/docs
Use MkDocs for documentation
2024-07-21 01:05:32 +02:00
Sli
c7b5c77395 Improve perms doc 2024-07-21 01:01:49 +02:00
Sli
223aa37161 move old pdf to the repo github wiki 2024-07-21 01:00:37 +02:00
c1acadbf3d add content to howto/querysets.md 2024-07-21 00:57:15 +02:00
Sli
54af894b82 Improve documentation 2024-07-21 00:57:12 +02:00
Sli
e1ac75f394 Rework readme and remove readthedocs config 2024-07-21 00:57:06 +02:00
8c69a94488 use google convention for docstrings 2024-07-21 00:57:04 +02:00
07b625d4aa Rewrite documentation with MkDocs 2024-07-21 00:56:58 +02:00
a1296dc7af Merge pull request #721 from ae-utbm/remove-pytz
Remove pytz from deps
2024-07-20 11:36:30 +02:00
e5a2236d72 remove pytz 2024-07-18 17:33:14 +02:00
588a82426e Merge pull request #719 from ae-utbm/page-fix
Fix markdown style for code
2024-07-18 15:08:21 +02:00
8245ddf2a6 fix font for code blocks in markdown 2024-07-18 14:51:50 +02:00
775a0c6478 Merge pull request #717 from ae-utbm/logo-24
Add promo 24 logo
2024-07-16 00:35:43 +02:00
Sli
bad67a8b65 Add promo 24 logo 2024-07-16 00:25:50 +02:00
Sli
7e98e184a0 Improve tests 2024-07-11 13:23:24 +02:00
Sli
6240eff160 Apply review suggestions 2024-07-11 13:23:24 +02:00
Sli
a8918ebe86 Fix forum topic creation 2024-07-11 13:23:24 +02:00
b852176958 Merge pull request #714 from ae-utbm/taiste
More ruff rules, mistune update and more bot-blocking features
2024-07-11 11:47:45 +02:00
c9e398b7ec Merge pull request #715 from ae-utbm/master 2024-07-11 11:33:28 +02:00
e84d5626df Merge pull request #711 from ae-utbm/bot-filtering
Implement mechanisms to block bots on authentication views
2024-07-11 11:00:10 +02:00
Sli
0fb61938ce Reorganize honeypot settings 2024-07-11 10:49:08 +02:00
Sli
d6b27f2f21 Make honeypot errors less suspicious 2024-07-10 19:30:01 +02:00
e15bcfae07 Send an email when creating an account via POST /register 2024-07-10 17:21:07 +02:00
Sli
72cf5a3d5e Introduce honeypot for login/registering/password changing 2024-07-10 14:51:39 +02:00
7de2e00c94 Merge pull request #701 from ae-utbm/dependabot/pip/taiste/ruff-0.5.1
[UPDATE] Bump ruff from 0.4.10 to 0.5.1
2024-07-10 14:15:38 +02:00
efe5d75798 update ruff config
Co-authored-by: Bartuccio Antoine <klmp200@users.noreply.github.com>
2024-07-10 10:52:30 +02:00
9f1eedbe1b [UPDATE] Bump ruff from 0.4.10 to 0.5.1
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.4.10 to 0.5.1.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/v0.4.10...0.5.1)

---
updated-dependencies:
- dependency-name: ruff
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-08 15:30:54 +00:00
7fe495179f Merge pull request #707 from ae-utbm/update-mistune
Update mistune (0.8 => 3.0)
2024-07-08 17:29:13 +02:00
7f6c4f6236 change sup and sub in mde editor 2024-07-08 17:04:18 +02:00
30948f1701 better style for rendered markdown 2024-07-08 15:56:38 +02:00
02ec3607b2 fix install_xapian.sh 2024-07-08 15:56:37 +02:00
3c2dcfbfa2 update mistune 2024-07-08 15:56:37 +02:00
8bcf59aaf0 Merge pull request #706 from ae-utbm/ruff-rules
Add more ruff rules
2024-07-08 15:42:22 +02:00
c6d2ac9100 ruff rule B 2024-07-08 15:37:10 +02:00
2ac578c3ad ruff rule DJ
Co-authored-by: Bartuccio Antoine <klmp200@users.noreply.github.com>
2024-07-08 15:37:10 +02:00
f941435232 ruff rule C4 2024-07-08 15:37:10 +02:00
171a1cb876 ruff rule FBT 2024-07-08 15:37:09 +02:00
cfc19434d0 ruff rules UP008 and UP009 2024-07-08 15:37:09 +02:00
688871a680 ruff rule A 2024-07-08 15:37:08 +02:00
44c8558aa3 Merge pull request #704 from ae-utbm/taiste
Mises à jour (django 4.2, Pillow 10, cryptography 42), changement de la CI et enlèvement de l'offre Eurockéennes
2024-07-08 11:16:39 +02:00
6b923d2310 Merge pull request #700 from ae-utbm/remove-eurocks
Remove eurocks
2024-07-08 10:11:09 +02:00
09e0b31bc9 remove Eurockéennes link 2024-07-08 10:03:27 +02:00
eb2454eded Merge branch 'master' into taiste
# Conflicts:
#	eboutic/templates/eboutic/eboutic_main.jinja
#	locale/fr/LC_MESSAGES/django.po
2024-07-08 10:01:37 +02:00
3014d8cead Merge pull request #698 from ae-utbm/update-cryptography
update cryptography
2024-07-05 15:21:42 +02:00
70fdc2edf2 update cryptography 2024-07-05 14:02:01 +02:00
e47f29aa38 Merge pull request #697 from ae-utbm/update-pillow
update pillow (9.5 => 10.4)
2024-07-05 13:53:47 +02:00
d811896e21 update pillow 2024-07-05 13:14:58 +02:00
5c999b6ef1 Merge pull request #696 from ae-utbm/fix-xapian-deploy
Fix missing xapian install step in deploy workflows
2024-07-05 10:28:41 +02:00
79a6d9e771 Merge pull request #693 from ae-utbm/faster-tests
faster tests
2024-07-05 10:27:30 +02:00
Sli
e1cf1c786d Fix missing xapian install step in deploy workflows 2024-07-04 19:44:22 +02:00
71fe9559b1 parallelize the CI 2024-07-04 14:44:28 +02:00
f1fa8d34bf fix family relations in generate_galaxy_test_data.py 2024-07-04 14:39:12 +02:00
aa07fa9207 faster tests 2024-07-04 14:03:19 +02:00
47fec973bc Merge pull request #691 from ae-utbm/update-django
Update django (3.2 => 4.2)
2024-07-04 12:40:23 +02:00
ea8247aa16 fix broken translations 2024-07-04 11:31:36 +02:00
bf18284450 apply forgotten migrations 2024-07-04 11:31:36 +02:00
cd58d5a357 resolve warnings 2024-07-04 11:31:35 +02:00
75bb3f992c fix: wrong logic in Club.delete() 2024-07-04 11:31:35 +02:00
ae1fcdb8c0 fix: CashRegisterSummaryItem.check overriding a django method 2024-07-04 11:20:24 +02:00
507080f75e update django to 4.2 2024-07-03 15:11:06 +02:00
5bcf043d97 Merge pull request #683 from ae-utbm/pre-commits
Use pre-commits hooks instead of ruff directly
2024-07-03 10:04:53 +02:00
Sli
99605b98d4 Two steps pre-commit and better workflow output 2024-07-02 20:16:02 +02:00
6dfd43a8da [UPDATE] Bump reportlab from 4.2.0 to 4.2.2
Bumps [reportlab](https://www.reportlab.com/) from 4.2.0 to 4.2.2.

---
updated-dependencies:
- dependency-name: reportlab
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-02 11:26:16 +02:00
Sli
c7135875b8 Use pre-commits hooks instead of ruff directly 2024-07-01 17:36:19 +02:00
e29e1101cd [UPDATE] Bump ipython from 7.34.0 to 8.26.0
Bumps [ipython](https://github.com/ipython/ipython) from 7.34.0 to 8.26.0.
- [Release notes](https://github.com/ipython/ipython/releases)
- [Commits](https://github.com/ipython/ipython/compare/7.34.0...8.26.0)

---
updated-dependencies:
- dependency-name: ipython
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-01 14:00:43 +02:00
d97602e60b Use pytest for tests (#681)
* use pytest for tests

Eh ouais, il y a que la config qui change. Pytest est implémentable par étapes. Et ça c'est beau.

* rework tests with pytest

* remove unittest custom TestRunner

* Edit doc and CI
2024-06-26 19:10:24 +02:00
a5cbac1f97 Merge pull request #680 from ae-utbm/ruff
Introduct Ruff as formater and linter
2024-06-26 14:11:26 +02:00
3143d3d91a reorganize imports with ruff 2024-06-26 12:35:38 +02:00
9bdf3fc4ac use ruff for formating
Co-authored-by: Bartuccio Antoine <klmp200@users.noreply.github.com>
2024-06-26 12:35:14 +02:00
e06bc7dba3 reorganize pyproject.toml 2024-06-26 12:33:35 +02:00
a8b9f38000 Merge pull request #679 from ae-utbm/xapian-from-sources
Xapian from sources and fix CVE
2024-06-26 11:48:31 +02:00
Sli
ca27b89a8b Apply shellcheck on install_xapian.sh 2024-06-26 11:31:39 +02:00
Sli
e1bf7caa9a Fix CVE-2023-31047 2024-06-24 13:27:22 +02:00
Sli
e681c17a0f Adapt CI to new xapian install process 2024-06-24 13:26:58 +02:00
Sli
5416d88c97 Upgrade dependencies and install xapian from sources 2024-06-24 13:26:58 +02:00
0dca152436 Hotfix (typo) 2024-05-28 21:25:30 +02:00
9ce7abd31d Partnership Eurockéennes 2024 2024-05-28 19:10:17 +02:00
f41ff281fb Remove eurocks tickets from eboutic (event is finished) 2023-10-10 15:50:35 +02:00
ee437649f0 Revert "Merge branch 'master' into taiste"
This reverts commit 4303d51c0a, reversing
changes made to d16bf12611.
2023-10-10 15:47:02 +02:00
321cb72ca8 October 2023 update (#672)
* integration of 3D secure v2 for eboutic bank payment

* edit yml to avoid git conflict when deploying on test

* escape html characters on xml (#505)

* Change country id to ISO 3166 1 numeric for 3DSV2 (#510)

* remove useless tests

* Fix le panier de l'Eboutic pour Safari (#518)

Co-authored-by: Théo DURR <git@theodurr.fr>
Co-authored-by: thomas girod <56346771+imperosol@users.noreply.github.com>

* update some dependencies (#523)

* [Eboutic] Fix double quote issue & improved user experience on small screen (#522)

* Fix #511 Regex issue with escaped double quotes

* Fix basket being when reloading the page (when cookie != "")

+ Added JSDoc
+ Cleaned some code

* Fix #509 Improved user experience on small screens

* Fix css class not being added back when reloading page

* CSS Fixes (see description)

+ Fixed overlaping item title with the cart emoji on small screen
+ Fixed minimal size of the basket on small screen (full width)

* Added darkened background circle to items with no image

* Fix issue were the basket could be None


* Edited CSS to have bette img ratio & the 🛒 icon

Adapt, Improve, Overcome

* Moved basket down on small screen size

* enhance admin pages

* update documentation

* Update doc/about/tech.rst

Co-authored-by: Julien Constant <49886317+Juknum@users.noreply.github.com>

* remove csrf_token

* Fix 3DSv2 implementation (#542)

* Fixed wrong HMAC signature generation

* Fix xml du panier

Co-authored-by: Julien Constant <julienconstant190@gmail.com>

* [FIX] 3DSv2 - Echappement du XML et modif tables (#543)

* Fixed wrong HMAC signature generation
* Updated migration files

Co-authored-by: Julien Constant <julienconstant190@gmail.com>

* Update doc/about/tech.rst

* Update doc/start/install.rst

* Updated lock file according to pyproject

* unify account_id creation

* upgrade re_path to path (#533)

* redirect directly on counter if user is barman

* Passage de vue à Alpine pour les comptoirs (#561)

Vue, c'est cool, mais avec Django c'est un peu chiant à utiliser. Alpine a l'avantage d'être plus léger et d'avoir une syntaxe qui ne ressemble pas à celle de Jinja (ce qui évite d'avoir à mettre des {% raw %} partout).

* resolved importError (#565)

* Add galaxy (#562)

* style.scss: lint

* style.scss: add 'th' padding

* core: populate: add much more data for development

* Add galaxy

* repair user merging tool (#498)

* Disabled galaxy feature (only visually)

* Disabled Galaxy button & Removed 404 exception display

* Update 404.jinja

* Fixed broken test

* Added eurocks links to eboutic

* fix typo

* fix wording

Co-authored-by: Théo DURR <git@theodurr.fr>

* Edited unit tests

This test caused a breach in security due to the alert block displaying sensitive data.

* Repair NaN bug for autocomplete on counter click

* remove-useless-queries-counter-stats (#519)

* Amélioration des pages utilisateurs pour les petits écrans (#578, #520)

- Refonte de l'organisation des pages utilisateurs (principalement du front)
  - Page des parrains/fillots
  - Page d'édition du profil
  - Page du profil
  - Page des outils
  - Page des préférences
  - Page des stats utilisateurs

- Refonte du CSS / organisation de la navbar principale (en haut de l'écran)
- Refonte du CSS de la navbar bleu clair (le menu)
- Refonte du CSS du SAS :
  - Page de photo
  - Page d'albums

* Added GA/Clubs Google Calendar to main page (#585)

* Added GA/Clubs google calendar to main page

* Made tables full width

* Create dependabot.yml (#587)

* Bump django from 3.2.16 to 3.2.18 (#574)

* [CSS] Follow up of #578 (#589)

* [FIX] Broken link in readme and license fix (& update) (#591)

* Fixes pour la mise à jour de mars (#598)

* Fix problème de cache dans le SAS & améliore le CSS du SAS

Co-authored-by: Bartuccio Antoine <klmp200@users.noreply.github.com>

* Fixes & améliorations du nouveau CSS (#616)

* [UPDATE] Bump sentry-sdk from 1.12.1 to 1.19.1 (#620)

* [FIX] Fixes supplémentaires pour la màj de mars (#622)

- Les photos de l'onglet de la page utilisateur utilise désormais leur version thumbnail au lieu de leur version HD
- Une des classes du CSS du SAS a été renommée car elle empiétait sur une class de la navbar
- Le profil utilisateur a été revu pour ajouter plus d'espacement entre le tableau des cotisations et le numéro de cotisants
- Les images de forum & blouse sont de nouveau cliquable pour les afficher en grands
- Sur mobile, lorsqu'on cliquait sur le premier élément de la navbar, ce dernier avait un overlay avec des angles arrondis
- Sur mobile, les utilisateurs avec des images de profils non carrées dépassait dans l'onglet Famille

* [UPDATE] Bump dict2xml from 1.7.2 to 1.7.3 (#592)

Bumps [dict2xml](https://github.com/delfick/python-dict2xml) from 1.7.2 to 1.7.3.
- [Release notes](https://github.com/delfick/python-dict2xml/releases)
- [Commits](https://github.com/delfick/python-dict2xml/compare/release-1.7.2...release-1.7.3)

---
updated-dependencies:
- dependency-name: dict2xml
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

* [UPDATE] Bump django-debug-toolbar from 3.8.1 to 4.0.0 (#593)

Bumps [django-debug-toolbar](https://github.com/jazzband/django-debug-toolbar) from 3.8.1 to 4.0.0.
- [Release notes](https://github.com/jazzband/django-debug-toolbar/releases)
- [Changelog](https://github.com/jazzband/django-debug-toolbar/blob/main/docs/changes.rst)
- [Commits](https://github.com/jazzband/django-debug-toolbar/compare/3.8.1...4.0.0)

---
updated-dependencies:
- dependency-name: django-debug-toolbar
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* [UPDATE] Bump cryptography from 37.0.4 to 40.0.1 (#594)

* [UPDATE] Bump cryptography from 37.0.4 to 40.0.1

Bumps [cryptography](https://github.com/pyca/cryptography) from 37.0.4 to 40.0.1.
- [Release notes](https://github.com/pyca/cryptography/releases)
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/37.0.4...40.0.1)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* Updated pyOpenSSL to match cryptography requirements

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Julien Constant <julienconstant190@gmail.com>

* Mise à jour de Black vers la version 23.3 (#629)

* update link for poetry install

* [UPDATE] Bump django-countries from 7.5 to 7.5.1 (#624)

Bumps [django-countries](https://github.com/SmileyChris/django-countries) from 7.5 to 7.5.1.
- [Release notes](https://github.com/SmileyChris/django-countries/releases)
- [Changelog](https://github.com/SmileyChris/django-countries/blob/main/CHANGES.rst)
- [Commits](https://github.com/SmileyChris/django-countries/compare/v7.5...v7.5.1)

---
updated-dependencies:
- dependency-name: django-countries
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* [UPDATE] Bump sentry-sdk from 1.19.1 to 1.21.0

Bumps [sentry-sdk](https://github.com/getsentry/sentry-python) from 1.19.1 to 1.21.0.
- [Release notes](https://github.com/getsentry/sentry-python/releases)
- [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-python/compare/1.19.1...1.21.0)

---
updated-dependencies:
- dependency-name: sentry-sdk
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* Speed up tests (#638)

* Better usage of cache for groups and clubs related operations (#634)

* Better usage of cache for group retrieval

* Cache clearing on object deletion or update

* replace signals by save and delete override

* add is_anonymous check in is_owned_by

Add in many is_owned_by(self, user) methods that user is not anonymous. Since many of those functions do db queries, this should reduce a little bit the load of the db.

* Stricter usage of User.is_in_group

Constrain the parameters that can be passed to the function to make sure only a str or an int can be used. Also force to explicitly specify if the group id or the group name is used.

* write test and correct bugs

* remove forgotten populate commands

* Correct test

* [FIX] Correction de bugs (#617)

* Fix #600

* Fix #602

* Fixes & améliorations du nouveau CSS (#616)

* Fix #604

* should fix #605

* Fix #608

* Update core/views/site.py

Co-Authored-By: thomas girod <56346771+imperosol@users.noreply.github.com>

* Added back the permission denied

* Should fix #609

* Fix failing test when 2 user are merged

* Should fix #610

* Should fix #627

* Should fix #109

Block les URLs suivantes lorsque le fichier se trouve dans le dir `profiles` ou `SAS` :
- `/file/<id>/`
- `/file/<id>/[delete|prop|edit]`

> Les urls du SAS restent accessiblent pour les roots & les admins SAS
> Les urls de profiles sont uniquement accessiblent aux roots

* Fix root dir of SAS being unnaccessible for sas admins

⚠️ need to edit the SAS directory & save it (no changes required in sas directory properties)

* Remove overwritten code

* Should fix duplicated albums in user profile (wtf)

* Fix typo

* Extended profiles picture access to board members

* Should fix #607

* Fix keyboard navigation not working properly

* Fix user tagged pictures section inside python rather than in the template

* Update utils.py

* Apply suggested changes

* Fix #604

* Fix #608

* Added back the permission denied

* Should fix duplicated albums in user profile (wtf)

* Fix user tagged pictures section inside python rather than in the template

* Apply suggested changes

---------

Co-authored-by: thomas girod <56346771+imperosol@users.noreply.github.com>

* Remove duplicated css

* Galaxy improvements (#628)

* galaxy: improve logging and performance reporting

* galaxy: add a full galaxy state test

* galaxy: optimize user self score computation

* galaxy: add 'generate_galaxy_test_data' command for development at scale

* galaxy: big refactor

Main changes:
  - Multiple Galaxy objects can now exist at the same time in DB. This allows for ruling a new galaxy while still
    displaying the old one.
  - The criteria to quickly know whether a user is a possible citizen is now a simple query on picture count. This
    avoids a very complicated query to database, that could often result in huge working memory load. With this change,
    it should be possible to run the galaxy even on a vanilla Postgres that didn't receive fine tuning for the Sith's
    galaxy.

* galaxy: template: make the galaxy graph work and be usable with a lot of stars

- Display focused star and its connections clearly
- Display star label faintly by default for other stars to avoid overloading the graph
- Hide non-focused lanes
- Avoid clicks on non-highlighted, too far stars
- Make the canva adapt its width to initial screen size, doesn't work dynamically

* galaxy: better docstrings

* galaxy: use bulk_create whenever possible

This is a big performance gain, especially for the tests.

Examples:

----

`./manage.py test galaxy.tests.GalaxyTest.test_full_galaxy_state`

Measurements averaged over 3 run on *my machine*™:
Before: 2min15s
After: 1m41s

----

`./manage.py generate_galaxy_test_data --user-pack-count 1`

Before: 48s
After: 25s

----

`./manage.py rule_galaxy` (for 600 citizen, corresponding to 1 user-pack)

Before: 14m4s
After: 12m34s

* core: populate: use a less ambiguous 'timezone.now()'

When running the tests around midnight, the day is changing, leading to some values being offset to the next day
depending on the timezone, and making some tests to fail. This ensure to use a less ambiguous `now` when populating
the database.

* write more extensive documentation

- add documentation to previously documented classes and functions and refactor some of the documented one, in accordance to the PEP257 and ReStructuredText standards ;
- add some type hints ;
- use a NamedTuple for the `Galaxy.compute_users_score` method instead of a raw tuple. Also change a little bit the logic in the function which call the latter ;
- add some additional parameter checks on a few functions ;
- change a little bit the logic of the log level setting for the galaxy related commands.

* galaxy: tests: split Model and View for more efficient data usage

---------

Co-authored-by: maréchal <thgirod@hotmail.com>

* [UPDATE] Bump libsass from 0.21.0 to 0.22.0 (#640)

Bumps [libsass](https://github.com/sass/libsass-python) from 0.21.0 to 0.22.0.
- [Release notes](https://github.com/sass/libsass-python/releases)
- [Changelog](https://github.com/sass/libsass-python/blob/main/docs/changes.rst)
- [Commits](https://github.com/sass/libsass-python/compare/0.21.0...0.22.0)

---
updated-dependencies:
- dependency-name: libsass
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* [FIX] Fix cached groups (#647)

* Bump sqlparse from 0.4.3 to 0.4.4 (#645)

Bumps [sqlparse](https://github.com/andialbrecht/sqlparse) from 0.4.3 to 0.4.4.
- [Release notes](https://github.com/andialbrecht/sqlparse/releases)
- [Changelog](https://github.com/andialbrecht/sqlparse/blob/master/CHANGELOG)
- [Commits](https://github.com/andialbrecht/sqlparse/compare/0.4.3...0.4.4)

---
updated-dependencies:
- dependency-name: sqlparse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* [UPDATE] Bump django-ordered-model from 3.6 to 3.7.4 (#625)

Bumps [django-ordered-model](https://github.com/django-ordered-model/django-ordered-model) from 3.6 to 3.7.4.
- [Release notes](https://github.com/django-ordered-model/django-ordered-model/releases)
- [Changelog](https://github.com/django-ordered-model/django-ordered-model/blob/master/CHANGES.md)
- [Commits](https://github.com/django-ordered-model/django-ordered-model/compare/3.6...3.7.4)

---
updated-dependencies:
- dependency-name: django-ordered-model
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Fix immutable default variable in `get_start_of_semester` (#656)

Le serveur ne percevait pas le changement de semestre, parce
que la valeur par défaut passée à la fonction `get_start_of_semester()` était une fonction appelée une seule fois, lors du lancement du serveur. Bref, c'était ça : https://beta.ruff.rs/docs/rules/function-call-in-default-argument/

---------

Co-authored-by: imperosol <thgirod@hotmail.com>

* Add missing method on AnonymousUser (#649)

* Add eurocks partnership in the eboutic (#661)

* Add eurocks partnership in the eboutic (#661)

Revert "Add eurocks partnership in the eboutic (#661)"

This reverts commit 193c820757.

Add eurocks partnership in the eboutic (#661)

* Update workflow

Following this update : https://github.blog/changelog/2023-09-13-github-actions-updates-to-github_ref-and-github-ref/

* Update workflow

* Remove eurocks tickets from eboutic (event is finished)

* Links update & translations typos fixes (#671)

* Remove BDF link (as BDF is now part of AE)

* Remove unused pages

* Fix typos

* Fix typo again

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Thomas Girod <thgirod@hotmail.com>
Co-authored-by: thomas girod <56346771+imperosol@users.noreply.github.com>
Co-authored-by: Théo DURR <git@theodurr.fr>
Co-authored-by: Skia <skia@hya.sk>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Bartuccio Antoine <klmp200@users.noreply.github.com>
2023-10-10 15:41:19 +02:00
4303d51c0a Merge branch 'master' into taiste 2023-10-10 15:32:46 +02:00
d16bf12611 Links update & translations typos fixes (#671)
* Remove BDF link (as BDF is now part of AE)

* Remove unused pages

* Fix typos

* Fix typo again
2023-10-10 15:29:02 +02:00
4231a7972d Remove eurocks tickets from eboutic (event is finished) 2023-10-04 14:27:21 +02:00
c436d39014 [PARTENARIAT] Partenariat Eurockéennes (#663) 2023-09-20 17:57:26 +02:00
51a12814f9 Update workflow 2023-09-19 22:17:26 +02:00
00ae6e4623 Update workflow
Following this update : https://github.blog/changelog/2023-09-13-github-actions-updates-to-github_ref-and-github-ref/
2023-09-19 22:04:46 +02:00
4b587e8711 Merge branch 'taiste' of https://github.com/ae-utbm/sith3 into taiste 2023-09-19 21:31:02 +02:00
d2f377b54f Add eurocks partnership in the eboutic (#661)
Revert "Add eurocks partnership in the eboutic (#661)"

This reverts commit 193c820757.

Add eurocks partnership in the eboutic (#661)
2023-09-19 21:29:17 +02:00
193c820757 Add eurocks partnership in the eboutic (#661) 2023-09-19 20:59:22 +02:00
b9298792ae Mise à jour de septembre 2023 (#659)
* integration of 3D secure v2 for eboutic bank payment

* edit yml to avoid git conflict when deploying on test

* escape html characters on xml (#505)

* Change country id to ISO 3166 1 numeric for 3DSV2 (#510)

* remove useless tests

* Fix le panier de l'Eboutic pour Safari (#518)

Co-authored-by: Théo DURR <git@theodurr.fr>
Co-authored-by: thomas girod <56346771+imperosol@users.noreply.github.com>

* update some dependencies (#523)

* [Eboutic] Fix double quote issue & improved user experience on small screen (#522)

* Fix #511 Regex issue with escaped double quotes

* Fix basket being when reloading the page (when cookie != "")

+ Added JSDoc
+ Cleaned some code

* Fix #509 Improved user experience on small screens

* Fix css class not being added back when reloading page

* CSS Fixes (see description)

+ Fixed overlaping item title with the cart emoji on small screen
+ Fixed minimal size of the basket on small screen (full width)

* Added darkened background circle to items with no image

* Fix issue were the basket could be None


* Edited CSS to have bette img ratio & the 🛒 icon

Adapt, Improve, Overcome

* Moved basket down on small screen size

* enhance admin pages

* update documentation

* Update doc/about/tech.rst

Co-authored-by: Julien Constant <49886317+Juknum@users.noreply.github.com>

* remove csrf_token

* Fix 3DSv2 implementation (#542)

* Fixed wrong HMAC signature generation

* Fix xml du panier

Co-authored-by: Julien Constant <julienconstant190@gmail.com>

* [FIX] 3DSv2 - Echappement du XML et modif tables (#543)

* Fixed wrong HMAC signature generation
* Updated migration files

Co-authored-by: Julien Constant <julienconstant190@gmail.com>

* Update doc/about/tech.rst

* Update doc/start/install.rst

* Updated lock file according to pyproject

* unify account_id creation

* upgrade re_path to path (#533)

* redirect directly on counter if user is barman

* Passage de vue à Alpine pour les comptoirs (#561)

Vue, c'est cool, mais avec Django c'est un peu chiant à utiliser. Alpine a l'avantage d'être plus léger et d'avoir une syntaxe qui ne ressemble pas à celle de Jinja (ce qui évite d'avoir à mettre des {% raw %} partout).

* resolved importError (#565)

* Add galaxy (#562)

* style.scss: lint

* style.scss: add 'th' padding

* core: populate: add much more data for development

* Add galaxy

* repair user merging tool (#498)

* Disabled galaxy feature (only visually)

* Disabled Galaxy button & Removed 404 exception display

* Update 404.jinja

* Fixed broken test

* Added eurocks links to eboutic

* fix typo

* fix wording

Co-authored-by: Théo DURR <git@theodurr.fr>

* Edited unit tests

This test caused a breach in security due to the alert block displaying sensitive data.

* Repair NaN bug for autocomplete on counter click

* remove-useless-queries-counter-stats (#519)

* Amélioration des pages utilisateurs pour les petits écrans (#578, #520)

- Refonte de l'organisation des pages utilisateurs (principalement du front)
  - Page des parrains/fillots
  - Page d'édition du profil
  - Page du profil
  - Page des outils
  - Page des préférences
  - Page des stats utilisateurs

- Refonte du CSS / organisation de la navbar principale (en haut de l'écran)
- Refonte du CSS de la navbar bleu clair (le menu)
- Refonte du CSS du SAS :
  - Page de photo
  - Page d'albums

* Added GA/Clubs Google Calendar to main page (#585)

* Added GA/Clubs google calendar to main page

* Made tables full width

* Create dependabot.yml (#587)

* Bump django from 3.2.16 to 3.2.18 (#574)

* [CSS] Follow up of #578 (#589)

* [FIX] Broken link in readme and license fix (& update) (#591)

* Fixes pour la mise à jour de mars (#598)

* Fix problème de cache dans le SAS & améliore le CSS du SAS

Co-authored-by: Bartuccio Antoine <klmp200@users.noreply.github.com>

* Fixes & améliorations du nouveau CSS (#616)

* [UPDATE] Bump sentry-sdk from 1.12.1 to 1.19.1 (#620)

* [FIX] Fixes supplémentaires pour la màj de mars (#622)

- Les photos de l'onglet de la page utilisateur utilise désormais leur version thumbnail au lieu de leur version HD
- Une des classes du CSS du SAS a été renommée car elle empiétait sur une class de la navbar
- Le profil utilisateur a été revu pour ajouter plus d'espacement entre le tableau des cotisations et le numéro de cotisants
- Les images de forum & blouse sont de nouveau cliquable pour les afficher en grands
- Sur mobile, lorsqu'on cliquait sur le premier élément de la navbar, ce dernier avait un overlay avec des angles arrondis
- Sur mobile, les utilisateurs avec des images de profils non carrées dépassait dans l'onglet Famille

* [UPDATE] Bump dict2xml from 1.7.2 to 1.7.3 (#592)

Bumps [dict2xml](https://github.com/delfick/python-dict2xml) from 1.7.2 to 1.7.3.
- [Release notes](https://github.com/delfick/python-dict2xml/releases)
- [Commits](https://github.com/delfick/python-dict2xml/compare/release-1.7.2...release-1.7.3)

---
updated-dependencies:
- dependency-name: dict2xml
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

* [UPDATE] Bump django-debug-toolbar from 3.8.1 to 4.0.0 (#593)

Bumps [django-debug-toolbar](https://github.com/jazzband/django-debug-toolbar) from 3.8.1 to 4.0.0.
- [Release notes](https://github.com/jazzband/django-debug-toolbar/releases)
- [Changelog](https://github.com/jazzband/django-debug-toolbar/blob/main/docs/changes.rst)
- [Commits](https://github.com/jazzband/django-debug-toolbar/compare/3.8.1...4.0.0)

---
updated-dependencies:
- dependency-name: django-debug-toolbar
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* [UPDATE] Bump cryptography from 37.0.4 to 40.0.1 (#594)

* [UPDATE] Bump cryptography from 37.0.4 to 40.0.1

Bumps [cryptography](https://github.com/pyca/cryptography) from 37.0.4 to 40.0.1.
- [Release notes](https://github.com/pyca/cryptography/releases)
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/37.0.4...40.0.1)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* Updated pyOpenSSL to match cryptography requirements

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Julien Constant <julienconstant190@gmail.com>

* Mise à jour de Black vers la version 23.3 (#629)

* update link for poetry install

* [UPDATE] Bump django-countries from 7.5 to 7.5.1 (#624)

Bumps [django-countries](https://github.com/SmileyChris/django-countries) from 7.5 to 7.5.1.
- [Release notes](https://github.com/SmileyChris/django-countries/releases)
- [Changelog](https://github.com/SmileyChris/django-countries/blob/main/CHANGES.rst)
- [Commits](https://github.com/SmileyChris/django-countries/compare/v7.5...v7.5.1)

---
updated-dependencies:
- dependency-name: django-countries
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* [UPDATE] Bump sentry-sdk from 1.19.1 to 1.21.0

Bumps [sentry-sdk](https://github.com/getsentry/sentry-python) from 1.19.1 to 1.21.0.
- [Release notes](https://github.com/getsentry/sentry-python/releases)
- [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-python/compare/1.19.1...1.21.0)

---
updated-dependencies:
- dependency-name: sentry-sdk
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* Speed up tests (#638)

* Better usage of cache for groups and clubs related operations (#634)

* Better usage of cache for group retrieval

* Cache clearing on object deletion or update

* replace signals by save and delete override

* add is_anonymous check in is_owned_by

Add in many is_owned_by(self, user) methods that user is not anonymous. Since many of those functions do db queries, this should reduce a little bit the load of the db.

* Stricter usage of User.is_in_group

Constrain the parameters that can be passed to the function to make sure only a str or an int can be used. Also force to explicitly specify if the group id or the group name is used.

* write test and correct bugs

* remove forgotten populate commands

* Correct test

* [FIX] Correction de bugs (#617)

* Fix #600

* Fix #602

* Fixes & améliorations du nouveau CSS (#616)

* Fix #604

* should fix #605

* Fix #608

* Update core/views/site.py

Co-Authored-By: thomas girod <56346771+imperosol@users.noreply.github.com>

* Added back the permission denied

* Should fix #609

* Fix failing test when 2 user are merged

* Should fix #610

* Should fix #627

* Should fix #109

Block les URLs suivantes lorsque le fichier se trouve dans le dir `profiles` ou `SAS` :
- `/file/<id>/`
- `/file/<id>/[delete|prop|edit]`

> Les urls du SAS restent accessiblent pour les roots & les admins SAS
> Les urls de profiles sont uniquement accessiblent aux roots

* Fix root dir of SAS being unnaccessible for sas admins

⚠️ need to edit the SAS directory & save it (no changes required in sas directory properties)

* Remove overwritten code

* Should fix duplicated albums in user profile (wtf)

* Fix typo

* Extended profiles picture access to board members

* Should fix #607

* Fix keyboard navigation not working properly

* Fix user tagged pictures section inside python rather than in the template

* Update utils.py

* Apply suggested changes

* Fix #604

* Fix #608

* Added back the permission denied

* Should fix duplicated albums in user profile (wtf)

* Fix user tagged pictures section inside python rather than in the template

* Apply suggested changes

---------

Co-authored-by: thomas girod <56346771+imperosol@users.noreply.github.com>

* Remove duplicated css

* Galaxy improvements (#628)

* galaxy: improve logging and performance reporting

* galaxy: add a full galaxy state test

* galaxy: optimize user self score computation

* galaxy: add 'generate_galaxy_test_data' command for development at scale

* galaxy: big refactor

Main changes:
  - Multiple Galaxy objects can now exist at the same time in DB. This allows for ruling a new galaxy while still
    displaying the old one.
  - The criteria to quickly know whether a user is a possible citizen is now a simple query on picture count. This
    avoids a very complicated query to database, that could often result in huge working memory load. With this change,
    it should be possible to run the galaxy even on a vanilla Postgres that didn't receive fine tuning for the Sith's
    galaxy.

* galaxy: template: make the galaxy graph work and be usable with a lot of stars

- Display focused star and its connections clearly
- Display star label faintly by default for other stars to avoid overloading the graph
- Hide non-focused lanes
- Avoid clicks on non-highlighted, too far stars
- Make the canva adapt its width to initial screen size, doesn't work dynamically

* galaxy: better docstrings

* galaxy: use bulk_create whenever possible

This is a big performance gain, especially for the tests.

Examples:

----

`./manage.py test galaxy.tests.GalaxyTest.test_full_galaxy_state`

Measurements averaged over 3 run on *my machine*™:
Before: 2min15s
After: 1m41s

----

`./manage.py generate_galaxy_test_data --user-pack-count 1`

Before: 48s
After: 25s

----

`./manage.py rule_galaxy` (for 600 citizen, corresponding to 1 user-pack)

Before: 14m4s
After: 12m34s

* core: populate: use a less ambiguous 'timezone.now()'

When running the tests around midnight, the day is changing, leading to some values being offset to the next day
depending on the timezone, and making some tests to fail. This ensure to use a less ambiguous `now` when populating
the database.

* write more extensive documentation

- add documentation to previously documented classes and functions and refactor some of the documented one, in accordance to the PEP257 and ReStructuredText standards ;
- add some type hints ;
- use a NamedTuple for the `Galaxy.compute_users_score` method instead of a raw tuple. Also change a little bit the logic in the function which call the latter ;
- add some additional parameter checks on a few functions ;
- change a little bit the logic of the log level setting for the galaxy related commands.

* galaxy: tests: split Model and View for more efficient data usage

---------

Co-authored-by: maréchal <thgirod@hotmail.com>

* [UPDATE] Bump libsass from 0.21.0 to 0.22.0 (#640)

Bumps [libsass](https://github.com/sass/libsass-python) from 0.21.0 to 0.22.0.
- [Release notes](https://github.com/sass/libsass-python/releases)
- [Changelog](https://github.com/sass/libsass-python/blob/main/docs/changes.rst)
- [Commits](https://github.com/sass/libsass-python/compare/0.21.0...0.22.0)

---
updated-dependencies:
- dependency-name: libsass
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* [FIX] Fix cached groups (#647)

* Bump sqlparse from 0.4.3 to 0.4.4 (#645)

Bumps [sqlparse](https://github.com/andialbrecht/sqlparse) from 0.4.3 to 0.4.4.
- [Release notes](https://github.com/andialbrecht/sqlparse/releases)
- [Changelog](https://github.com/andialbrecht/sqlparse/blob/master/CHANGELOG)
- [Commits](https://github.com/andialbrecht/sqlparse/compare/0.4.3...0.4.4)

---
updated-dependencies:
- dependency-name: sqlparse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* [UPDATE] Bump django-ordered-model from 3.6 to 3.7.4 (#625)

Bumps [django-ordered-model](https://github.com/django-ordered-model/django-ordered-model) from 3.6 to 3.7.4.
- [Release notes](https://github.com/django-ordered-model/django-ordered-model/releases)
- [Changelog](https://github.com/django-ordered-model/django-ordered-model/blob/master/CHANGES.md)
- [Commits](https://github.com/django-ordered-model/django-ordered-model/compare/3.6...3.7.4)

---
updated-dependencies:
- dependency-name: django-ordered-model
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Fix immutable default variable in `get_start_of_semester` (#656)

Le serveur ne percevait pas le changement de semestre, parce
que la valeur par défaut passée à la fonction `get_start_of_semester()` était une fonction appelée une seule fois, lors du lancement du serveur. Bref, c'était ça : https://beta.ruff.rs/docs/rules/function-call-in-default-argument/

---------

Co-authored-by: imperosol <thgirod@hotmail.com>

* Add missing method on AnonymousUser (#649)

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Thomas Girod <thgirod@hotmail.com>
Co-authored-by: thomas girod <56346771+imperosol@users.noreply.github.com>
Co-authored-by: Théo DURR <git@theodurr.fr>
Co-authored-by: Skia <skia@hya.sk>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Bartuccio Antoine <klmp200@users.noreply.github.com>
2023-09-09 13:09:13 +02:00
aaf30ab965 Add missing method on AnonymousUser (#649) 2023-09-07 23:53:42 +02:00
2db66e6154 Merge branch 'master' into taiste 2023-09-07 23:44:09 +02:00
38295e591d Fix immutable default variable in get_start_of_semester (#656)
Le serveur ne percevait pas le changement de semestre, parce
que la valeur par défaut passée à la fonction `get_start_of_semester()` était une fonction appelée une seule fois, lors du lancement du serveur. Bref, c'était ça : https://beta.ruff.rs/docs/rules/function-call-in-default-argument/

---------

Co-authored-by: imperosol <thgirod@hotmail.com>
2023-09-07 23:11:58 +02:00
544b0248b2 [UPDATE] Bump django-ordered-model from 3.6 to 3.7.4 (#625)
Bumps [django-ordered-model](https://github.com/django-ordered-model/django-ordered-model) from 3.6 to 3.7.4.
- [Release notes](https://github.com/django-ordered-model/django-ordered-model/releases)
- [Changelog](https://github.com/django-ordered-model/django-ordered-model/blob/master/CHANGES.md)
- [Commits](https://github.com/django-ordered-model/django-ordered-model/compare/3.6...3.7.4)

---
updated-dependencies:
- dependency-name: django-ordered-model
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-06 12:01:44 +02:00
2bccf633d5 Bump sqlparse from 0.4.3 to 0.4.4 (#645)
Bumps [sqlparse](https://github.com/andialbrecht/sqlparse) from 0.4.3 to 0.4.4.
- [Release notes](https://github.com/andialbrecht/sqlparse/releases)
- [Changelog](https://github.com/andialbrecht/sqlparse/blob/master/CHANGELOG)
- [Commits](https://github.com/andialbrecht/sqlparse/compare/0.4.3...0.4.4)

---
updated-dependencies:
- dependency-name: sqlparse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-06 11:37:28 +02:00
4f9d5ae7b1 Revert "[PARTENARIAT] Ajout vitrine d'achat billets eurockéennes 2023 (#582)"
This reverts commit b12e8dc147.
2023-07-02 18:22:14 +02:00
259337dff1 [FIX] Fix cached groups (#647) 2023-05-12 13:29:16 +02:00
84768eb74e [FIX] Fix cached groups (#647) 2023-05-12 13:27:51 +02:00
288764b551 Mise à jour d'avril (#643) 2023-05-10 11:56:33 +02:00
944 changed files with 52329 additions and 70439 deletions

14
.envrc
View File

@ -1,14 +1,6 @@
if [[ ! -f pyproject.toml ]]; then if [[ ! -d .venv ]]; then
log_error 'No pyproject.toml found. Use `poetry new` or `poetry init` to create one first.' log_error 'No .venv folder found. Use `uv sync` to create one first.'
exit 2 exit 2
fi fi
local VENV=$(poetry env list --full-path | cut -d' ' -f1) . .venv/bin/activate
if [[ -z $VENV || ! -d $VENV/bin ]]; then
log_error 'No poetry virtual environment found. Use `poetry install` to create one first.'
exit 2
fi
export VIRTUAL_ENV=$VENV
export POETRY_ACTIVE=1
PATH_add "$VENV/bin"

View File

@ -1,8 +0,0 @@
name: "Compile messages"
description: "Compile the gettext translation messages"
runs:
using: composite
steps:
- name: Setup project
run: poetry run ./manage.py compilemessages
shell: bash

View File

@ -6,48 +6,41 @@ runs:
- name: Install apt packages - name: Install apt packages
uses: awalsh128/cache-apt-pkgs-action@latest uses: awalsh128/cache-apt-pkgs-action@latest
with: with:
packages: gettext libxapian-dev libgraphviz-dev packages: gettext
version: 1.0 # increment to reset cache version: 1.0 # increment to reset cache
- name: Install dependencies - name: Install uv
run: | uses: astral-sh/setup-uv@v5
sudo apt update
sudo apt install gettext libxapian-dev libgraphviz-dev
shell: bash
- name: Set up python
uses: actions/setup-python@v4
with: with:
python-version: "3.10" version: "0.5.14"
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: Load cached Poetry installation - name: "Set up Python"
id: cached-poetry uses: actions/setup-python@v5
uses: actions/cache@v3
with: with:
path: ~/.local python-version-file: ".python-version"
key: poetry-0 # increment to reset cache
- name: Install Poetry - name: Restore cached virtualenv
if: steps.cached-poetry.outputs.cache-hit != 'true' uses: actions/cache/restore@v4
shell: bash
run: curl -sSL https://install.python-poetry.org | python3 -
- name: Check pyproject.toml syntax
shell: bash
run: poetry check
- name: Load cached dependencies
uses: actions/cache@v3
with: with:
path: ~/.cache/pypoetry key: venv-${{ runner.os }}-${{ hashFiles('.python-version') }}-${{ hashFiles('pyproject.toml') }}-${{ env.CACHE_SUFFIX }}
key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }} path: .venv
restore-keys: |
${{ runner.os }}-poetry-
- name: Install dependencies - name: Install dependencies
run: poetry install -E testing -E docs run: uv sync
shell: bash shell: bash
- name: Install Xapian
run: uv run ./manage.py install_xapian
shell: bash
- name: Save cached virtualenv
uses: actions/cache/save@v4
with:
key: venv-${{ runner.os }}-${{ hashFiles('.python-version') }}-${{ hashFiles('pyproject.toml') }}-${{ env.CACHE_SUFFIX }}
path: .venv
- name: Compile gettext messages - name: Compile gettext messages
run: poetry run ./manage.py compilemessages run: uv run ./manage.py compilemessages
shell: bash shell: bash

View File

@ -1,10 +0,0 @@
name: "Setup xapian"
description: "Setup the xapian indexes"
runs:
using: composite
steps:
- name: Setup xapian index
run: |
mkdir -p /dev/shm/search_indexes
ln -s /dev/shm/search_indexes sith/search_indexes
shell: bash

View File

@ -8,11 +8,7 @@ updates:
- package-ecosystem: "pip" # See documentation for possible values - package-ecosystem: "pip" # See documentation for possible values
directory: "/" # Location of package manifests directory: "/" # Location of package manifests
schedule: schedule:
interval: "daily" interval: "weekly"
# Raise pull requests for version updates
# to pip against the `develop` branch
target-branch: "taiste" target-branch: "taiste"
reviewers:
- "ae-utbm/developpers-v3"
commit-message: commit-message:
prefix: "[UPDATE] " prefix: "[UPDATE] "

View File

@ -1,41 +1,45 @@
name: Sith 3 CI name: Sith CI
on: on:
push: push:
branches: branches: [master, taiste]
- master
- taiste
pull_request: pull_request:
branches: branches: [master, taiste]
- master workflow_dispatch:
- taiste
jobs: jobs:
black: pre-commit:
name: Black format name: Launch pre-commits checks (ruff)
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out repository - uses: actions/checkout@v4
uses: actions/checkout@v3 - uses: actions/setup-python@v5
- name: Setup Project with:
uses: ./.github/actions/setup_project python-version-file: ".python-version"
- run: poetry run black --check . - uses: pre-commit/action@v3.0.1
with:
extra_args: --all-files
tests: tests:
name: Run tests and generate coverage report name: Run tests and generate coverage report
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
fail-fast: false # don't interrupt the other test processes
matrix:
pytest-mark: [slow, not slow]
steps: steps:
- name: Check out repository - name: Check out repository
uses: actions/checkout@v3 uses: actions/checkout@v4
- uses: ./.github/actions/setup_project - uses: ./.github/actions/setup_project
- uses: ./.github/actions/setup_xapian env:
- uses: ./.github/actions/compile_messages # To avoid race conditions on environment cache
CACHE_SUFFIX: ${{ matrix.pytest-mark }}
- name: Run tests - name: Run tests
run: poetry run coverage run ./manage.py test run: uv run coverage run -m pytest -m "${{ matrix.pytest-mark }}"
- name: Generate coverage report - name: Generate coverage report
run: | run: |
poetry run coverage report uv run coverage report
poetry run coverage html uv run coverage html
- name: Archive code coverage results - name: Archive code coverage results
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:

View File

@ -14,7 +14,7 @@ jobs:
steps: steps:
- name: SSH Remote Commands - name: SSH Remote Commands
uses: appleboy/ssh-action@dce9d565de8d876c11d93fa4fe677c0285a66d78 uses: appleboy/ssh-action@v1.1.0
with: with:
# Proxy # Proxy
proxy_host : ${{secrets.PROXY_HOST}} proxy_host : ${{secrets.PROXY_HOST}}
@ -31,17 +31,18 @@ jobs:
script_stop: true script_stop: true
# See https://github.com/ae-utbm/sith3/wiki/GitHub-Actions#deployment-action # See https://github.com/ae-utbm/sith/wiki/GitHub-Actions#deployment-action
script: | script: |
export PATH="/home/sith/.local/bin:$PATH" cd ${{secrets.SITH_PATH}}
pushd ${{secrets.SITH_PATH}}
git pull git fetch
poetry install git reset --hard origin/master
poetry run ./manage.py migrate uv sync --group prod
echo "yes" | poetry run ./manage.py collectstatic npm install
poetry run ./manage.py compilestatic uv run ./manage.py install_xapian
poetry run ./manage.py compilemessages uv run ./manage.py migrate
uv run ./manage.py collectstatic --clear --noinput
uv run ./manage.py compilemessages
sudo systemctl restart uwsgi sudo systemctl restart uwsgi
@ -51,14 +52,14 @@ jobs:
timeout-minutes: 30 timeout-minutes: 30
needs: deployment needs: deployment
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Sentry Release - name: Sentry Release
uses: getsentry/action-release@v1.2.0 uses: getsentry/action-release@v1.7.0
env: env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }} SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
SENTRY_URL: ${{ secrets.SENTRY_URL }} SENTRY_URL: ${{ secrets.SENTRY_URL }}
with: with:
environment: production environment: production

21
.github/workflows/deploy_docs.yml vendored Normal file
View File

@ -0,0 +1,21 @@
name: deploy_docs
on:
push:
branches:
- master
permissions:
contents: write
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup_project
- run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
- uses: actions/cache@v3
with:
key: mkdocs-material-${{ env.cache_id }}
path: .cache
restore-keys: |
mkdocs-material-
- run: uv run mkdocs gh-deploy --force

View File

@ -1,8 +1,9 @@
name: Sith3 taiste name: Sith taiste
on: on:
push: push:
branches: [ taiste ] branches: [taiste]
workflow_dispatch:
jobs: jobs:
deployment: deployment:
@ -12,7 +13,7 @@ jobs:
steps: steps:
- name: SSH Remote Commands - name: SSH Remote Commands
uses: appleboy/ssh-action@dce9d565de8d876c11d93fa4fe677c0285a66d78 uses: appleboy/ssh-action@v1.1.0
with: with:
# Proxy # Proxy
proxy_host : ${{secrets.PROXY_HOST}} proxy_host : ${{secrets.PROXY_HOST}}
@ -29,34 +30,17 @@ jobs:
script_stop: true script_stop: true
# See https://github.com/ae-utbm/sith3/wiki/GitHub-Actions#deployment-action # See https://github.com/ae-utbm/sith/wiki/GitHub-Actions#deployment-action
script: | script: |
export PATH="$HOME/.poetry/bin:$PATH" cd ${{secrets.SITH_PATH}}
pushd ${{secrets.SITH_PATH}}
git pull git fetch
poetry install git reset --hard origin/taiste
poetry run ./manage.py migrate uv sync --group prod
echo "yes" | poetry run ./manage.py collectstatic npm install
poetry run ./manage.py compilestatic uv run ./manage.py install_xapian
poetry run ./manage.py compilemessages uv run ./manage.py migrate
uv run ./manage.py collectstatic --clear --noinput
uv run ./manage.py compilemessages
sudo systemctl restart uwsgi sudo systemctl restart uwsgi
sentry:
runs-on: ubuntu-latest
environment: taiste
timeout-minutes: 30
needs: deployment
steps:
- uses: actions/checkout@v3
- name: Sentry Release
uses: getsentry/action-release@v1.2.0
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
SENTRY_URL: ${{ secrets.SENTRY_URL }}
with:
environment: taiste

9
.gitignore vendored
View File

@ -1,4 +1,4 @@
db.sqlite3 *.sqlite3
*.log *.log
*.pyc *.pyc
*.mo *.mo
@ -8,7 +8,7 @@ pyrightconfig.json
dist/ dist/
.vscode/ .vscode/
.idea/ .idea/
env/ .venv/
doc/html doc/html
data/ data/
galaxy/test_galaxy_state.json galaxy/test_galaxy_state.json
@ -17,4 +17,7 @@ sith/settings_custom.py
sith/search_indexes/ sith/search_indexes/
.coverage .coverage
coverage_report/ coverage_report/
doc/_build node_modules/
# compiled documentation
site/

26
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,26 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.8.3
hooks:
- id: ruff # just check the code, and print the errors
- id: ruff # actually fix the fixable errors, but print nothing
args: ["--fix", "--silent"]
# Run the formatter.
- id: ruff-format
- repo: https://github.com/biomejs/pre-commit
rev: "v0.1.0" # Use the sha / tag you want to point at
hooks:
- id: biome-check
additional_dependencies: ["@biomejs/biome@1.9.3"]
- repo: https://github.com/rtts/djhtml
rev: 3.0.7
hooks:
- id: djhtml
name: format templates
entry: djhtml --tabwidth 2
types: ["jinja"]
- id: djcss
name: format scss files
entry: djcss --tabwidth 2
types: ["scss"]

1
.python-version Normal file
View File

@ -0,0 +1 @@
3.12

View File

@ -1,26 +0,0 @@
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
# Required
version: 2
# Allow installing xapian-bindings in pip
build:
apt_packages:
- libxapian-dev
# Build documentation in the doc/ directory with Sphinx
sphinx:
configuration: doc/conf.py
# Optionally build your docs in additional formats such as PDF and ePub
formats: all
# Optionally set the version of Python and requirements required to build your docs
python:
version: "3.8"
install:
- method: pip
path: .
extra_requirements:
- docs

View File

@ -1,40 +1,21 @@
<p align="center"> # Sith
<a href="#">
<img src="https://img.shields.io/badge/Code%20Style-Black-000000?style=for-the-badge">
</a>
<a href="#">
<img src="https://img.shields.io/github/checks-status/ae-utbm/sith3/master?logo=github&style=for-the-badge&label=BUILD">
</a>
<a href="https://sith-ae.readthedocs.io/">
<img src="https://img.shields.io/readthedocs/sith-ae?logo=readthedocs&style=for-the-badge">
</a>
<a href="https://discord.gg/XK9WfPsUFm">
<img src="https://img.shields.io/discord/971448179075731476?label=Discord&logo=discord&style=for-the-badge">
</a>
</p>
<h3 align="center">This is the source code of the UTBM's student association available at https://ae.utbm.fr/.</h3> [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](#)
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
[![CI status](https://github.com/ae-utbm/sith/actions/workflows/ci.yml/badge.svg)](#)
[![Docs status](https://github.com/ae-utbm/sith/actions/workflows/deploy_docs.yml/badge.svg)](https://ae-utbm.github.io/sith)
[![Built with Material for MkDocs](https://img.shields.io/badge/Material_for_MkDocs-526CFE?style=default&logo=MaterialForMkDocs&logoColor=white)](https://squidfunk.github.io/mkdocs-material/)
[![Checked with Biome](https://img.shields.io/badge/Checked_with-Biome-60a5fa?style=flat&logo=biome)](https://biomejs.dev)
[![discord](https://img.shields.io/discord/971448179075731476?label=discord&logo=discord&style=default)](https://discord.gg/xk9wfpsufm)
<p align="justify">All documentation is in the <code>docs</code> directory and online at https://sith-ae.readthedocs.io/. This documentation is written in French because it targets a French audience and it's too much work to maintain two versions. The code and code comments are strictly written in English.</p> ### This is the source code of the UTBM's student association available at [https://ae.utbm.fr/](https://ae.utbm.fr/).
<h4>If you want to contribute, here's how we recommend to read the docs:</h4> All documentation is in the `docs` directory and online at [https://ae-utbm.github.io/sith](https://ae-utbm.github.io/sith). This documentation is written in French because it targets a French audience and it's too much work to maintain two versions. The code and code comments are strictly written in English.
<ul> #### If you want to contribute, here's how we recommend to read the docs:
<li>
<p align="justify"> * First, it's advised to read the about part of the project to understand the goals and the mindset of the current and previous maintainers and know what to expect to learn.
First, it's advised to read the about part of the project to understand the goals and the mindset of the current and previous maintainers and know what to expect to learn. * If in the first part you realize that you need more background about what we use, we provide some links to tutorials and documentation at the end of our documentation. Feel free to use it and complete it with what you found helpful.
</p> * Keep in mind that this documentation is thought to be read in order.
</li>
<li>
<p align="justify">
If in the first part you realize that you need more background about what we use, we provide some links to tutorials and documentation at the end of our documentation. Feel free to use it and complete it with what you found helpful.
</p>
</li>
<li>
<p align="justify">
Keep in mind that this documentation is thought to be read in order.
</p>
</li>
</ul>
> This project is licensed under GNU GPL, see the LICENSE file at the top of the repository for more details. > This project is licensed under GNU GPL, see the LICENSE file at the top of the repository for more details.

View File

@ -1,4 +1,3 @@
# -*- coding:utf-8 -*
# #
# Copyright 2023 © AE UTBM # Copyright 2023 © AE UTBM
# ae@utbm.fr / ae.info@utbm.fr # ae@utbm.fr / ae.info@utbm.fr
@ -6,10 +5,10 @@
# This file is part of the website of the UTBM Student Association (AE UTBM), # This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr. # https://ae.utbm.fr.
# #
# You can find the source code of the website at https://github.com/ae-utbm/sith3 # You can find the source code of the website at https://github.com/ae-utbm/sith
# #
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3) # LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE # SEE : https://raw.githubusercontent.com/ae-utbm/sith/master/LICENSE
# OR WITHIN THE LOCAL FILE "LICENSE" # OR WITHIN THE LOCAL FILE "LICENSE"
# #
# #

View File

@ -1,4 +1,3 @@
# -*- coding:utf-8 -*
# #
# Copyright 2023 © AE UTBM # Copyright 2023 © AE UTBM
# ae@utbm.fr / ae.info@utbm.fr # ae@utbm.fr / ae.info@utbm.fr
@ -6,18 +5,26 @@
# This file is part of the website of the UTBM Student Association (AE UTBM), # This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr. # https://ae.utbm.fr.
# #
# You can find the source code of the website at https://github.com/ae-utbm/sith3 # You can find the source code of the website at https://github.com/ae-utbm/sith
# #
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3) # LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE # SEE : https://raw.githubusercontent.com/ae-utbm/sith/master/LICENSE
# OR WITHIN THE LOCAL FILE "LICENSE" # OR WITHIN THE LOCAL FILE "LICENSE"
# #
# #
from django.contrib import admin from django.contrib import admin
from accounting.models import * from accounting.models import (
AccountingType,
BankAccount,
ClubAccount,
Company,
GeneralJournal,
Label,
Operation,
SimplifiedAccountingType,
)
admin.site.register(BankAccount) admin.site.register(BankAccount)
admin.site.register(ClubAccount) admin.site.register(ClubAccount)

23
accounting/api.py Normal file
View File

@ -0,0 +1,23 @@
from typing import Annotated
from annotated_types import MinLen
from ninja_extra import ControllerBase, api_controller, paginate, route
from ninja_extra.pagination import PageNumberPaginationExtra
from ninja_extra.schemas import PaginatedResponseSchema
from accounting.models import ClubAccount, Company
from accounting.schemas import ClubAccountSchema, CompanySchema
from core.api_permissions import CanAccessLookup
@api_controller("/lookup", permissions=[CanAccessLookup])
class AccountingController(ControllerBase):
@route.get("/club-account", response=PaginatedResponseSchema[ClubAccountSchema])
@paginate(PageNumberPaginationExtra, page_size=50)
def search_club_account(self, search: Annotated[str, MinLen(1)]):
return ClubAccount.objects.filter(name__icontains=search).values()
@route.get("/company", response=PaginatedResponseSchema[CompanySchema])
@paginate(PageNumberPaginationExtra, page_size=50)
def search_company(self, search: Annotated[str, MinLen(1)]):
return Company.objects.filter(name__icontains=search).values()

View File

@ -1,10 +1,10 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import migrations, models
import django.core.validators import django.core.validators
import accounting.models
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models
import accounting.models
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@ -1,8 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
@ -101,6 +100,6 @@ class Migration(migrations.Migration):
), ),
), ),
migrations.AlterUniqueTogether( migrations.AlterUniqueTogether(
name="operation", unique_together=set([("number", "journal")]) name="operation", unique_together={("number", "journal")}
), ),
] ]

View File

@ -1,8 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import migrations, models
import phonenumber_field.modelfields import phonenumber_field.modelfields
from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@ -1,8 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
@ -46,6 +45,6 @@ class Migration(migrations.Migration):
), ),
), ),
migrations.AlterUniqueTogether( migrations.AlterUniqueTogether(
name="label", unique_together=set([("name", "club_account")]) name="label", unique_together={("name", "club_account")}
), ),
] ]

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import migrations, models from django.db import migrations, models

View File

@ -1,4 +1,3 @@
# -*- coding:utf-8 -*
# #
# Copyright 2023 © AE UTBM # Copyright 2023 © AE UTBM
# ae@utbm.fr / ae.info@utbm.fr # ae@utbm.fr / ae.info@utbm.fr
@ -6,46 +5,56 @@
# This file is part of the website of the UTBM Student Association (AE UTBM), # This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr. # https://ae.utbm.fr.
# #
# You can find the source code of the website at https://github.com/ae-utbm/sith3 # You can find the source code of the website at https://github.com/ae-utbm/sith
# #
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3) # LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE # SEE : https://raw.githubusercontent.com/ae-utbm/sith/master/LICENSE
# OR WITHIN THE LOCAL FILE "LICENSE" # OR WITHIN THE LOCAL FILE "LICENSE"
# #
# #
from django.urls import reverse from decimal import Decimal
from django.core.exceptions import ValidationError
from django.core import validators
from django.db import models
from django.conf import settings
from django.utils.translation import gettext_lazy as _
from django.template import defaultfilters
from django.conf import settings
from django.core import validators
from django.core.exceptions import ValidationError
from django.db import models
from django.template import defaultfilters
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from phonenumber_field.modelfields import PhoneNumberField from phonenumber_field.modelfields import PhoneNumberField
from decimal import Decimal
from core.models import User, SithFile
from club.models import Club from club.models import Club
from core.models import SithFile, User
class CurrencyField(models.DecimalField): class CurrencyField(models.DecimalField):
""" """Custom database field used for currency."""
This is a custom database field used for currency
"""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
kwargs["max_digits"] = 12 kwargs["max_digits"] = 12
kwargs["decimal_places"] = 2 kwargs["decimal_places"] = 2
super(CurrencyField, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def to_python(self, value): def to_python(self, value):
try: try:
return super(CurrencyField, self).to_python(value).quantize(Decimal("0.01")) return super().to_python(value).quantize(Decimal("0.01"))
except AttributeError: except AttributeError:
return None return None
if settings.TESTING:
from model_bakery import baker
baker.generators.add(
CurrencyField,
lambda: baker.random_gen.gen_decimal(max_digits=8, decimal_places=2),
)
else: # pragma: no cover
# baker is only used in tests, so we don't need coverage for this part
pass
# Accounting classes # Accounting classes
@ -62,31 +71,8 @@ class Company(models.Model):
class Meta: class Meta:
verbose_name = _("company") verbose_name = _("company")
def is_owned_by(self, user): def __str__(self):
""" return self.name
Method to see if that object can be edited by the given user
"""
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
return True
return False
def can_be_edited_by(self, user):
"""
Method to see if that object can be edited by the given user
"""
for club in user.memberships.filter(end_date=None).all():
if club and club.role == settings.SITH_CLUB_ROLES_ID["Treasurer"]:
return True
return False
def can_be_viewed_by(self, user):
"""
Method to see if that object can be viewed by the given user
"""
for club in user.memberships.filter(end_date=None).all():
if club and club.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]:
return True
return False
def get_absolute_url(self): def get_absolute_url(self):
return reverse("accounting:co_edit", kwargs={"co_id": self.id}) return reverse("accounting:co_edit", kwargs={"co_id": self.id})
@ -94,8 +80,21 @@ class Company(models.Model):
def get_display_name(self): def get_display_name(self):
return self.name return self.name
def __str__(self): def is_owned_by(self, user):
return self.name """Check if that object can be edited by the given user."""
return user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
def can_be_edited_by(self, user):
"""Check if that object can be edited by the given user."""
return user.memberships.filter(
end_date=None, club__role=settings.SITH_CLUB_ROLES_ID["Treasurer"]
).exists()
def can_be_viewed_by(self, user):
"""Check if that object can be viewed by the given user."""
return user.memberships.filter(
end_date=None, club__role_gte=settings.SITH_CLUB_ROLES_ID["Treasurer"]
).exists()
class BankAccount(models.Model): class BankAccount(models.Model):
@ -113,24 +112,20 @@ class BankAccount(models.Model):
verbose_name = _("Bank account") verbose_name = _("Bank account")
ordering = ["club", "name"] ordering = ["club", "name"]
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse("accounting:bank_details", kwargs={"b_account_id": self.id})
def is_owned_by(self, user): def is_owned_by(self, user):
""" """Check if that object can be edited by the given user."""
Method to see if that object can be edited by the given user
"""
if user.is_anonymous: if user.is_anonymous:
return False return False
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID): if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
return True return True
m = self.club.get_membership_for(user) m = self.club.get_membership_for(user)
if m is not None and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]: return m is not None and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]
return True
return False
def get_absolute_url(self):
return reverse("accounting:bank_details", kwargs={"b_account_id": self.id})
def __str__(self):
return self.name
class ClubAccount(models.Model): class ClubAccount(models.Model):
@ -152,48 +147,33 @@ class ClubAccount(models.Model):
verbose_name = _("Club account") verbose_name = _("Club account")
ordering = ["bank_account", "name"] ordering = ["bank_account", "name"]
def is_owned_by(self, user): def __str__(self):
""" return self.name
Method to see if that object can be edited by the given user
"""
if user.is_anonymous:
return False
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
return True
return False
def can_be_edited_by(self, user):
"""
Method to see if that object can be edited by the given user
"""
m = self.club.get_membership_for(user)
if m and m.role == settings.SITH_CLUB_ROLES_ID["Treasurer"]:
return True
return False
def can_be_viewed_by(self, user):
"""
Method to see if that object can be viewed by the given user
"""
m = self.club.get_membership_for(user)
if m and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]:
return True
return False
def has_open_journal(self):
for j in self.journals.all():
if not j.closed:
return True
return False
def get_open_journal(self):
return self.journals.filter(closed=False).first()
def get_absolute_url(self): def get_absolute_url(self):
return reverse("accounting:club_details", kwargs={"c_account_id": self.id}) return reverse("accounting:club_details", kwargs={"c_account_id": self.id})
def __str__(self): def is_owned_by(self, user):
return self.name """Check if that object can be edited by the given user."""
if user.is_anonymous:
return False
return user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
def can_be_edited_by(self, user):
"""Check if that object can be edited by the given user."""
m = self.club.get_membership_for(user)
return m and m.role == settings.SITH_CLUB_ROLES_ID["Treasurer"]
def can_be_viewed_by(self, user):
"""Check if that object can be viewed by the given user."""
m = self.club.get_membership_for(user)
return m and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]
def has_open_journal(self):
return self.journals.filter(closed=False).exists()
def get_open_journal(self):
return self.journals.filter(closed=False).first()
def get_display_name(self): def get_display_name(self):
return _("%(club_account)s on %(bank_account)s") % { return _("%(club_account)s on %(bank_account)s") % {
@ -203,9 +183,7 @@ class ClubAccount(models.Model):
class GeneralJournal(models.Model): class GeneralJournal(models.Model):
""" """Class storing all the operations for a period of time."""
Class storing all the operations for a period of time
"""
start_date = models.DateField(_("start date")) start_date = models.DateField(_("start date"))
end_date = models.DateField(_("end date"), null=True, blank=True, default=None) end_date = models.DateField(_("end date"), null=True, blank=True, default=None)
@ -225,36 +203,28 @@ class GeneralJournal(models.Model):
verbose_name = _("General journal") verbose_name = _("General journal")
ordering = ["-start_date"] ordering = ["-start_date"]
def is_owned_by(self, user): def __str__(self):
""" return self.name
Method to see if that object can be edited by the given user
"""
if user.is_anonymous:
return False
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
return True
if self.club_account.can_be_edited_by(user):
return True
return False
def can_be_edited_by(self, user):
"""
Method to see if that object can be edited by the given user
"""
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
return True
if self.club_account.can_be_edited_by(user):
return True
return False
def can_be_viewed_by(self, user):
return self.club_account.can_be_viewed_by(user)
def get_absolute_url(self): def get_absolute_url(self):
return reverse("accounting:journal_details", kwargs={"j_id": self.id}) return reverse("accounting:journal_details", kwargs={"j_id": self.id})
def __str__(self): def is_owned_by(self, user):
return self.name """Check if that object can be edited by the given user."""
if user.is_anonymous:
return False
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
return True
return self.club_account.can_be_edited_by(user)
def can_be_edited_by(self, user):
"""Check if that object can be edited by the given user."""
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
return True
return self.club_account.can_be_edited_by(user)
def can_be_viewed_by(self, user):
return self.club_account.can_be_viewed_by(user)
def update_amounts(self): def update_amounts(self):
self.amount = 0 self.amount = 0
@ -272,9 +242,7 @@ class GeneralJournal(models.Model):
class Operation(models.Model): class Operation(models.Model):
""" """An operation is a line in the journal, a debit or a credit."""
An operation is a line in the journal, a debit or a credit
"""
number = models.IntegerField(_("number")) number = models.IntegerField(_("number"))
journal = models.ForeignKey( journal = models.ForeignKey(
@ -357,6 +325,18 @@ class Operation(models.Model):
unique_together = ("number", "journal") unique_together = ("number", "journal")
ordering = ["-number"] ordering = ["-number"]
def __str__(self):
return f"{self.amount} € | {self.date} | {self.accounting_type} | {self.done}"
def save(self, *args, **kwargs):
if self.number is None:
self.number = self.journal.operations.count() + 1
super().save(*args, **kwargs)
self.journal.update_amounts()
def get_absolute_url(self):
return reverse("accounting:journal_details", kwargs={"j_id": self.journal.id})
def __getattribute__(self, attr): def __getattribute__(self, attr):
if attr == "target": if attr == "target":
return self.get_target() return self.get_target()
@ -364,7 +344,7 @@ class Operation(models.Model):
return object.__getattribute__(self, attr) return object.__getattribute__(self, attr)
def clean(self): def clean(self):
super(Operation, self).clean() super().clean()
if self.date is None: if self.date is None:
raise ValidationError(_("The date must be set.")) raise ValidationError(_("The date must be set."))
elif self.date < self.journal.start_date: elif self.date < self.journal.start_date:
@ -410,16 +390,8 @@ class Operation(models.Model):
tar = Company.objects.filter(id=self.target_id).first() tar = Company.objects.filter(id=self.target_id).first()
return tar return tar
def save(self):
if self.number is None:
self.number = self.journal.operations.count() + 1
super(Operation, self).save()
self.journal.update_amounts()
def is_owned_by(self, user): def is_owned_by(self, user):
""" """Check if that object can be edited by the given user."""
Method to see if that object can be edited by the given user
"""
if user.is_anonymous: if user.is_anonymous:
return False return False
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID): if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
@ -427,40 +399,22 @@ class Operation(models.Model):
if self.journal.closed: if self.journal.closed:
return False return False
m = self.journal.club_account.club.get_membership_for(user) m = self.journal.club_account.club.get_membership_for(user)
if m is not None and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]: return m is not None and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]
return True
return False
def can_be_edited_by(self, user): def can_be_edited_by(self, user):
""" """Check if that object can be edited by the given user."""
Method to see if that object can be edited by the given user
"""
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID): if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
return True return True
if self.journal.closed: if self.journal.closed:
return False return False
m = self.journal.club_account.club.get_membership_for(user) m = self.journal.club_account.club.get_membership_for(user)
if m is not None and m.role == settings.SITH_CLUB_ROLES_ID["Treasurer"]: return m is not None and m.role == settings.SITH_CLUB_ROLES_ID["Treasurer"]
return True
return False
def get_absolute_url(self):
return reverse("accounting:journal_details", kwargs={"j_id": self.journal.id})
def __str__(self):
return "%d € | %s | %s | %s" % (
self.amount,
self.date,
self.accounting_type,
self.done,
)
class AccountingType(models.Model): class AccountingType(models.Model):
""" """Accounting types.
Class describing the accounting types.
Thoses are numbers used in accounting to classify operations Those are numbers used in accounting to classify operations
""" """
code = models.CharField( code = models.CharField(
@ -487,27 +441,21 @@ class AccountingType(models.Model):
verbose_name = _("accounting type") verbose_name = _("accounting type")
ordering = ["movement_type", "code"] ordering = ["movement_type", "code"]
def is_owned_by(self, user): def __str__(self):
""" return self.code + " - " + self.get_movement_type_display() + " - " + self.label
Method to see if that object can be edited by the given user
"""
if user.is_anonymous:
return False
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
return True
return False
def get_absolute_url(self): def get_absolute_url(self):
return reverse("accounting:type_list") return reverse("accounting:type_list")
def __str__(self): def is_owned_by(self, user):
return self.code + " - " + self.get_movement_type_display() + " - " + self.label """Check if that object can be edited by the given user."""
if user.is_anonymous:
return False
return user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
class SimplifiedAccountingType(models.Model): class SimplifiedAccountingType(models.Model):
""" """Simplified version of `AccountingType`."""
Class describing the simplified accounting types.
"""
label = models.CharField(_("label"), max_length=128) label = models.CharField(_("label"), max_length=128)
accounting_type = models.ForeignKey( accounting_type = models.ForeignKey(
@ -521,6 +469,15 @@ class SimplifiedAccountingType(models.Model):
verbose_name = _("simplified type") verbose_name = _("simplified type")
ordering = ["accounting_type__movement_type", "accounting_type__code"] ordering = ["accounting_type__movement_type", "accounting_type__code"]
def __str__(self):
return (
f"{self.get_movement_type_display()} "
f"- {self.accounting_type.code} - {self.label}"
)
def get_absolute_url(self):
return reverse("accounting:simple_type_list")
@property @property
def movement_type(self): def movement_type(self):
return self.accounting_type.movement_type return self.accounting_type.movement_type
@ -528,21 +485,9 @@ class SimplifiedAccountingType(models.Model):
def get_movement_type_display(self): def get_movement_type_display(self):
return self.accounting_type.get_movement_type_display() return self.accounting_type.get_movement_type_display()
def get_absolute_url(self):
return reverse("accounting:simple_type_list")
def __str__(self):
return (
self.get_movement_type_display()
+ " - "
+ self.accounting_type.code
+ " - "
+ self.label
)
class Label(models.Model): class Label(models.Model):
"""Label allow a club to sort its operations""" """Label allow a club to sort its operations."""
name = models.CharField(_("label"), max_length=64) name = models.CharField(_("label"), max_length=64)
club_account = models.ForeignKey( club_account = models.ForeignKey(

15
accounting/schemas.py Normal file
View File

@ -0,0 +1,15 @@
from ninja import ModelSchema
from accounting.models import ClubAccount, Company
class ClubAccountSchema(ModelSchema):
class Meta:
model = ClubAccount
fields = ["id", "name"]
class CompanySchema(ModelSchema):
class Meta:
model = Company
fields = ["id", "name"]

View File

@ -0,0 +1,60 @@
import { AjaxSelect } from "#core:core/components/ajax-select-base";
import { registerComponent } from "#core:utils/web-components";
import type { TomOption } from "tom-select/dist/types/types";
import type { escape_html } from "tom-select/dist/types/utils";
import {
type ClubAccountSchema,
type CompanySchema,
accountingSearchClubAccount,
accountingSearchCompany,
} from "#openapi";
@registerComponent("club-account-ajax-select")
export class ClubAccountAjaxSelect extends AjaxSelect {
protected valueField = "id";
protected labelField = "name";
protected searchField = ["code", "name"];
protected async search(query: string): Promise<TomOption[]> {
const resp = await accountingSearchClubAccount({ query: { search: query } });
if (resp.data) {
return resp.data.results;
}
return [];
}
protected renderOption(item: ClubAccountSchema, sanitize: typeof escape_html) {
return `<div class="select-item">
<span class="select-item-text">${sanitize(item.name)}</span>
</div>`;
}
protected renderItem(item: ClubAccountSchema, sanitize: typeof escape_html) {
return `<span>${sanitize(item.name)}</span>`;
}
}
@registerComponent("company-ajax-select")
export class CompanyAjaxSelect extends AjaxSelect {
protected valueField = "id";
protected labelField = "name";
protected searchField = ["code", "name"];
protected async search(query: string): Promise<TomOption[]> {
const resp = await accountingSearchCompany({ query: { search: query } });
if (resp.data) {
return resp.data.results;
}
return [];
}
protected renderOption(item: CompanySchema, sanitize: typeof escape_html) {
return `<div class="select-item">
<span class="select-item-text">${sanitize(item.name)}</span>
</div>`;
}
protected renderItem(item: CompanySchema, sanitize: typeof escape_html) {
return `<span>${sanitize(item.name)}</span>`;
}
}

View File

@ -1,27 +1,27 @@
{% extends "core/base.jinja" %} {% extends "core/base.jinja" %}
{% block title %} {% block title %}
{% trans %}Accounting type list{% endtrans %} {% trans %}Accounting type list{% endtrans %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div id="accounting"> <div id="accounting">
<p> <p>
<a href="{{ url('accounting:bank_list') }}">{% trans %}Accounting{% endtrans %}</a> > <a href="{{ url('accounting:bank_list') }}">{% trans %}Accounting{% endtrans %}</a> >
{% trans %}Accounting types{% endtrans %} {% trans %}Accounting types{% endtrans %}
</p> </p>
<hr> <hr>
<p><a href="{{ url('accounting:type_new') }}">{% trans %}New accounting type{% endtrans %}</a></p> <p><a href="{{ url('accounting:type_new') }}">{% trans %}New accounting type{% endtrans %}</a></p>
{% if accountingtype_list %} {% if accountingtype_list %}
<h3>{% trans %}Accounting type list{% endtrans %}</h3> <h3>{% trans %}Accounting type list{% endtrans %}</h3>
<ul> <ul>
{% for a in accountingtype_list %} {% for a in accountingtype_list %}
<li><a href="{{ url('accounting:type_edit', type_id=a.id) }}">{{ a }}</a></li> <li><a href="{{ url('accounting:type_edit', type_id=a.id) }}">{{ a }}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>
{% else %} {% else %}
{% trans %}There is no types in this website.{% endtrans %} {% trans %}There is no types in this website.{% endtrans %}
{% endif %} {% endif %}
</div> </div>
{% endblock %} {% endblock %}

View File

@ -1,37 +1,37 @@
{% extends "core/base.jinja" %} {% extends "core/base.jinja" %}
{% block title %} {% block title %}
{% trans %}Bank account: {% endtrans %}{{ object.name }} {% trans %}Bank account: {% endtrans %}{{ object.name }}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div id="accounting"> <div id="accounting">
<p> <p>
<a href="{{ url('accounting:bank_list') }}">{% trans %}Accounting{% endtrans %}</a> > <a href="{{ url('accounting:bank_list') }}">{% trans %}Accounting{% endtrans %}</a> >
{{ object.name }} {{ object.name }}
</p> </p>
<hr> <hr>
<h2>{% trans %}Bank account: {% endtrans %}{{ object.name }}</h2> <h2>{% trans %}Bank account: {% endtrans %}{{ object.name }}</h2>
{% if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) and not object.club_accounts.exists() %} {% if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) and not object.club_accounts.exists() %}
<a href="{{ url('accounting:bank_delete', b_account_id=object.id) }}">{% trans %}Delete{% endtrans %}</a> <a href="{{ url('accounting:bank_delete', b_account_id=object.id) }}">{% trans %}Delete{% endtrans %}</a>
{% endif %} {% endif %}
<h4>{% trans %}Infos{% endtrans %}</h4> <h4>{% trans %}Infos{% endtrans %}</h4>
<ul> <ul>
<li><strong>{% trans %}IBAN: {% endtrans %}</strong>{{ object.iban }}</li> <li><strong>{% trans %}IBAN: {% endtrans %}</strong>{{ object.iban }}</li>
<li><strong>{% trans %}Number: {% endtrans %}</strong>{{ object.number }}</li> <li><strong>{% trans %}Number: {% endtrans %}</strong>{{ object.number }}</li>
</ul> </ul>
<p><a href="{{ url('accounting:club_new') }}?parent={{ object.id }}">{% trans %}New club account{% endtrans %}</a></p> <p><a href="{{ url('accounting:club_new') }}?parent={{ object.id }}">{% trans %}New club account{% endtrans %}</a></p>
<ul> <ul>
{% for c in object.club_accounts.all() %} {% for c in object.club_accounts.all() %}
<li><a href="{{ url('accounting:club_details', c_account_id=c.id) }}">{{ c }}</a> <li><a href="{{ url('accounting:club_details', c_account_id=c.id) }}">{{ c }}</a>
- <a href="{{ url('accounting:club_edit', c_account_id=c.id) }}">{% trans %}Edit{% endtrans %}</a> - <a href="{{ url('accounting:club_edit', c_account_id=c.id) }}">{% trans %}Edit{% endtrans %}</a>
{% if c.journals.count() == 0 %} {% if c.journals.count() == 0 %}
- <a href="{{ url('accounting:club_delete', c_account_id=c.id) }}">{% trans %}Delete{% endtrans %}</a> - <a href="{{ url('accounting:club_delete', c_account_id=c.id) }}">{% trans %}Delete{% endtrans %}</a>
{% endif %} {% endif %}
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -1,32 +1,32 @@
{% extends "core/base.jinja" %} {% extends "core/base.jinja" %}
{% block title %} {% block title %}
{% trans %}Bank account list{% endtrans %} {% trans %}Bank account list{% endtrans %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div id="accounting"> <div id="accounting">
<h4> <h4>
{% trans %}Accounting{% endtrans %} {% trans %}Accounting{% endtrans %}
</h4> </h4>
{% if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) %} {% if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) %}
<p><a href="{{ url('accounting:simple_type_list') }}">{% trans %}Manage simplified types{% endtrans %}</a></p> <p><a href="{{ url('accounting:simple_type_list') }}">{% trans %}Manage simplified types{% endtrans %}</a></p>
<p><a href="{{ url('accounting:type_list') }}">{% trans %}Manage accounting types{% endtrans %}</a></p> <p><a href="{{ url('accounting:type_list') }}">{% trans %}Manage accounting types{% endtrans %}</a></p>
<p><a href="{{ url('accounting:bank_new') }}">{% trans %}New bank account{% endtrans %}</a></p> <p><a href="{{ url('accounting:bank_new') }}">{% trans %}New bank account{% endtrans %}</a></p>
{% endif %} {% endif %}
{% if bankaccount_list %} {% if bankaccount_list %}
<h3>{% trans %}Bank account list{% endtrans %}</h3> <h3>{% trans %}Bank account list{% endtrans %}</h3>
<ul> <ul>
{% for a in object_list %} {% for a in object_list %}
<li><a href="{{ url('accounting:bank_details', b_account_id=a.id) }}">{{ a }}</a> <li><a href="{{ url('accounting:bank_details', b_account_id=a.id) }}">{{ a }}</a>
- <a href="{{ url('accounting:bank_edit', b_account_id=a.id) }}">{% trans %}Edit{% endtrans %}</a> - <a href="{{ url('accounting:bank_edit', b_account_id=a.id) }}">{% trans %}Edit{% endtrans %}</a>
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
{% else %} {% else %}
{% trans %}There is no accounts in this website.{% endtrans %} {% trans %}There is no accounts in this website.{% endtrans %}
{% endif %} {% endif %}
</div> </div>
{% endblock %} {% endblock %}

View File

@ -1,68 +1,68 @@
{% extends "core/base.jinja" %} {% extends "core/base.jinja" %}
{% block title %} {% block title %}
{% trans %}Club account:{% endtrans %} {{ object.name }} {% trans %}Club account:{% endtrans %} {{ object.name }}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div id="accounting"> <div id="accounting">
<p> <p>
<a href="{{ url('accounting:bank_list') }}">{% trans %}Accounting{% endtrans %}</a> > <a href="{{ url('accounting:bank_list') }}">{% trans %}Accounting{% endtrans %}</a> >
<a href="{{ url('accounting:bank_details', b_account_id=object.bank_account.id) }}">{{object.bank_account }}</a> > <a href="{{ url('accounting:bank_details', b_account_id=object.bank_account.id) }}">{{object.bank_account }}</a> >
{{ object }} {{ object }}
</p> </p>
<hr> <hr>
<h2>{% trans %}Club account:{% endtrans %} {{ object.name }}</h2> <h2>{% trans %}Club account:{% endtrans %} {{ object.name }}</h2>
{% if user.is_root and not object.journals.exists() %} {% if user.is_root and not object.journals.exists() %}
<a href="{{ url('accounting:club_delete', c_account_id=object.id) }}">{% trans %}Delete{% endtrans %}</a> <a href="{{ url('accounting:club_delete', c_account_id=object.id) }}">{% trans %}Delete{% endtrans %}</a>
{% endif %} {% endif %}
{% if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) %} {% if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) %}
<p><a href="{{ url('accounting:label_new') }}?parent={{ object.id }}">{% trans %}New label{% endtrans %}</a></p> <p><a href="{{ url('accounting:label_new') }}?parent={{ object.id }}">{% trans %}New label{% endtrans %}</a></p>
{% endif %} {% endif %}
<p><a href="{{ url('accounting:label_list', clubaccount_id=object.id) }}">{% trans %}Label list{% endtrans %}</a></p> <p><a href="{{ url('accounting:label_list', clubaccount_id=object.id) }}">{% trans %}Label list{% endtrans %}</a></p>
{% if not object.has_open_journal() %} {% if not object.has_open_journal() %}
<p><a href="{{ url('accounting:journal_new') }}?parent={{ object.id }}">{% trans %}New journal{% endtrans %}</a></p> <p><a href="{{ url('accounting:journal_new') }}?parent={{ object.id }}">{% trans %}New journal{% endtrans %}</a></p>
{% else %} {% else %}
<p>{% trans %}You can not create new journal while you still have one opened{% endtrans %}</p> <p>{% trans %}You can not create new journal while you still have one opened{% endtrans %}</p>
{% endif %} {% endif %}
<table> <table>
<thead> <thead>
<tr> <tr>
<td>{% trans %}Name{% endtrans %}</td> <td>{% trans %}Name{% endtrans %}</td>
<td>{% trans %}Start{% endtrans %}</td> <td>{% trans %}Start{% endtrans %}</td>
<td>{% trans %}End{% endtrans %}</td> <td>{% trans %}End{% endtrans %}</td>
<td>{% trans %}Amount{% endtrans %}</td> <td>{% trans %}Amount{% endtrans %}</td>
<td>{% trans %}Effective amount{% endtrans %}</td> <td>{% trans %}Effective amount{% endtrans %}</td>
<td>{% trans %}Closed{% endtrans %}</td> <td>{% trans %}Closed{% endtrans %}</td>
<td>{% trans %}Actions{% endtrans %}</td> <td>{% trans %}Actions{% endtrans %}</td>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for j in object.journals.all() %} {% for j in object.journals.all() %}
<tr> <tr>
<td>{{ j.name }}</td> <td>{{ j.name }}</td>
<td>{{ j.start_date }}</td> <td>{{ j.start_date }}</td>
{% if j.end_date %} {% if j.end_date %}
<td>{{ j.end_date }}</td> <td>{{ j.end_date }}</td>
{% else %} {% else %}
<td> - </td> <td> - </td>
{% endif %} {% endif %}
<td>{{ j.amount }} €</td> <td>{{ j.amount }} €</td>
<td>{{ j.effective_amount }} €</td> <td>{{ j.effective_amount }} €</td>
{% if j.closed %} {% if j.closed %}
<td>{% trans %}Yes{% endtrans %}</td> <td>{% trans %}Yes{% endtrans %}</td>
{% else %} {% else %}
<td>{% trans %}No{% endtrans %}</td> <td>{% trans %}No{% endtrans %}</td>
{% endif %} {% endif %}
<td> <a href="{{ url('accounting:journal_details', j_id=j.id) }}">{% trans %}View{% endtrans %}</a> <td> <a href="{{ url('accounting:journal_details', j_id=j.id) }}">{% trans %}View{% endtrans %}</a>
<a href="{{ url('accounting:journal_edit', j_id=j.id) }}">{% trans %}Edit{% endtrans %}</a> <a href="{{ url('accounting:journal_edit', j_id=j.id) }}">{% trans %}Edit{% endtrans %}</a>
{% if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) and j.operations.count() == 0 %} {% if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) and j.operations.count() == 0 %}
<a href="{{ url('accounting:journal_delete', j_id=j.id) }}">{% trans %}Delete{% endtrans %}</a> <a href="{{ url('accounting:journal_delete', j_id=j.id) }}">{% trans %}Delete{% endtrans %}</a>
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -1,30 +1,30 @@
{% extends "core/base.jinja" %} {% extends "core/base.jinja" %}
{% block title %} {% block title %}
{% trans %}Company list{% endtrans %} {% trans %}Company list{% endtrans %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div id="accounting"> <div id="accounting">
{% if user.is_root {% if user.is_root
or user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) or user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
%} %}
<p><a href="{{ url('accounting:co_new') }}">{% trans %}Create new company{% endtrans %}</a></p> <p><a href="{{ url('accounting:co_new') }}">{% trans %}Create new company{% endtrans %}</a></p>
{% endif %} {% endif %}
<br/> <br/>
<table> <table>
<thead> <thead>
<tr> <tr>
<td>{% trans %}Companies{% endtrans %}</td> <td>{% trans %}Companies{% endtrans %}</td>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for o in object_list %} {% for o in object_list %}
<tr> <tr>
<td><a href="{{ url('accounting:co_edit', co_id=o.id) }}">{{ o.get_display_name() }}</a></td> <td><a href="{{ url('accounting:co_edit', co_id=o.id) }}">{{ o.get_display_name() }}</a></td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -1,103 +1,103 @@
{% extends "core/base.jinja" %} {% extends "core/base.jinja" %}
{% block title %} {% block title %}
{% trans %}General journal:{% endtrans %} {{ object.name }} {% trans %}General journal:{% endtrans %} {{ object.name }}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div id="accounting"> <div id="accounting">
<p> <p>
<a href="{{ url('accounting:bank_list') }}">{% trans %}Accounting{% endtrans %}</a> > <a href="{{ url('accounting:bank_list') }}">{% trans %}Accounting{% endtrans %}</a> >
<a href="{{ url('accounting:bank_details', b_account_id=object.club_account.bank_account.id) }}">{{object.club_account.bank_account }}</a> > <a href="{{ url('accounting:bank_details', b_account_id=object.club_account.bank_account.id) }}">{{object.club_account.bank_account }}</a> >
<a href="{{ url('accounting:club_details', c_account_id=object.club_account.id) }}">{{ object.club_account }}</a> > <a href="{{ url('accounting:club_details', c_account_id=object.club_account.id) }}">{{ object.club_account }}</a> >
{{ object.name }} {{ object.name }}
</p> </p>
<hr> <hr>
<h2>{% trans %}General journal:{% endtrans %} {{ object.name }}</h2> <h2>{% trans %}General journal:{% endtrans %} {{ object.name }}</h2>
<p><a href="{{ url('accounting:label_new') }}?parent={{ object.club_account.id }}">{% trans %}New label{% endtrans %}</a></p> <p><a href="{{ url('accounting:label_new') }}?parent={{ object.club_account.id }}">{% trans %}New label{% endtrans %}</a></p>
<p><a href="{{ url('accounting:label_list', clubaccount_id=object.club_account.id) }}">{% trans %}Label list{% endtrans %}</a></p> <p><a href="{{ url('accounting:label_list', clubaccount_id=object.club_account.id) }}">{% trans %}Label list{% endtrans %}</a></p>
<p><a href="{{ url('accounting:co_list') }}">{% trans %}Company list{% endtrans %}</a></p> <p><a href="{{ url('accounting:co_list') }}">{% trans %}Company list{% endtrans %}</a></p>
<p><strong>{% trans %}Amount: {% endtrans %}</strong>{{ object.amount }} € - <p><strong>{% trans %}Amount: {% endtrans %}</strong>{{ object.amount }} € -
<strong>{% trans %}Effective amount: {% endtrans %}</strong>{{ object.effective_amount }} €</p> <strong>{% trans %}Effective amount: {% endtrans %}</strong>{{ object.effective_amount }} €</p>
{% if object.closed %} {% if object.closed %}
<p>{% trans %}Journal is closed, you can not create operation{% endtrans %}</p> <p>{% trans %}Journal is closed, you can not create operation{% endtrans %}</p>
{% else %} {% else %}
<p><a href="{{ url('accounting:op_new', j_id=object.id) }}">{% trans %}New operation{% endtrans %}</a></p> <p><a href="{{ url('accounting:op_new', j_id=object.id) }}">{% trans %}New operation{% endtrans %}</a></p>
</br> </br>
{% endif %} {% endif %}
<div class="journal-table"> <div class="journal-table">
<table> <table>
<thead> <thead>
<tr>
<td>{% trans %}Nb{% endtrans %}</td>
<td>{% trans %}Date{% endtrans %}</td>
<td>{% trans %}Label{% endtrans %}</td>
<td>{% trans %}Amount{% endtrans %}</td>
<td>{% trans %}Payment mode{% endtrans %}</td>
<td>{% trans %}Target{% endtrans %}</td>
<td>{% trans %}Code{% endtrans %}</td>
<td>{% trans %}Nature{% endtrans %}</td>
<td>{% trans %}Done{% endtrans %}</td>
<td>{% trans %}Comment{% endtrans %}</td>
<td>{% trans %}File{% endtrans %}</td>
<td>{% trans %}Actions{% endtrans %}</td>
<td>{% trans %}PDF{% endtrans %}</td>
</tr>
</thead>
<tbody>
{% for o in object.operations.all() %}
<tr> <tr>
<td>{% trans %}Nb{% endtrans %}</td> <td>{{ o.number }}</td>
<td>{% trans %}Date{% endtrans %}</td> <td>{{ o.date }}</td>
<td>{% trans %}Label{% endtrans %}</td> <td>{{ o.label or "" }}</td>
<td>{% trans %}Amount{% endtrans %}</td> {% if o.accounting_type.movement_type == "DEBIT" %}
<td>{% trans %}Payment mode{% endtrans %}</td> <td class="neg-amount">&nbsp;{{ o.amount }}&nbsp;€</td>
<td>{% trans %}Target{% endtrans %}</td> {% else %}
<td>{% trans %}Code{% endtrans %}</td> <td class="pos-amount">&nbsp;{{ o.amount }}&nbsp;€</td>
<td>{% trans %}Nature{% endtrans %}</td> {% endif %}
<td>{% trans %}Done{% endtrans %}</td> <td>{{ o.get_mode_display() }}</td>
<td>{% trans %}Comment{% endtrans %}</td> {% if o.target_type == "OTHER" %}
<td>{% trans %}File{% endtrans %}</td>
<td>{% trans %}Actions{% endtrans %}</td>
<td>{% trans %}PDF{% endtrans %}</td>
</tr>
</thead>
<tbody>
{% for o in object.operations.all() %}
<tr>
<td>{{ o.number }}</td>
<td>{{ o.date }}</td>
<td>{{ o.label or "" }}</td>
{% if o.accounting_type.movement_type == "DEBIT" %}
<td class="neg-amount">&nbsp;{{ o.amount }}&nbsp;€</td>
{% else %}
<td class="pos-amount">&nbsp;{{ o.amount }}&nbsp;€</td>
{% endif %}
<td>{{ o.get_mode_display() }}</td>
{% if o.target_type == "OTHER" %}
<td>{{ o.target_label }}</td> <td>{{ o.target_label }}</td>
{% else %} {% else %}
<td><a href="{{ o.target.get_absolute_url() }}">{{ o.target.get_display_name() }}</a></td> <td><a href="{{ o.target.get_absolute_url() }}">{{ o.target.get_display_name() }}</a></td>
{% endif %} {% endif %}
<td>{{ o.accounting_type.code }}</td> <td>{{ o.accounting_type.code }}</td>
<td>{{ o.accounting_type.label }}</td> <td>{{ o.accounting_type.label }}</td>
{% if o.done %} {% if o.done %}
<td>{% trans %}Yes{% endtrans %}</td> <td>{% trans %}Yes{% endtrans %}</td>
{% else %} {% else %}
<td>{% trans %}No{% endtrans %}</td> <td>{% trans %}No{% endtrans %}</td>
{% endif %} {% endif %}
<td>{{ o.remark }} <td>{{ o.remark }}
{% if not o.linked_operation and o.target_type == "ACCOUNT" and not o.target.has_open_journal() %} {% if not o.linked_operation and o.target_type == "ACCOUNT" and not o.target.has_open_journal() %}
<p><strong> <p><strong>
{% trans %}Warning: this operation has no linked operation because the targeted club account has no opened journal.{% endtrans %} {% trans %}Warning: this operation has no linked operation because the targeted club account has no opened journal.{% endtrans %}
</strong></p> </strong></p>
<p><strong> <p><strong>
{% trans url=o.target.get_absolute_url() %}Open a journal in <a href="{{ url }}">this club account</a>, then save this operation again to make the linked operation.{% endtrans %} {% trans url=o.target.get_absolute_url() %}Open a journal in <a href="{{ url }}">this club account</a>, then save this operation again to make the linked operation.{% endtrans %}
</strong></p> </strong></p>
{% endif %} {% endif %}
</td> </td>
{% if o.invoice %} {% if o.invoice %}
<td><a href="{{ url('core:download', file_id=o.invoice.id) }}">{{ o.invoice.name }}</a></td> <td><a href="{{ url('core:download', file_id=o.invoice.id) }}">{{ o.invoice.name }}</a></td>
{% else %} {% else %}
<td>-</td> <td>-</td>
{% endif %}
<td>
{%
if o.journal.club_account.bank_account.name not in ["AE TI", "TI"]
or user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
%}
{% if not o.journal.closed %}
<a href="{{ url('accounting:op_edit', op_id=o.id) }}">{% trans %}Edit{% endtrans %}</a>
{% endif %} {% endif %}
<td> {% endif %}
{% </td>
if o.journal.club_account.bank_account.name not in ["AE TI", "TI"] <td><a href="{{ url('accounting:op_pdf', op_id=o.id) }}">{% trans %}Generate{% endtrans %}</a></td>
or user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) </tr>
%} {% endfor %}
{% if not o.journal.closed %} </tbody>
<a href="{{ url('accounting:op_edit', op_id=o.id) }}">{% trans %}Edit{% endtrans %}</a> </table>
{% endif %} </div>
{% endif %} </div>
</td>
<td><a href="{{ url('accounting:op_pdf', op_id=o.id) }}">{% trans %}Generate{% endtrans %}</a></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %} {% endblock %}

View File

@ -1,33 +1,33 @@
{% extends "core/base.jinja" %} {% extends "core/base.jinja" %}
{% block title %} {% block title %}
{% trans %}General journal:{% endtrans %} {{ object.name }} {% trans %}General journal:{% endtrans %} {{ object.name }}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div id="accounting"> <div id="accounting">
<h3>{% trans %}Accounting statement: {% endtrans %} {{ object.name }}</h3> <h3>{% trans %}Accounting statement: {% endtrans %} {{ object.name }}</h3>
<table> <table>
<thead> <thead>
<tr> <tr>
<td>{% trans %}Operation type{% endtrans %}</td> <td>{% trans %}Operation type{% endtrans %}</td>
<td>{% trans %}Sum{% endtrans %}</td> <td>{% trans %}Sum{% endtrans %}</td>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for k,v in statement.items() %} {% for k,v in statement.items() %}
<tr> <tr>
<td>{{ k }}</td> <td>{{ k }}</td>
<td>{{ "%.2f" % v }}</td> <td>{{ "%.2f" % v }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
<p><strong>{% trans %}Amount: {% endtrans %}</strong>{{ "%.2f" % object.amount }} €</p> <p><strong>{% trans %}Amount: {% endtrans %}</strong>{{ "%.2f" % object.amount }} €</p>
<p><strong>{% trans %}Effective amount: {% endtrans %}</strong>{{ "%.2f" %object.effective_amount }} €</p> <p><strong>{% trans %}Effective amount: {% endtrans %}</strong>{{ "%.2f" %object.effective_amount }} €</p>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -1,57 +1,57 @@
{% extends "core/base.jinja" %} {% extends "core/base.jinja" %}
{% block title %} {% block title %}
{% trans %}General journal:{% endtrans %} {{ object.name }} {% trans %}General journal:{% endtrans %} {{ object.name }}
{% endblock %} {% endblock %}
{% macro display_tables(dict) %} {% macro display_tables(dict) %}
<div id="accounting"> <div id="accounting">
<h6>{% trans %}Credit{% endtrans %}</h6> <h6>{% trans %}Credit{% endtrans %}</h6>
<table> <table>
<thead> <thead>
<tr> <tr>
<td>{% trans %}Nature of operation{% endtrans %}</td> <td>{% trans %}Nature of operation{% endtrans %}</td>
<td>{% trans %}Sum{% endtrans %}</td> <td>{% trans %}Sum{% endtrans %}</td>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for k,v in dict['CREDIT'].items() %} {% for k,v in dict['CREDIT'].items() %}
<tr> <tr>
<td>{{ k }}</td> <td>{{ k }}</td>
<td>{{ "%.2f" % v }}</td> <td>{{ "%.2f" % v }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
{% trans %}Total: {% endtrans %}{{ "%.2f" % dict['CREDIT_sum'] }} {% trans %}Total: {% endtrans %}{{ "%.2f" % dict['CREDIT_sum'] }}
<h6>{% trans %}Debit{% endtrans %}</h6> <h6>{% trans %}Debit{% endtrans %}</h6>
<table> <table>
<thead> <thead>
<tr> <tr>
<td>{% trans %}Nature of operation{% endtrans %}</td> <td>{% trans %}Nature of operation{% endtrans %}</td>
<td>{% trans %}Sum{% endtrans %}</td> <td>{% trans %}Sum{% endtrans %}</td>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for k,v in dict['DEBIT'].items() %} {% for k,v in dict['DEBIT'].items() %}
<tr> <tr>
<td>{{ k }}</td> <td>{{ k }}</td>
<td>{{ "%.2f" % v }}</td> <td>{{ "%.2f" % v }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
{% trans %}Total: {% endtrans %}{{ "%.2f" % dict['DEBIT_sum'] }} {% trans %}Total: {% endtrans %}{{ "%.2f" % dict['DEBIT_sum'] }}
{% endmacro %} {% endmacro %}
{% block content %} {% block content %}
<h3>{% trans %}Statement by nature: {% endtrans %} {{ object.name }}</h3> <h3>{% trans %}Statement by nature: {% endtrans %} {{ object.name }}</h3>
{% for k,v in statement.items() %} {% for k,v in statement.items() %}
<h4 style="background: lightblue; padding: 4px;">{{ k }} : {{ "%.2f" % (v['CREDIT_sum'] - v['DEBIT_sum']) }}</h4> <h4 style="background: lightblue; padding: 4px;">{{ k }} : {{ "%.2f" % (v['CREDIT_sum'] - v['DEBIT_sum']) }}</h4>
{{ display_tables(v) }} {{ display_tables(v) }}
<hr> <hr>
{% endfor %} {% endfor %}
</div> </div>
{% endblock %} {% endblock %}

View File

@ -1,37 +1,37 @@
{% extends "core/base.jinja" %} {% extends "core/base.jinja" %}
{% block title %} {% block title %}
{% trans %}General journal:{% endtrans %} {{ object.name }} {% trans %}General journal:{% endtrans %} {{ object.name }}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div id="accounting"> <div id="accounting">
<h3>{% trans %}Statement by person: {% endtrans %} {{ object.name }}</h3> <h3>{% trans %}Statement by person: {% endtrans %} {{ object.name }}</h3>
<h4>{% trans %}Credit{% endtrans %}</h4> <h4>{% trans %}Credit{% endtrans %}</h4>
<table> <table>
<thead> <thead>
<tr> <tr>
<td>{% trans %}Target of the operation{% endtrans %}</td> <td>{% trans %}Target of the operation{% endtrans %}</td>
<td>{% trans %}Sum{% endtrans %}</td> <td>{% trans %}Sum{% endtrans %}</td>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for key in credit_statement.keys() %} {% for key in credit_statement.keys() %}
<tr> <tr>
{% if key.target_type == "OTHER" %} {% if key.target_type == "OTHER" %}
<td>{{ o.target_label }}</td> <td>{{ o.target_label }}</td>
{% elif key %} {% elif key %}
<td><a href="{{ key.get_absolute_url() }}">{{ key.get_display_name() }}</a></td> <td><a href="{{ key.get_absolute_url() }}">{{ key.get_display_name() }}</a></td>
{% else %} {% else %}
<td></td> <td></td>
{% endif %} {% endif %}
<td>{{ "%.2f" % credit_statement[key] }}</td> <td>{{ "%.2f" % credit_statement[key] }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
@ -40,29 +40,29 @@
<h4>{% trans %}Debit{% endtrans %}</h4> <h4>{% trans %}Debit{% endtrans %}</h4>
<table> <table>
<thead> <thead>
<tr> <tr>
<td>{% trans %}Target of the operation{% endtrans %}</td> <td>{% trans %}Target of the operation{% endtrans %}</td>
<td>{% trans %}Sum{% endtrans %}</td> <td>{% trans %}Sum{% endtrans %}</td>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for key in debit_statement.keys() %} {% for key in debit_statement.keys() %}
<tr> <tr>
{% if key.target_type == "OTHER" %} {% if key.target_type == "OTHER" %}
<td>{{ o.target_label }}</td> <td>{{ o.target_label }}</td>
{% elif key %} {% elif key %}
<td><a href="{{ key.get_absolute_url() }}">{{ key.get_display_name() }}</a></td> <td><a href="{{ key.get_absolute_url() }}">{{ key.get_display_name() }}</a></td>
{% else %} {% else %}
<td></td> <td></td>
{% endif %} {% endif %}
<td>{{ "%.2f" % debit_statement[key] }}</td> <td>{{ "%.2f" % debit_statement[key] }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
<p>Total : {{ "%.2f" % total_debit }}</p> <p>Total : {{ "%.2f" % total_debit }}</p>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -1,36 +1,36 @@
{% extends "core/base.jinja" %} {% extends "core/base.jinja" %}
{% block title %} {% block title %}
{% trans %}Label list{% endtrans %} {% trans %}Label list{% endtrans %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div id="accounting"> <div id="accounting">
<p> <p>
<a href="{{ url('accounting:bank_list') }}">{% trans %}Accounting{% endtrans %}</a> > <a href="{{ url('accounting:bank_list') }}">{% trans %}Accounting{% endtrans %}</a> >
<a href="{{ url('accounting:bank_details', b_account_id=object.bank_account.id) }}">{{object.bank_account }}</a> > <a href="{{ url('accounting:bank_details', b_account_id=object.bank_account.id) }}">{{object.bank_account }}</a> >
<a href="{{ url('accounting:club_details', c_account_id=object.id) }}">{{ object }}</a> <a href="{{ url('accounting:club_details', c_account_id=object.id) }}">{{ object }}</a>
</p> </p>
<hr> <hr>
<p><a href="{{ url('accounting:club_details', c_account_id=object.id) }}">{% trans %}Back to club account{% endtrans %}</a></p> <p><a href="{{ url('accounting:club_details', c_account_id=object.id) }}">{% trans %}Back to club account{% endtrans %}</a></p>
{% if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) %} {% if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) %}
<p><a href="{{ url('accounting:label_new') }}?parent={{ object.id }}">{% trans %}New label{% endtrans %}</a></p> <p><a href="{{ url('accounting:label_new') }}?parent={{ object.id }}">{% trans %}New label{% endtrans %}</a></p>
{% endif %} {% endif %}
{% if object.labels.all() %} {% if object.labels.all() %}
<h3>{% trans %}Label list{% endtrans %}</h3> <h3>{% trans %}Label list{% endtrans %}</h3>
<ul> <ul>
{% for l in object.labels.all() %} {% for l in object.labels.all() %}
<li><a href="{{ url('accounting:label_edit', label_id=l.id) }}">{{ l }}</a> <li><a href="{{ url('accounting:label_edit', label_id=l.id) }}">{{ l }}</a>
{% if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) %} {% if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) %}
- -
<a href="{{ url('accounting:label_delete', label_id=l.id) }}">{% trans %}Delete{% endtrans %}</a> <a href="{{ url('accounting:label_delete', label_id=l.id) }}">{% trans %}Delete{% endtrans %}</a>
{% endif %} {% endif %}
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
{% else %} {% else %}
{% trans %}There is no label in this club account.{% endtrans %} {% trans %}There is no label in this club account.{% endtrans %}
{% endif %} {% endif %}
</div> </div>
{% endblock %} {% endblock %}

View File

@ -1,123 +1,123 @@
{% extends "core/base.jinja" %} {% extends "core/base.jinja" %}
{% block title %} {% block title %}
{% trans %}Edit operation{% endtrans %} {% trans %}Edit operation{% endtrans %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div id="accounting"> <div id="accounting">
<p> <p>
<a href="{{ url('accounting:bank_list') }}">{% trans %}Accounting{% endtrans %}</a> > <a href="{{ url('accounting:bank_list') }}">{% trans %}Accounting{% endtrans %}</a> >
<a href="{{ url('accounting:bank_details', b_account_id=object.club_account.bank_account.id) }}">{{object.club_account.bank_account }}</a> > <a href="{{ url('accounting:bank_details', b_account_id=object.club_account.bank_account.id) }}">{{object.club_account.bank_account }}</a> >
<a href="{{ url('accounting:club_details', c_account_id=object.club_account.id) }}">{{ object.club_account }}</a> > <a href="{{ url('accounting:club_details', c_account_id=object.club_account.id) }}">{{ object.club_account }}</a> >
<a href="{{ url('accounting:journal_details', j_id=object.id) }}">{{ object.name }}</a> > <a href="{{ url('accounting:journal_details', j_id=object.id) }}">{{ object.name }}</a> >
{% trans %}Edit operation{% endtrans %} {% trans %}Edit operation{% endtrans %}
</p> </p>
<hr> <hr>
<h2>{% trans %}Edit operation{% endtrans %}</h2> <h2>{% trans %}Edit operation{% endtrans %}</h2>
<form action="" method="post"> <form action="" method="post">
{% csrf_token %} {% csrf_token %}
{{ form.non_field_errors() }} {{ form.non_field_errors() }}
{{ form.journal }} {{ form.journal }}
{{ form.target_id }} {{ form.target_id }}
<p>{{ form.amount.errors }}<label for="{{ form.amount.name }}">{{ form.amount.label }}</label> {{ form.amount }}</p> <p>{{ form.amount.errors }}<label for="{{ form.amount.name }}">{{ form.amount.label }}</label> {{ form.amount }}</p>
<p>{{ form.remark.errors }}<label for="{{ form.remark.name }}">{{ form.remark.label }}</label> {{ form.remark }}</p> <p>{{ form.remark.errors }}<label for="{{ form.remark.name }}">{{ form.remark.label }}</label> {{ form.remark }}</p>
<br /> <br />
<strong>{% trans %}Warning: if you select <em>Account</em>, the opposite operation will be created in the target account. If you don't want that, select <em>Club</em> instead of <em>Account</em>.{% endtrans %}</strong> <strong>{% trans %}Warning: if you select <em>Account</em>, the opposite operation will be created in the target account. If you don't want that, select <em>Club</em> instead of <em>Account</em>.{% endtrans %}</strong>
<p>{{ form.target_type.errors }}<label for="{{ form.target_type.name }}">{{ form.target_type.label }}</label> {{ form.target_type }}</p> <p>{{ form.target_type.errors }}<label for="{{ form.target_type.name }}">{{ form.target_type.label }}</label> {{ form.target_type }}</p>
{{ form.user }} {{ form.user }}
{{ form.club }} {{ form.club }}
{{ form.club_account }} {{ form.club_account }}
{{ form.company }} {{ form.company }}
{{ form.target_label }} {{ form.target_label }}
<span id="id_need_link_full"><label>{{ form.need_link.label }}</label> {{ form.need_link }}</span> <span id="id_need_link_full"><label>{{ form.need_link.label }}</label> {{ form.need_link }}</span>
<p>{{ form.date.errors }}<label for="{{ form.date.name }}">{{ form.date.label }}</label> {{ form.date }}</p> <p>{{ form.date.errors }}<label for="{{ form.date.name }}">{{ form.date.label }}</label> {{ form.date }}</p>
<p>{{ form.mode.errors }}<label for="{{ form.mode.name }}">{{ form.mode.label }}</label> {{ form.mode }}</p> <p>{{ form.mode.errors }}<label for="{{ form.mode.name }}">{{ form.mode.label }}</label> {{ form.mode }}</p>
<p>{{ form.cheque_number.errors }}<label for="{{ form.cheque_number.name }}">{{ form.cheque_number.label }}</label> {{ <p>{{ form.cheque_number.errors }}<label for="{{ form.cheque_number.name }}">{{ form.cheque_number.label }}</label> {{
form.cheque_number }}</p> form.cheque_number }}</p>
<p>{{ form.invoice.errors }}<label for="{{ form.invoice.name }}">{{ form.invoice.label }}</label> {{ form.invoice }}</p> <p>{{ form.invoice.errors }}<label for="{{ form.invoice.name }}">{{ form.invoice.label }}</label> {{ form.invoice }}</p>
<p>{{ form.simpleaccounting_type.errors }}<label for="{{ form.simpleaccounting_type.name }}">{{ <p>{{ form.simpleaccounting_type.errors }}<label for="{{ form.simpleaccounting_type.name }}">{{
form.simpleaccounting_type.label }}</label> {{ form.simpleaccounting_type }}</p> form.simpleaccounting_type.label }}</label> {{ form.simpleaccounting_type }}</p>
<p>{{ form.accounting_type.errors }}<label for="{{ form.accounting_type.name }}">{{ form.accounting_type.label }}</label> {{ <p>{{ form.accounting_type.errors }}<label for="{{ form.accounting_type.name }}">{{ form.accounting_type.label }}</label> {{
form.accounting_type }}</p> form.accounting_type }}</p>
<p>{{ form.label.errors }}<label for="{{ form.label.name }}">{{ form.label.label }}</label> {{ form.label }}</p> <p>{{ form.label.errors }}<label for="{{ form.label.name }}">{{ form.label.label }}</label> {{ form.label }}</p>
<p>{{ form.done.errors }}<label for="{{ form.done.name }}">{{ form.done.label }}</label> {{ form.done }}</p> <p>{{ form.done.errors }}<label for="{{ form.done.name }}">{{ form.done.label }}</label> {{ form.done }}</p>
{% if form.instance.linked_operation %} {% if form.instance.linked_operation %}
{% set obj = form.instance.linked_operation %} {% set obj = form.instance.linked_operation %}
<p><strong>{% trans %}Linked operation:{% endtrans %}</strong><br> <p><strong>{% trans %}Linked operation:{% endtrans %}</strong><br>
<a href="{{ url('accounting:bank_details', b_account_id=obj.journal.club_account.bank_account.id) }}"> <a href="{{ url('accounting:bank_details', b_account_id=obj.journal.club_account.bank_account.id) }}">
{{obj.journal.club_account.bank_account }}</a> > {{obj.journal.club_account.bank_account }}</a> >
<a href="{{ url('accounting:club_details', c_account_id=obj.journal.club_account.id) }}">{{ obj.journal.club_account }}</a> > <a href="{{ url('accounting:club_details', c_account_id=obj.journal.club_account.id) }}">{{ obj.journal.club_account }}</a> >
<a href="{{ url('accounting:journal_details', j_id=obj.journal.id) }}">{{ obj.journal }}</a> > <a href="{{ url('accounting:journal_details', j_id=obj.journal.id) }}">{{ obj.journal }}</a> >
{{ obj.number }} {{ obj.number }}
</p> </p>
{% endif %} {% endif %}
<p><input type="submit" value="{% trans %}Save{% endtrans %}" /></p> <p><input type="submit" value="{% trans %}Save{% endtrans %}" /></p>
</form> </form>
{% endblock %} {% endblock %}
{% block script %} {% block script %}
{{ super() }} {{ super() }}
<script> <script>
$( function() { $( function() {
var target_type = $('#id_target_type'); var target_type = $('#id_target_type');
var user = $('#id_user_wrapper'); var user = $('user-ajax-select');
var club = $('#id_club_wrapper'); var club = $('club-ajax-select');
var club_account = $('#id_club_account_wrapper'); var club_account = $('club-account-ajax-select');
var company = $('#id_company_wrapper'); var company = $('company-ajax-select');
var other = $('#id_target_label'); var other = $('#id_target_label');
var need_link = $('#id_need_link_full'); var need_link = $('#id_need_link_full');
function update_targets () { function update_targets () {
if (target_type.val() == "USER") { if (target_type.val() == "USER") {
console.log(user); console.log(user);
user.show(); user.show();
club.hide(); club.hide();
club_account.hide(); club_account.hide();
company.hide(); company.hide();
other.hide(); other.hide();
need_link.hide(); need_link.hide();
} else if (target_type.val() == "ACCOUNT") { } else if (target_type.val() == "ACCOUNT") {
club_account.show(); club_account.show();
need_link.show(); need_link.show();
user.hide(); user.hide();
club.hide(); club.hide();
company.hide(); company.hide();
other.hide(); other.hide();
} else if (target_type.val() == "CLUB") { } else if (target_type.val() == "CLUB") {
club.show(); club.show();
user.hide(); user.hide();
club_account.hide(); club_account.hide();
company.hide(); company.hide();
other.hide(); other.hide();
need_link.hide(); need_link.hide();
} else if (target_type.val() == "COMPANY") { } else if (target_type.val() == "COMPANY") {
company.show(); company.show();
user.hide(); user.hide();
club_account.hide(); club_account.hide();
club.hide(); club.hide();
other.hide(); other.hide();
need_link.hide(); need_link.hide();
} else if (target_type.val() == "OTHER") { } else if (target_type.val() == "OTHER") {
other.show(); other.show();
user.hide(); user.hide();
club.hide(); club.hide();
club_account.hide(); club_account.hide();
company.hide(); company.hide();
need_link.hide(); need_link.hide();
} else { } else {
company.hide(); company.hide();
user.hide(); user.hide();
club_account.hide(); club_account.hide();
club.hide(); club.hide();
other.hide(); other.hide();
need_link.hide(); need_link.hide();
} }
} }
update_targets(); update_targets();
target_type.change(update_targets); target_type.change(update_targets);
} ); } );
</script> </script>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -1,16 +1,16 @@
{% extends "core/base.jinja" %} {% extends "core/base.jinja" %}
{% block title %} {% block title %}
{% trans %}Refound account{% endtrans %} {% trans %}Refound account{% endtrans %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div id="accounting"> <div id="accounting">
<h3>{% trans %}Refound account{% endtrans %}</h3> <h3>{% trans %}Refound account{% endtrans %}</h3>
<form action="" method="post"> <form action="" method="post">
{% csrf_token %} {% csrf_token %}
{{ form.as_p() }} {{ form.as_p() }}
<p><input type="submit" value="{% trans %}Refound{% endtrans %}" /></p> <p><input type="submit" value="{% trans %}Refound{% endtrans %}" /></p>
</form> </form>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -1,27 +1,27 @@
{% extends "core/base.jinja" %} {% extends "core/base.jinja" %}
{% block title %} {% block title %}
{% trans %}Simplified type list{% endtrans %} {% trans %}Simplified type list{% endtrans %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div id="accounting"> <div id="accounting">
<p> <p>
<a href="{{ url('accounting:bank_list') }}">{% trans %}Accounting{% endtrans %}</a> > <a href="{{ url('accounting:bank_list') }}">{% trans %}Accounting{% endtrans %}</a> >
{% trans %}Simplified types{% endtrans %} {% trans %}Simplified types{% endtrans %}
</p> </p>
<hr> <hr>
<p><a href="{{ url('accounting:simple_type_new') }}">{% trans %}New simplified type{% endtrans %}</a></p> <p><a href="{{ url('accounting:simple_type_new') }}">{% trans %}New simplified type{% endtrans %}</a></p>
{% if simplifiedaccountingtype_list %} {% if simplifiedaccountingtype_list %}
<h3>{% trans %}Simplified type list{% endtrans %}</h3> <h3>{% trans %}Simplified type list{% endtrans %}</h3>
<ul> <ul>
{% for a in simplifiedaccountingtype_list %} {% for a in simplifiedaccountingtype_list %}
<li><a href="{{ url('accounting:simple_type_edit', type_id=a.id) }}">{{ a }}</a></li> <li><a href="{{ url('accounting:simple_type_edit', type_id=a.id) }}">{{ a }}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>
{% else %} {% else %}
{% trans %}There is no types in this website.{% endtrans %} {% trans %}There is no types in this website.{% endtrans %}
{% endif %} {% endif %}
</div> </div>
{% endblock %} {% endblock %}

View File

@ -1,4 +1,3 @@
# -*- coding:utf-8 -*
# #
# Copyright 2023 © AE UTBM # Copyright 2023 © AE UTBM
# ae@utbm.fr / ae.info@utbm.fr # ae@utbm.fr / ae.info@utbm.fr
@ -6,98 +5,93 @@
# This file is part of the website of the UTBM Student Association (AE UTBM), # This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr. # https://ae.utbm.fr.
# #
# You can find the source code of the website at https://github.com/ae-utbm/sith3 # You can find the source code of the website at https://github.com/ae-utbm/sith
# #
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3) # LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE # SEE : https://raw.githubusercontent.com/ae-utbm/sith/master/LICENSE
# OR WITHIN THE LOCAL FILE "LICENSE" # OR WITHIN THE LOCAL FILE "LICENSE"
# #
# #
from django.test import TestCase
from django.urls import reverse
from django.core.management import call_command
from datetime import date, timedelta from datetime import date, timedelta
from core.models import User from django.test import TestCase
from django.urls import reverse
from accounting.models import ( from accounting.models import (
GeneralJournal,
Operation,
Label,
AccountingType, AccountingType,
GeneralJournal,
Label,
Operation,
SimplifiedAccountingType, SimplifiedAccountingType,
) )
from core.models import User
class RefoundAccountTest(TestCase): class TestRefoundAccount(TestCase):
def setUp(self): @classmethod
self.skia = User.objects.filter(username="skia").first() def setUpTestData(cls):
cls.skia = User.objects.get(username="skia")
# reffil skia's account # reffil skia's account
self.skia.customer.amount = 800 cls.skia.customer.amount = 800
self.skia.customer.save() cls.skia.customer.save()
cls.refound_account_url = reverse("accounting:refound_account")
def test_permission_denied(self): def test_permission_denied(self):
self.client.login(username="guy", password="plop") self.client.force_login(User.objects.get(username="guy"))
response_post = self.client.post( response_post = self.client.post(
reverse("accounting:refound_account"), {"user": self.skia.id} self.refound_account_url, {"user": self.skia.id}
) )
response_get = self.client.get(reverse("accounting:refound_account")) response_get = self.client.get(self.refound_account_url)
self.assertTrue(response_get.status_code == 403) assert response_get.status_code == 403
self.assertTrue(response_post.status_code == 403) assert response_post.status_code == 403
def test_root_granteed(self): def test_root_granteed(self):
self.client.login(username="root", password="plop") self.client.force_login(User.objects.get(username="root"))
response_post = self.client.post( response = self.client.post(self.refound_account_url, {"user": self.skia.id})
reverse("accounting:refound_account"), {"user": self.skia.id} self.assertRedirects(response, self.refound_account_url)
) self.skia.refresh_from_db()
self.skia = User.objects.filter(username="skia").first() response = self.client.get(self.refound_account_url)
response_get = self.client.get(reverse("accounting:refound_account")) assert response.status_code == 200
self.assertFalse(response_get.status_code == 403) assert '<form action="" method="post">' in str(response.content)
self.assertTrue('<form action="" method="post">' in str(response_get.content)) assert self.skia.customer.amount == 0
self.assertFalse(response_post.status_code == 403)
self.assertTrue(self.skia.customer.amount == 0)
def test_comptable_granteed(self): def test_comptable_granteed(self):
self.client.login(username="comptable", password="plop") self.client.force_login(User.objects.get(username="comptable"))
response_post = self.client.post( response = self.client.post(self.refound_account_url, {"user": self.skia.id})
reverse("accounting:refound_account"), {"user": self.skia.id} self.assertRedirects(response, self.refound_account_url)
) self.skia.refresh_from_db()
self.skia = User.objects.filter(username="skia").first() response = self.client.get(self.refound_account_url)
response_get = self.client.get(reverse("accounting:refound_account")) assert response.status_code == 200
self.assertFalse(response_get.status_code == 403) assert '<form action="" method="post">' in str(response.content)
self.assertTrue('<form action="" method="post">' in str(response_get.content)) assert self.skia.customer.amount == 0
self.assertFalse(response_post.status_code == 403)
self.assertTrue(self.skia.customer.amount == 0)
class JournalTest(TestCase): class TestJournal(TestCase):
def setUp(self): @classmethod
self.journal = GeneralJournal.objects.filter(id=1).first() def setUpTestData(cls):
cls.journal = GeneralJournal.objects.get(id=1)
def test_permission_granted(self): def test_permission_granted(self):
self.client.login(username="comptable", password="plop") self.client.force_login(User.objects.get(username="comptable"))
response_get = self.client.get( response_get = self.client.get(
reverse("accounting:journal_details", args=[self.journal.id]) reverse("accounting:journal_details", args=[self.journal.id])
) )
self.assertTrue(response_get.status_code == 200) assert response_get.status_code == 200
self.assertTrue( assert "<td>M\\xc3\\xa9thode de paiement</td>" in str(response_get.content)
"<td>M\\xc3\\xa9thode de paiement</td>" in str(response_get.content)
)
def test_permission_not_granted(self): def test_permission_not_granted(self):
self.client.login(username="skia", password="plop") self.client.force_login(User.objects.get(username="skia"))
response_get = self.client.get( response_get = self.client.get(
reverse("accounting:journal_details", args=[self.journal.id]) reverse("accounting:journal_details", args=[self.journal.id])
) )
self.assertTrue(response_get.status_code == 403) assert response_get.status_code == 403
self.assertFalse( assert "<td>M\xc3\xa9thode de paiement</td>" not in str(response_get.content)
"<td>M\xc3\xa9thode de paiement</td>" in str(response_get.content)
)
class OperationTest(TestCase): class TestOperation(TestCase):
def setUp(self): def setUp(self):
self.tomorrow_formatted = (date.today() + timedelta(days=1)).strftime( self.tomorrow_formatted = (date.today() + timedelta(days=1)).strftime(
"%d/%m/%Y" "%d/%m/%Y"
@ -108,9 +102,8 @@ class OperationTest(TestCase):
code="443", label="Ce code n'existe pas", movement_type="CREDIT" code="443", label="Ce code n'existe pas", movement_type="CREDIT"
) )
at.save() at.save()
l = Label(club_account=self.journal.club_account, name="bob") label = Label.objects.create(club_account=self.journal.club_account, name="bob")
l.save() self.client.force_login(User.objects.get(username="comptable"))
self.client.login(username="comptable", password="plop")
self.op1 = Operation( self.op1 = Operation(
journal=self.journal, journal=self.journal,
date=date.today(), date=date.today(),
@ -118,7 +111,7 @@ class OperationTest(TestCase):
remark="Test bilan", remark="Test bilan",
mode="CASH", mode="CASH",
done=True, done=True,
label=l, label=label,
accounting_type=at, accounting_type=at,
target_type="USER", target_type="USER",
target_id=self.skia.id, target_id=self.skia.id,
@ -131,7 +124,7 @@ class OperationTest(TestCase):
remark="Test bilan", remark="Test bilan",
mode="CASH", mode="CASH",
done=True, done=True,
label=l, label=label,
accounting_type=at, accounting_type=at,
target_type="USER", target_type="USER",
target_id=self.skia.id, target_id=self.skia.id,
@ -139,8 +132,7 @@ class OperationTest(TestCase):
self.op2.save() self.op2.save()
def test_new_operation(self): def test_new_operation(self):
self.client.login(username="comptable", password="plop") at = AccountingType.objects.get(code="604")
at = AccountingType.objects.filter(code="604").first()
response = self.client.post( response = self.client.post(
reverse("accounting:op_new", args=[self.journal.id]), reverse("accounting:op_new", args=[self.journal.id]),
{ {
@ -172,8 +164,7 @@ class OperationTest(TestCase):
self.assertTrue("<td>Le fantome de la nuit</td>" in str(response_get.content)) self.assertTrue("<td>Le fantome de la nuit</td>" in str(response_get.content))
def test_bad_new_operation(self): def test_bad_new_operation(self):
self.client.login(username="comptable", password="plop") AccountingType.objects.get(code="604")
AccountingType.objects.filter(code="604").first()
response = self.client.post( response = self.client.post(
reverse("accounting:op_new", args=[self.journal.id]), reverse("accounting:op_new", args=[self.journal.id]),
{ {
@ -199,7 +190,7 @@ class OperationTest(TestCase):
) )
def test_new_operation_not_authorized(self): def test_new_operation_not_authorized(self):
self.client.login(username="skia", password="plop") self.client.force_login(self.skia)
at = AccountingType.objects.filter(code="604").first() at = AccountingType.objects.filter(code="604").first()
response = self.client.post( response = self.client.post(
reverse("accounting:op_new", args=[self.journal.id]), reverse("accounting:op_new", args=[self.journal.id]),
@ -225,8 +216,7 @@ class OperationTest(TestCase):
self.journal.operations.filter(target_label="Le fantome du jour").exists() self.journal.operations.filter(target_label="Le fantome du jour").exists()
) )
def test__operation_simple_accounting(self): def test_operation_simple_accounting(self):
self.client.login(username="comptable", password="plop")
sat = SimplifiedAccountingType.objects.all().first() sat = SimplifiedAccountingType.objects.all().first()
response = self.client.post( response = self.client.post(
reverse("accounting:op_new", args=[self.journal.id]), reverse("accounting:op_new", args=[self.journal.id]),
@ -247,15 +237,14 @@ class OperationTest(TestCase):
"done": False, "done": False,
}, },
) )
self.assertFalse(response.status_code == 403) assert response.status_code != 403
self.assertTrue(self.journal.operations.filter(amount=23).exists()) assert self.journal.operations.filter(amount=23).exists()
response_get = self.client.get( response_get = self.client.get(
reverse("accounting:journal_details", args=[self.journal.id]) reverse("accounting:journal_details", args=[self.journal.id])
) )
self.assertTrue( assert "<td>Le fantome de l&#39;aurore</td>" in str(response_get.content)
"<td>Le fantome de l&#39;aurore</td>" in str(response_get.content)
) assert (
self.assertTrue(
self.journal.operations.filter(amount=23) self.journal.operations.filter(amount=23)
.values("accounting_type") .values("accounting_type")
.first()["accounting_type"] .first()["accounting_type"]
@ -263,47 +252,37 @@ class OperationTest(TestCase):
) )
def test_nature_statement(self): def test_nature_statement(self):
self.client.login(username="comptable", password="plop")
response = self.client.get( response = self.client.get(
reverse("accounting:journal_nature_statement", args=[self.journal.id]) reverse("accounting:journal_nature_statement", args=[self.journal.id])
) )
self.assertContains(response, "bob (Troll Penché) : 3.00", status_code=200) self.assertContains(response, "bob (Troll Penché) : 3.00", status_code=200)
def test_person_statement(self): def test_person_statement(self):
self.client.login(username="comptable", password="plop")
response = self.client.get( response = self.client.get(
reverse("accounting:journal_person_statement", args=[self.journal.id]) reverse("accounting:journal_person_statement", args=[self.journal.id])
) )
self.assertContains(response, "Total : 5575.72", status_code=200) self.assertContains(response, "Total : 5575.72", status_code=200)
self.assertContains(response, "Total : 71.42") self.assertContains(response, "Total : 71.42")
self.assertContains( content = response.content.decode()
response, self.assertInHTML(
""" """<td><a href="/user/1/">S&#39; Kia</a></td><td>3.00</td>""", content
<td><a href="/user/1/">S&#39; Kia</a></td>
<td>3.00</td>""",
) )
self.assertContains( self.assertInHTML(
response, """<td><a href="/user/1/">S&#39; Kia</a></td><td>823.00</td>""", content
"""
<td><a href="/user/1/">S&#39; Kia</a></td>
<td>823.00</td>""",
) )
def test_accounting_statement(self): def test_accounting_statement(self):
self.client.login(username="comptable", password="plop")
response = self.client.get( response = self.client.get(
reverse("accounting:journal_accounting_statement", args=[self.journal.id]) reverse("accounting:journal_accounting_statement", args=[self.journal.id])
) )
self.assertContains( assert response.status_code == 200
response, self.assertInHTML(
""" """
<tr> <tr>
<td>443 - Crédit - Ce code n&#39;existe pas</td> <td>443 - Crédit - Ce code n&#39;existe pas</td>
<td>3.00</td> <td>3.00</td>
</tr>""", </tr>""",
status_code=200, response.content.decode(),
) )
self.assertContains( self.assertContains(
response, response,

View File

@ -1,4 +1,3 @@
# -*- coding:utf-8 -*
# #
# Copyright 2023 © AE UTBM # Copyright 2023 © AE UTBM
# ae@utbm.fr / ae.info@utbm.fr # ae@utbm.fr / ae.info@utbm.fr
@ -6,17 +5,51 @@
# This file is part of the website of the UTBM Student Association (AE UTBM), # This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr. # https://ae.utbm.fr.
# #
# You can find the source code of the website at https://github.com/ae-utbm/sith3 # You can find the source code of the website at https://github.com/ae-utbm/sith
# #
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3) # LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE # SEE : https://raw.githubusercontent.com/ae-utbm/sith/master/LICENSE
# OR WITHIN THE LOCAL FILE "LICENSE" # OR WITHIN THE LOCAL FILE "LICENSE"
# #
# #
from django.urls import path from django.urls import path
from accounting.views import * from accounting.views import (
AccountingTypeCreateView,
AccountingTypeEditView,
AccountingTypeListView,
BankAccountCreateView,
BankAccountDeleteView,
BankAccountDetailView,
BankAccountEditView,
BankAccountListView,
ClubAccountCreateView,
ClubAccountDeleteView,
ClubAccountDetailView,
ClubAccountEditView,
CompanyCreateView,
CompanyEditView,
CompanyListView,
JournalAccountingStatementView,
JournalCreateView,
JournalDeleteView,
JournalDetailView,
JournalEditView,
JournalNatureStatementView,
JournalPersonStatementView,
LabelCreateView,
LabelDeleteView,
LabelEditView,
LabelListView,
OperationCreateView,
OperationEditView,
OperationPDFView,
RefoundAccountView,
SimplifiedAccountingTypeCreateView,
SimplifiedAccountingTypeEditView,
SimplifiedAccountingTypeListView,
)
urlpatterns = [ urlpatterns = [
# Accounting types # Accounting types

View File

@ -1,4 +1,3 @@
# -*- coding:utf-8 -*
# #
# Copyright 2023 © AE UTBM # Copyright 2023 © AE UTBM
# ae@utbm.fr / ae.info@utbm.fr # ae@utbm.fr / ae.info@utbm.fr
@ -6,57 +5,62 @@
# This file is part of the website of the UTBM Student Association (AE UTBM), # This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr. # https://ae.utbm.fr.
# #
# You can find the source code of the website at https://github.com/ae-utbm/sith3 # You can find the source code of the website at https://github.com/ae-utbm/sith
# #
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3) # LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE # SEE : https://raw.githubusercontent.com/ae-utbm/sith/master/LICENSE
# OR WITHIN THE LOCAL FILE "LICENSE" # OR WITHIN THE LOCAL FILE "LICENSE"
# #
# #
from django.views.generic import ListView, DetailView
from django.views.generic.edit import UpdateView, CreateView, DeleteView, FormView
from django.urls import reverse_lazy, reverse
from django.utils.translation import gettext_lazy as _
from django.forms.models import modelform_factory
from django.core.exceptions import PermissionDenied, ValidationError
from django.forms import HiddenInput
from django.db import transaction
from django.db.models import Sum
from django.conf import settings
from django import forms
from django.http import HttpResponse
import collections import collections
from ajax_select.fields import AutoCompleteSelectField from django import forms
from django.conf import settings
from django.core.exceptions import PermissionDenied, ValidationError
from django.db import transaction
from django.db.models import Sum
from django.forms import HiddenInput
from django.forms.models import modelform_factory
from django.http import HttpResponse
from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext_lazy as _
from django.views.generic import DetailView, ListView
from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView
from core.views import (
CanViewMixin,
CanEditMixin,
CanEditPropMixin,
CanCreateMixin,
TabedViewMixin,
)
from core.views.forms import SelectFile, SelectDate
from accounting.models import ( from accounting.models import (
AccountingType,
BankAccount, BankAccount,
ClubAccount, ClubAccount,
GeneralJournal,
Operation,
AccountingType,
Company, Company,
SimplifiedAccountingType, GeneralJournal,
Label, Label,
Operation,
SimplifiedAccountingType,
) )
from counter.models import Counter, Selling, Product from accounting.widgets.select import (
AutoCompleteSelectClubAccount,
AutoCompleteSelectCompany,
)
from club.models import Club
from club.widgets.select import AutoCompleteSelectClub
from core.models import User
from core.views import (
CanCreateMixin,
CanEditMixin,
CanEditPropMixin,
CanViewMixin,
TabedViewMixin,
)
from core.views.forms import SelectDate, SelectFile
from core.views.widgets.select import AutoCompleteSelectUser
from counter.models import Counter, Product, Selling
# Main accounting view # Main accounting view
class BankAccountListView(CanViewMixin, ListView): class BankAccountListView(CanViewMixin, ListView):
""" """A list view for the admins."""
A list view for the admins
"""
model = BankAccount model = BankAccount
template_name = "accounting/bank_account_list.jinja" template_name = "accounting/bank_account_list.jinja"
@ -67,18 +71,14 @@ class BankAccountListView(CanViewMixin, ListView):
class SimplifiedAccountingTypeListView(CanViewMixin, ListView): class SimplifiedAccountingTypeListView(CanViewMixin, ListView):
""" """A list view for the admins."""
A list view for the admins
"""
model = SimplifiedAccountingType model = SimplifiedAccountingType
template_name = "accounting/simplifiedaccountingtype_list.jinja" template_name = "accounting/simplifiedaccountingtype_list.jinja"
class SimplifiedAccountingTypeEditView(CanViewMixin, UpdateView): class SimplifiedAccountingTypeEditView(CanViewMixin, UpdateView):
""" """An edit view for the admins."""
An edit view for the admins
"""
model = SimplifiedAccountingType model = SimplifiedAccountingType
pk_url_kwarg = "type_id" pk_url_kwarg = "type_id"
@ -87,9 +87,7 @@ class SimplifiedAccountingTypeEditView(CanViewMixin, UpdateView):
class SimplifiedAccountingTypeCreateView(CanCreateMixin, CreateView): class SimplifiedAccountingTypeCreateView(CanCreateMixin, CreateView):
""" """Create an accounting type (for the admins)."""
Create an accounting type (for the admins)
"""
model = SimplifiedAccountingType model = SimplifiedAccountingType
fields = ["label", "accounting_type"] fields = ["label", "accounting_type"]
@ -100,18 +98,14 @@ class SimplifiedAccountingTypeCreateView(CanCreateMixin, CreateView):
class AccountingTypeListView(CanViewMixin, ListView): class AccountingTypeListView(CanViewMixin, ListView):
""" """A list view for the admins."""
A list view for the admins
"""
model = AccountingType model = AccountingType
template_name = "accounting/accountingtype_list.jinja" template_name = "accounting/accountingtype_list.jinja"
class AccountingTypeEditView(CanViewMixin, UpdateView): class AccountingTypeEditView(CanViewMixin, UpdateView):
""" """An edit view for the admins."""
An edit view for the admins
"""
model = AccountingType model = AccountingType
pk_url_kwarg = "type_id" pk_url_kwarg = "type_id"
@ -120,9 +114,7 @@ class AccountingTypeEditView(CanViewMixin, UpdateView):
class AccountingTypeCreateView(CanCreateMixin, CreateView): class AccountingTypeCreateView(CanCreateMixin, CreateView):
""" """Create an accounting type (for the admins)."""
Create an accounting type (for the admins)
"""
model = AccountingType model = AccountingType
fields = ["code", "label", "movement_type"] fields = ["code", "label", "movement_type"]
@ -133,9 +125,7 @@ class AccountingTypeCreateView(CanCreateMixin, CreateView):
class BankAccountEditView(CanViewMixin, UpdateView): class BankAccountEditView(CanViewMixin, UpdateView):
""" """An edit view for the admins."""
An edit view for the admins
"""
model = BankAccount model = BankAccount
pk_url_kwarg = "b_account_id" pk_url_kwarg = "b_account_id"
@ -144,9 +134,7 @@ class BankAccountEditView(CanViewMixin, UpdateView):
class BankAccountDetailView(CanViewMixin, DetailView): class BankAccountDetailView(CanViewMixin, DetailView):
""" """A detail view, listing every club account."""
A detail view, listing every club account
"""
model = BankAccount model = BankAccount
pk_url_kwarg = "b_account_id" pk_url_kwarg = "b_account_id"
@ -154,9 +142,7 @@ class BankAccountDetailView(CanViewMixin, DetailView):
class BankAccountCreateView(CanCreateMixin, CreateView): class BankAccountCreateView(CanCreateMixin, CreateView):
""" """Create a bank account (for the admins)."""
Create a bank account (for the admins)
"""
model = BankAccount model = BankAccount
fields = ["name", "club", "iban", "number"] fields = ["name", "club", "iban", "number"]
@ -166,9 +152,7 @@ class BankAccountCreateView(CanCreateMixin, CreateView):
class BankAccountDeleteView( class BankAccountDeleteView(
CanEditPropMixin, DeleteView CanEditPropMixin, DeleteView
): # TODO change Delete to Close ): # TODO change Delete to Close
""" """Delete a bank account (for the admins)."""
Delete a bank account (for the admins)
"""
model = BankAccount model = BankAccount
pk_url_kwarg = "b_account_id" pk_url_kwarg = "b_account_id"
@ -180,9 +164,7 @@ class BankAccountDeleteView(
class ClubAccountEditView(CanViewMixin, UpdateView): class ClubAccountEditView(CanViewMixin, UpdateView):
""" """An edit view for the admins."""
An edit view for the admins
"""
model = ClubAccount model = ClubAccount
pk_url_kwarg = "c_account_id" pk_url_kwarg = "c_account_id"
@ -191,9 +173,7 @@ class ClubAccountEditView(CanViewMixin, UpdateView):
class ClubAccountDetailView(CanViewMixin, DetailView): class ClubAccountDetailView(CanViewMixin, DetailView):
""" """A detail view, listing every journal."""
A detail view, listing every journal
"""
model = ClubAccount model = ClubAccount
pk_url_kwarg = "c_account_id" pk_url_kwarg = "c_account_id"
@ -201,17 +181,15 @@ class ClubAccountDetailView(CanViewMixin, DetailView):
class ClubAccountCreateView(CanCreateMixin, CreateView): class ClubAccountCreateView(CanCreateMixin, CreateView):
""" """Create a club account (for the admins)."""
Create a club account (for the admins)
"""
model = ClubAccount model = ClubAccount
fields = ["name", "club", "bank_account"] fields = ["name", "club", "bank_account"]
template_name = "core/create.jinja" template_name = "core/create.jinja"
def get_initial(self): def get_initial(self):
ret = super(ClubAccountCreateView, self).get_initial() ret = super().get_initial()
if "parent" in self.request.GET.keys(): if "parent" in self.request.GET:
obj = BankAccount.objects.filter(id=int(self.request.GET["parent"])).first() obj = BankAccount.objects.filter(id=int(self.request.GET["parent"])).first()
if obj is not None: if obj is not None:
ret["bank_account"] = obj.id ret["bank_account"] = obj.id
@ -221,9 +199,7 @@ class ClubAccountCreateView(CanCreateMixin, CreateView):
class ClubAccountDeleteView( class ClubAccountDeleteView(
CanEditPropMixin, DeleteView CanEditPropMixin, DeleteView
): # TODO change Delete to Close ): # TODO change Delete to Close
""" """Delete a club account (for the admins)."""
Delete a club account (for the admins)
"""
model = ClubAccount model = ClubAccount
pk_url_kwarg = "c_account_id" pk_url_kwarg = "c_account_id"
@ -239,17 +215,14 @@ class JournalTabsMixin(TabedViewMixin):
return _("Journal") return _("Journal")
def get_list_of_tabs(self): def get_list_of_tabs(self):
tab_list = [] return [
tab_list.append(
{ {
"url": reverse( "url": reverse(
"accounting:journal_details", kwargs={"j_id": self.object.id} "accounting:journal_details", kwargs={"j_id": self.object.id}
), ),
"slug": "journal", "slug": "journal",
"name": _("Journal"), "name": _("Journal"),
} },
)
tab_list.append(
{ {
"url": reverse( "url": reverse(
"accounting:journal_nature_statement", "accounting:journal_nature_statement",
@ -257,9 +230,7 @@ class JournalTabsMixin(TabedViewMixin):
), ),
"slug": "nature_statement", "slug": "nature_statement",
"name": _("Statement by nature"), "name": _("Statement by nature"),
} },
)
tab_list.append(
{ {
"url": reverse( "url": reverse(
"accounting:journal_person_statement", "accounting:journal_person_statement",
@ -267,9 +238,7 @@ class JournalTabsMixin(TabedViewMixin):
), ),
"slug": "person_statement", "slug": "person_statement",
"name": _("Statement by person"), "name": _("Statement by person"),
} },
)
tab_list.append(
{ {
"url": reverse( "url": reverse(
"accounting:journal_accounting_statement", "accounting:journal_accounting_statement",
@ -277,15 +246,12 @@ class JournalTabsMixin(TabedViewMixin):
), ),
"slug": "accounting_statement", "slug": "accounting_statement",
"name": _("Accounting statement"), "name": _("Accounting statement"),
} },
) ]
return tab_list
class JournalCreateView(CanCreateMixin, CreateView): class JournalCreateView(CanCreateMixin, CreateView):
""" """Create a general journal."""
Create a general journal
"""
model = GeneralJournal model = GeneralJournal
form_class = modelform_factory( form_class = modelform_factory(
@ -296,8 +262,8 @@ class JournalCreateView(CanCreateMixin, CreateView):
template_name = "core/create.jinja" template_name = "core/create.jinja"
def get_initial(self): def get_initial(self):
ret = super(JournalCreateView, self).get_initial() ret = super().get_initial()
if "parent" in self.request.GET.keys(): if "parent" in self.request.GET:
obj = ClubAccount.objects.filter(id=int(self.request.GET["parent"])).first() obj = ClubAccount.objects.filter(id=int(self.request.GET["parent"])).first()
if obj is not None: if obj is not None:
ret["club_account"] = obj.id ret["club_account"] = obj.id
@ -305,9 +271,7 @@ class JournalCreateView(CanCreateMixin, CreateView):
class JournalDetailView(JournalTabsMixin, CanViewMixin, DetailView): class JournalDetailView(JournalTabsMixin, CanViewMixin, DetailView):
""" """A detail view, listing every operation."""
A detail view, listing every operation
"""
model = GeneralJournal model = GeneralJournal
pk_url_kwarg = "j_id" pk_url_kwarg = "j_id"
@ -316,9 +280,7 @@ class JournalDetailView(JournalTabsMixin, CanViewMixin, DetailView):
class JournalEditView(CanEditMixin, UpdateView): class JournalEditView(CanEditMixin, UpdateView):
""" """Update a general journal."""
Update a general journal
"""
model = GeneralJournal model = GeneralJournal
pk_url_kwarg = "j_id" pk_url_kwarg = "j_id"
@ -327,9 +289,7 @@ class JournalEditView(CanEditMixin, UpdateView):
class JournalDeleteView(CanEditPropMixin, DeleteView): class JournalDeleteView(CanEditPropMixin, DeleteView):
""" """Delete a club account (for the admins)."""
Delete a club account (for the admins)
"""
model = GeneralJournal model = GeneralJournal
pk_url_kwarg = "j_id" pk_url_kwarg = "j_id"
@ -339,7 +299,7 @@ class JournalDeleteView(CanEditPropMixin, DeleteView):
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
self.object = self.get_object() self.object = self.get_object()
if self.object.operations.count() == 0: if self.object.operations.count() == 0:
return super(JournalDeleteView, self).dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
else: else:
raise PermissionDenied raise PermissionDenied
@ -373,12 +333,30 @@ class OperationForm(forms.ModelForm):
"invoice": SelectFile, "invoice": SelectFile,
} }
user = AutoCompleteSelectField("users", help_text=None, required=False) user = forms.ModelChoiceField(
club_account = AutoCompleteSelectField( help_text=None,
"club_accounts", help_text=None, required=False required=False,
widget=AutoCompleteSelectUser,
queryset=User.objects.all(),
)
club_account = forms.ModelChoiceField(
help_text=None,
required=False,
widget=AutoCompleteSelectClubAccount,
queryset=ClubAccount.objects.all(),
)
club = forms.ModelChoiceField(
help_text=None,
required=False,
widget=AutoCompleteSelectClub,
queryset=Club.objects.all(),
)
company = forms.ModelChoiceField(
help_text=None,
required=False,
widget=AutoCompleteSelectCompany,
queryset=Company.objects.all(),
) )
club = AutoCompleteSelectField("clubs", help_text=None, required=False)
company = AutoCompleteSelectField("companies", help_text=None, required=False)
need_link = forms.BooleanField( need_link = forms.BooleanField(
label=_("Link this operation to the target account"), label=_("Link this operation to the target account"),
required=False, required=False,
@ -387,7 +365,7 @@ class OperationForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
club_account = kwargs.pop("club_account", None) club_account = kwargs.pop("club_account", None)
super(OperationForm, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if club_account: if club_account:
self.fields["label"].queryset = club_account.labels.order_by("name").all() self.fields["label"].queryset = club_account.labels.order_by("name").all()
if self.instance.target_type == "USER": if self.instance.target_type == "USER":
@ -400,8 +378,8 @@ class OperationForm(forms.ModelForm):
self.fields["company"].initial = self.instance.target_id self.fields["company"].initial = self.instance.target_id
def clean(self): def clean(self):
self.cleaned_data = super(OperationForm, self).clean() self.cleaned_data = super().clean()
if "target_type" in self.cleaned_data.keys(): if "target_type" in self.cleaned_data:
if ( if (
self.cleaned_data.get("user") is None self.cleaned_data.get("user") is None
and self.cleaned_data.get("club") is None and self.cleaned_data.get("club") is None
@ -430,7 +408,7 @@ class OperationForm(forms.ModelForm):
return self.cleaned_data return self.cleaned_data
def save(self): def save(self):
ret = super(OperationForm, self).save() ret = super().save()
if ( if (
self.instance.target_type == "ACCOUNT" self.instance.target_type == "ACCOUNT"
and not self.instance.linked_operation and not self.instance.linked_operation
@ -468,9 +446,7 @@ class OperationForm(forms.ModelForm):
class OperationCreateView(CanCreateMixin, CreateView): class OperationCreateView(CanCreateMixin, CreateView):
""" """Create an operation."""
Create an operation
"""
model = Operation model = Operation
form_class = OperationForm form_class = OperationForm
@ -482,23 +458,21 @@ class OperationCreateView(CanCreateMixin, CreateView):
return self.form_class(club_account=ca, **self.get_form_kwargs()) return self.form_class(club_account=ca, **self.get_form_kwargs())
def get_initial(self): def get_initial(self):
ret = super(OperationCreateView, self).get_initial() ret = super().get_initial()
if self.journal is not None: if self.journal is not None:
ret["journal"] = self.journal.id ret["journal"] = self.journal.id
return ret return ret
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
"""Add journal to the context""" """Add journal to the context."""
kwargs = super(OperationCreateView, self).get_context_data(**kwargs) kwargs = super().get_context_data(**kwargs)
if self.journal: if self.journal:
kwargs["object"] = self.journal kwargs["object"] = self.journal
return kwargs return kwargs
class OperationEditView(CanEditMixin, UpdateView): class OperationEditView(CanEditMixin, UpdateView):
""" """An edit view, working as detail for the moment."""
An edit view, working as detail for the moment
"""
model = Operation model = Operation
pk_url_kwarg = "op_id" pk_url_kwarg = "op_id"
@ -506,29 +480,27 @@ class OperationEditView(CanEditMixin, UpdateView):
template_name = "accounting/operation_edit.jinja" template_name = "accounting/operation_edit.jinja"
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
"""Add journal to the context""" """Add journal to the context."""
kwargs = super(OperationEditView, self).get_context_data(**kwargs) kwargs = super().get_context_data(**kwargs)
kwargs["object"] = self.object.journal kwargs["object"] = self.object.journal
return kwargs return kwargs
class OperationPDFView(CanViewMixin, DetailView): class OperationPDFView(CanViewMixin, DetailView):
""" """Display the PDF of a given operation."""
Display the PDF of a given operation
"""
model = Operation model = Operation
pk_url_kwarg = "op_id" pk_url_kwarg = "op_id"
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
from reportlab.pdfgen import canvas
from reportlab.lib.units import cm
from reportlab.platypus import Table, TableStyle
from reportlab.lib import colors from reportlab.lib import colors
from reportlab.lib.pagesizes import letter from reportlab.lib.pagesizes import letter
from reportlab.lib.units import cm
from reportlab.lib.utils import ImageReader from reportlab.lib.utils import ImageReader
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.pdfgen import canvas
from reportlab.platypus import Table, TableStyle
pdfmetrics.registerFont(TTFont("DejaVu", "DejaVuSerif.ttf")) pdfmetrics.registerFont(TTFont("DejaVu", "DejaVuSerif.ttf"))
@ -599,7 +571,7 @@ class OperationPDFView(CanViewMixin, DetailView):
payment_mode = "" payment_mode = ""
for m in settings.SITH_ACCOUNTING_PAYMENT_METHOD: for m in settings.SITH_ACCOUNTING_PAYMENT_METHOD:
if m[0] == mode: if m[0] == mode:
payment_mode += "[\u00D7]" payment_mode += "[\u00d7]"
else: else:
payment_mode += "[ ]" payment_mode += "[ ]"
payment_mode += " %s\n" % (m[1]) payment_mode += " %s\n" % (m[1])
@ -667,9 +639,7 @@ class OperationPDFView(CanViewMixin, DetailView):
class JournalNatureStatementView(JournalTabsMixin, CanViewMixin, DetailView): class JournalNatureStatementView(JournalTabsMixin, CanViewMixin, DetailView):
""" """Display a statement sorted by labels."""
Display a statement sorted by labels
"""
model = GeneralJournal model = GeneralJournal
pk_url_kwarg = "j_id" pk_url_kwarg = "j_id"
@ -680,19 +650,17 @@ class JournalNatureStatementView(JournalTabsMixin, CanViewMixin, DetailView):
ret = collections.OrderedDict() ret = collections.OrderedDict()
statement = collections.OrderedDict() statement = collections.OrderedDict()
total_sum = 0 total_sum = 0
for sat in [None] + list( for sat in [
SimplifiedAccountingType.objects.order_by("label").all() None,
): *list(SimplifiedAccountingType.objects.order_by("label")),
sum = queryset.filter( ]:
amount = queryset.filter(
accounting_type__movement_type=movement_type, simpleaccounting_type=sat accounting_type__movement_type=movement_type, simpleaccounting_type=sat
).aggregate(amount_sum=Sum("amount"))["amount_sum"] ).aggregate(amount_sum=Sum("amount"))["amount_sum"]
if sat: label = sat.label if sat is not None else ""
sat = sat.label if amount:
else: total_sum += amount
sat = "" statement[label] = amount
if sum:
total_sum += sum
statement[sat] = sum
ret[movement_type] = statement ret[movement_type] = statement
ret[movement_type + "_sum"] = total_sum ret[movement_type + "_sum"] = total_sum
return ret return ret
@ -715,28 +683,23 @@ class JournalNatureStatementView(JournalTabsMixin, CanViewMixin, DetailView):
self.statement(self.object.operations.filter(label=None).all(), "DEBIT") self.statement(self.object.operations.filter(label=None).all(), "DEBIT")
) )
statement[_("No label operations")] = no_label_statement statement[_("No label operations")] = no_label_statement
for l in labels: for label in labels:
l_stmt = collections.OrderedDict() l_stmt = collections.OrderedDict()
l_stmt.update( journals = self.object.operations.filter(label=label).all()
self.statement(self.object.operations.filter(label=l).all(), "CREDIT") l_stmt.update(self.statement(journals, "CREDIT"))
) l_stmt.update(self.statement(journals, "DEBIT"))
l_stmt.update( statement[label] = l_stmt
self.statement(self.object.operations.filter(label=l).all(), "DEBIT")
)
statement[l] = l_stmt
return statement return statement
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
"""Add infos to the context""" """Add infos to the context."""
kwargs = super(JournalNatureStatementView, self).get_context_data(**kwargs) kwargs = super().get_context_data(**kwargs)
kwargs["statement"] = self.big_statement() kwargs["statement"] = self.big_statement()
return kwargs return kwargs
class JournalPersonStatementView(JournalTabsMixin, CanViewMixin, DetailView): class JournalPersonStatementView(JournalTabsMixin, CanViewMixin, DetailView):
""" """Calculate a dictionary with operation target and sum of operations."""
Calculate a dictionary with operation target and sum of operations
"""
model = GeneralJournal model = GeneralJournal
pk_url_kwarg = "j_id" pk_url_kwarg = "j_id"
@ -766,8 +729,8 @@ class JournalPersonStatementView(JournalTabsMixin, CanViewMixin, DetailView):
return sum(self.statement(movement_type).values()) return sum(self.statement(movement_type).values())
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
"""Add journal to the context""" """Add journal to the context."""
kwargs = super(JournalPersonStatementView, self).get_context_data(**kwargs) kwargs = super().get_context_data(**kwargs)
kwargs["credit_statement"] = self.statement("CREDIT") kwargs["credit_statement"] = self.statement("CREDIT")
kwargs["debit_statement"] = self.statement("DEBIT") kwargs["debit_statement"] = self.statement("DEBIT")
kwargs["total_credit"] = self.total("CREDIT") kwargs["total_credit"] = self.total("CREDIT")
@ -776,9 +739,7 @@ class JournalPersonStatementView(JournalTabsMixin, CanViewMixin, DetailView):
class JournalAccountingStatementView(JournalTabsMixin, CanViewMixin, DetailView): class JournalAccountingStatementView(JournalTabsMixin, CanViewMixin, DetailView):
""" """Calculate a dictionary with operation type and sum of operations."""
Calculate a dictionary with operation type and sum of operations
"""
model = GeneralJournal model = GeneralJournal
pk_url_kwarg = "j_id" pk_url_kwarg = "j_id"
@ -796,8 +757,8 @@ class JournalAccountingStatementView(JournalTabsMixin, CanViewMixin, DetailView)
return statement return statement
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
"""Add journal to the context""" """Add journal to the context."""
kwargs = super(JournalAccountingStatementView, self).get_context_data(**kwargs) kwargs = super().get_context_data(**kwargs)
kwargs["statement"] = self.statement() kwargs["statement"] = self.statement()
return kwargs return kwargs
@ -811,9 +772,7 @@ class CompanyListView(CanViewMixin, ListView):
class CompanyCreateView(CanCreateMixin, CreateView): class CompanyCreateView(CanCreateMixin, CreateView):
""" """Create a company."""
Create a company
"""
model = Company model = Company
fields = ["name"] fields = ["name"]
@ -822,9 +781,7 @@ class CompanyCreateView(CanCreateMixin, CreateView):
class CompanyEditView(CanCreateMixin, UpdateView): class CompanyEditView(CanCreateMixin, UpdateView):
""" """Edit a company."""
Edit a company
"""
model = Company model = Company
pk_url_kwarg = "co_id" pk_url_kwarg = "co_id"
@ -852,8 +809,8 @@ class LabelCreateView(
template_name = "core/create.jinja" template_name = "core/create.jinja"
def get_initial(self): def get_initial(self):
ret = super(LabelCreateView, self).get_initial() ret = super().get_initial()
if "parent" in self.request.GET.keys(): if "parent" in self.request.GET:
obj = ClubAccount.objects.filter(id=int(self.request.GET["parent"])).first() obj = ClubAccount.objects.filter(id=int(self.request.GET["parent"])).first()
if obj is not None: if obj is not None:
ret["club_account"] = obj.id ret["club_account"] = obj.id
@ -877,15 +834,17 @@ class LabelDeleteView(CanEditMixin, DeleteView):
class CloseCustomerAccountForm(forms.Form): class CloseCustomerAccountForm(forms.Form):
user = AutoCompleteSelectField( user = forms.ModelChoiceField(
"users", label=_("Refound this account"), help_text=None, required=True label=_("Refound this account"),
help_text=None,
required=True,
widget=AutoCompleteSelectUser,
queryset=User.objects.all(),
) )
class RefoundAccountView(FormView): class RefoundAccountView(FormView):
""" """Create a selling with the same amount than the current user money."""
Create a selling with the same amount than the current user money
"""
template_name = "accounting/refound_account.jinja" template_name = "accounting/refound_account.jinja"
form_class = CloseCustomerAccountForm form_class = CloseCustomerAccountForm
@ -897,19 +856,19 @@ class RefoundAccountView(FormView):
raise PermissionDenied raise PermissionDenied
def dispatch(self, request, *arg, **kwargs): def dispatch(self, request, *arg, **kwargs):
res = super(RefoundAccountView, self).dispatch(request, *arg, **kwargs) res = super().dispatch(request, *arg, **kwargs)
if self.permission(request.user): if self.permission(request.user):
return res return res
def post(self, request, *arg, **kwargs): def post(self, request, *arg, **kwargs):
self.operator = request.user self.operator = request.user
if self.permission(request.user): if self.permission(request.user):
return super(RefoundAccountView, self).post(self, request, *arg, **kwargs) return super().post(self, request, *arg, **kwargs)
def form_valid(self, form): def form_valid(self, form):
self.customer = form.cleaned_data["user"] self.customer = form.cleaned_data["user"]
self.create_selling() self.create_selling()
return super(RefoundAccountView, self).form_valid(form) return super().form_valid(form)
def get_success_url(self): def get_success_url(self):
return reverse("accounting:refound_account") return reverse("accounting:refound_account")

View File

@ -0,0 +1,39 @@
from pydantic import TypeAdapter
from accounting.models import ClubAccount, Company
from accounting.schemas import ClubAccountSchema, CompanySchema
from core.views.widgets.select import AutoCompleteSelect, AutoCompleteSelectMultiple
_js = ["bundled/accounting/components/ajax-select-index.ts"]
class AutoCompleteSelectClubAccount(AutoCompleteSelect):
component_name = "club-account-ajax-select"
model = ClubAccount
adapter = TypeAdapter(list[ClubAccountSchema])
js = _js
class AutoCompleteSelectMultipleClubAccount(AutoCompleteSelectMultiple):
component_name = "club-account-ajax-select"
model = ClubAccount
adapter = TypeAdapter(list[ClubAccountSchema])
js = _js
class AutoCompleteSelectCompany(AutoCompleteSelect):
component_name = "company-ajax-select"
model = Company
adapter = TypeAdapter(list[CompanySchema])
js = _js
class AutoCompleteSelectMultipleCompany(AutoCompleteSelectMultiple):
component_name = "company-ajax-select"
model = Company
adapter = TypeAdapter(list[CompanySchema])
js = _js

10
antispam/admin.py Normal file
View File

@ -0,0 +1,10 @@
from django.contrib import admin
from antispam.models import ToxicDomain
@admin.register(ToxicDomain)
class ToxicDomainAdmin(admin.ModelAdmin):
list_display = ("domain", "is_externally_managed", "created")
search_fields = ("domain", "is_externally_managed", "created")
list_filter = ("is_externally_managed",)

7
antispam/apps.py Normal file
View File

@ -0,0 +1,7 @@
from django.apps import AppConfig
class AntispamConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
verbose_name = "antispam"
name = "antispam"

18
antispam/forms.py Normal file
View File

@ -0,0 +1,18 @@
import re
from django import forms
from django.core.validators import EmailValidator
from django.utils.translation import gettext_lazy as _
from antispam.models import ToxicDomain
class AntiSpamEmailField(forms.EmailField):
"""An email field that email addresses with a known toxic domain."""
def run_validators(self, value: str):
super().run_validators(value)
# Domain part should exist since email validation is guaranteed to run first
domain = re.search(EmailValidator.domain_regex, value)
if ToxicDomain.objects.filter(domain=domain[0]).exists():
raise forms.ValidationError(_("Email domain is not allowed."))

View File

View File

@ -0,0 +1,69 @@
import requests
from django.conf import settings
from django.core.management import BaseCommand
from django.db.models import Max
from django.utils import timezone
from antispam.models import ToxicDomain
class Command(BaseCommand):
"""Update blocked ips/mails database"""
help = "Update blocked ips/mails database"
def add_arguments(self, parser):
parser.add_argument(
"--force", action="store_true", help="Force re-creation even if up to date"
)
def _should_update(self, *, force: bool = False) -> bool:
if force:
return True
oldest = ToxicDomain.objects.filter(is_externally_managed=True).aggregate(
res=Max("created")
)["res"]
return not (oldest and timezone.now() < (oldest + timezone.timedelta(days=1)))
def _download_domains(self, providers: list[str]) -> set[str]:
domains = set()
for provider in providers:
res = requests.get(provider)
if not res.ok:
self.stderr.write(
f"Source {provider} responded with code {res.status_code}"
)
continue
domains |= set(res.content.decode().splitlines())
return domains
def _update_domains(self, domains: set[str]):
# Cleanup database
ToxicDomain.objects.filter(is_externally_managed=True).delete()
# Create database
ToxicDomain.objects.bulk_create(
[
ToxicDomain(domain=domain, is_externally_managed=True)
for domain in domains
],
ignore_conflicts=True,
)
self.stdout.write("Domain database updated")
def handle(self, *args, **options):
if not self._should_update(force=options["force"]):
self.stdout.write("Domain database is up to date")
return
self.stdout.write("Updating domain database")
domains = self._download_domains(settings.TOXIC_DOMAINS_PROVIDERS)
if not domains:
self.stderr.write(
"No domains could be fetched from settings.TOXIC_DOMAINS_PROVIDERS. "
"Please, have a look at your settings."
)
return
self._update_domains(domains)

View File

@ -0,0 +1,35 @@
# Generated by Django 4.2.14 on 2024-08-03 23:05
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name="ToxicDomain",
fields=[
(
"domain",
models.URLField(
max_length=253,
primary_key=True,
serialize=False,
verbose_name="domain",
),
),
("created", models.DateTimeField(auto_now_add=True)),
(
"is_externally_managed",
models.BooleanField(
default=False,
help_text="True if kept up-to-date using external toxic domain providers, else False",
verbose_name="is externally managed",
),
),
],
),
]

View File

19
antispam/models.py Normal file
View File

@ -0,0 +1,19 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
class ToxicDomain(models.Model):
"""Domain marked as spam in public databases"""
domain = models.URLField(_("domain"), max_length=253, primary_key=True)
created = models.DateTimeField(auto_now_add=True)
is_externally_managed = models.BooleanField(
_("is externally managed"),
default=False,
help_text=_(
"True if kept up-to-date using external toxic domain providers, else False"
),
)
def __str__(self) -> str:
return self.domain

View File

@ -1,15 +0,0 @@
# -*- coding:utf-8 -*
#
# Copyright 2023 © AE UTBM
# ae@utbm.fr / ae.info@utbm.fr
#
# This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr.
#
# You can find the source code of the website at https://github.com/ae-utbm/sith3
#
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
# OR WITHIN THE LOCAL FILE "LICENSE"
#
#

View File

@ -1,19 +0,0 @@
# -*- coding:utf-8 -*
#
# Copyright 2023 © AE UTBM
# ae@utbm.fr / ae.info@utbm.fr
#
# This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr.
#
# You can find the source code of the website at https://github.com/ae-utbm/sith3
#
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
# OR WITHIN THE LOCAL FILE "LICENSE"
#
#
from django.contrib import admin
# Register your models here.

View File

@ -1,19 +0,0 @@
# -*- coding:utf-8 -*
#
# Copyright 2023 © AE UTBM
# ae@utbm.fr / ae.info@utbm.fr
#
# This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr.
#
# You can find the source code of the website at https://github.com/ae-utbm/sith3
#
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
# OR WITHIN THE LOCAL FILE "LICENSE"
#
#
from django.db import models
# Create your models here.

View File

@ -1,19 +0,0 @@
# -*- coding:utf-8 -*
#
# Copyright 2023 © AE UTBM
# ae@utbm.fr / ae.info@utbm.fr
#
# This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr.
#
# You can find the source code of the website at https://github.com/ae-utbm/sith3
#
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
# OR WITHIN THE LOCAL FILE "LICENSE"
#
#
from django.test import TestCase
# Create your tests here.

View File

@ -1,50 +0,0 @@
# -*- coding:utf-8 -*
#
# Copyright 2023 © AE UTBM
# ae@utbm.fr / ae.info@utbm.fr
#
# This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr.
#
# You can find the source code of the website at https://github.com/ae-utbm/sith3
#
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
# OR WITHIN THE LOCAL FILE "LICENSE"
#
#
from django.urls import re_path, path, include
from api.views import *
from rest_framework import routers
# Router config
router = routers.DefaultRouter()
router.register(r"counter", CounterViewSet, basename="api_counter")
router.register(r"user", UserViewSet, basename="api_user")
router.register(r"club", ClubViewSet, basename="api_club")
router.register(r"group", GroupViewSet, basename="api_group")
# Launderette
router.register(
r"launderette/place", LaunderettePlaceViewSet, basename="api_launderette_place"
)
router.register(
r"launderette/machine",
LaunderetteMachineViewSet,
basename="api_launderette_machine",
)
router.register(
r"launderette/token", LaunderetteTokenViewSet, basename="api_launderette_token"
)
urlpatterns = [
# API
re_path(r"^", include(router.urls)),
re_path(r"^login/", include("rest_framework.urls", namespace="rest_framework")),
re_path(r"^markdown$", RenderMarkdown, name="api_markdown"),
re_path(r"^mailings$", FetchMailingLists, name="mailings_fetch"),
re_path(r"^uv$", uv_endpoint, name="uv_endpoint"),
path("sas/<int:user>", all_pictures_of_user_endpoint, name="all_pictures_of_user"),
]

View File

@ -1,73 +0,0 @@
# -*- coding:utf-8 -*
#
# Copyright 2023 © AE UTBM
# ae@utbm.fr / ae.info@utbm.fr
#
# This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr.
#
# You can find the source code of the website at https://github.com/ae-utbm/sith3
#
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
# OR WITHIN THE LOCAL FILE "LICENSE"
#
#
from rest_framework.response import Response
from rest_framework import viewsets
from django.core.exceptions import PermissionDenied
from rest_framework.decorators import action
from django.db.models.query import QuerySet
from core.views import can_view, can_edit
def check_if(obj, user, test):
"""
Detect if it's a single object or a queryset
aply a given test on individual object and return global permission
"""
if isinstance(obj, QuerySet):
for o in obj:
if test(o, user) is False:
return False
return True
else:
return test(obj, user)
class ManageModelMixin:
@action(detail=True)
def id(self, request, pk=None):
"""
Get by id (api/v1/router/{pk}/id/)
"""
self.queryset = get_object_or_404(self.queryset.filter(id=pk))
serializer = self.get_serializer(self.queryset)
return Response(serializer.data)
class RightModelViewSet(ManageModelMixin, viewsets.ModelViewSet):
def dispatch(self, request, *arg, **kwargs):
res = super(RightModelViewSet, self).dispatch(request, *arg, **kwargs)
obj = self.queryset
user = self.request.user
try:
if request.method == "GET" and check_if(obj, user, can_view):
return res
if request.method != "GET" and check_if(obj, user, can_edit):
return res
except:
pass # To prevent bug with Anonymous user
raise PermissionDenied
from .api import *
from .counter import *
from .user import *
from .club import *
from .group import *
from .launderette import *
from .uv import *
from .sas import *

View File

@ -1,34 +0,0 @@
# -*- coding:utf-8 -*
#
# Copyright 2023 © AE UTBM
# ae@utbm.fr / ae.info@utbm.fr
#
# This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr.
#
# You can find the source code of the website at https://github.com/ae-utbm/sith3
#
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
# OR WITHIN THE LOCAL FILE "LICENSE"
#
#
from rest_framework.response import Response
from rest_framework.decorators import api_view, renderer_classes
from rest_framework.renderers import StaticHTMLRenderer
from core.templatetags.renderer import markdown
@api_view(["POST"])
@renderer_classes((StaticHTMLRenderer,))
def RenderMarkdown(request):
"""
Render Markdown
"""
try:
data = markdown(request.POST["text"])
except:
data = "Error"
return Response(data)

View File

@ -1,56 +0,0 @@
# -*- coding:utf-8 -*
#
# Copyright 2023 © AE UTBM
# ae@utbm.fr / ae.info@utbm.fr
#
# This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr.
#
# You can find the source code of the website at https://github.com/ae-utbm/sith3
#
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
# OR WITHIN THE LOCAL FILE "LICENSE"
#
#
from rest_framework.response import Response
from rest_framework import serializers
from rest_framework.decorators import api_view, renderer_classes
from rest_framework.renderers import StaticHTMLRenderer
from django.conf import settings
from django.core.exceptions import PermissionDenied
from club.models import Club, Mailing
from api.views import RightModelViewSet
class ClubSerializer(serializers.ModelSerializer):
class Meta:
model = Club
fields = ("id", "name", "unix_name", "address", "members")
class ClubViewSet(RightModelViewSet):
"""
Manage Clubs (api/v1/club/)
"""
serializer_class = ClubSerializer
queryset = Club.objects.all()
@api_view(["GET"])
@renderer_classes((StaticHTMLRenderer,))
def FetchMailingLists(request):
key = request.GET.get("key", "")
if key != settings.SITH_MAILING_FETCH_KEY:
raise PermissionDenied
data = ""
for mailing in Mailing.objects.filter(
is_moderated=True, club__is_active=True
).all():
data += mailing.fetch_format() + "\n"
return Response(data)

View File

@ -1,52 +0,0 @@
# -*- coding:utf-8 -*
#
# Copyright 2023 © AE UTBM
# ae@utbm.fr / ae.info@utbm.fr
#
# This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr.
#
# You can find the source code of the website at https://github.com/ae-utbm/sith3
#
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
# OR WITHIN THE LOCAL FILE "LICENSE"
#
#
from rest_framework import serializers
from rest_framework.response import Response
from rest_framework.decorators import action
from counter.models import Counter
from api.views import RightModelViewSet
class CounterSerializer(serializers.ModelSerializer):
is_open = serializers.BooleanField(read_only=True)
barman_list = serializers.ListField(
child=serializers.IntegerField(), read_only=True
)
class Meta:
model = Counter
fields = ("id", "name", "type", "club", "products", "is_open", "barman_list")
class CounterViewSet(RightModelViewSet):
"""
Manage Counters (api/v1/counter/)
"""
serializer_class = CounterSerializer
queryset = Counter.objects.all()
@action(detail=False)
def bar(self, request):
"""
Return all bars (api/v1/counter/bar/)
"""
self.queryset = self.queryset.filter(type="BAR")
serializer = self.get_serializer(self.queryset, many=True)
return Response(serializer.data)

View File

@ -1,35 +0,0 @@
# -*- coding:utf-8 -*
#
# Copyright 2023 © AE UTBM
# ae@utbm.fr / ae.info@utbm.fr
#
# This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr.
#
# You can find the source code of the website at https://github.com/ae-utbm/sith3
#
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
# OR WITHIN THE LOCAL FILE "LICENSE"
#
#
from rest_framework import serializers
from core.models import RealGroup
from api.views import RightModelViewSet
class GroupSerializer(serializers.ModelSerializer):
class Meta:
model = RealGroup
class GroupViewSet(RightModelViewSet):
"""
Manage Groups (api/v1/group/)
"""
serializer_class = GroupSerializer
queryset = RealGroup.objects.all()

View File

@ -1,128 +0,0 @@
# -*- coding:utf-8 -*
#
# Copyright 2023 © AE UTBM
# ae@utbm.fr / ae.info@utbm.fr
#
# This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr.
#
# You can find the source code of the website at https://github.com/ae-utbm/sith3
#
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
# OR WITHIN THE LOCAL FILE "LICENSE"
#
#
from rest_framework import serializers
from rest_framework.response import Response
from rest_framework.decorators import action
from launderette.models import Launderette, Machine, Token
from api.views import RightModelViewSet
class LaunderettePlaceSerializer(serializers.ModelSerializer):
machine_list = serializers.ListField(
child=serializers.IntegerField(), read_only=True
)
token_list = serializers.ListField(child=serializers.IntegerField(), read_only=True)
class Meta:
model = Launderette
fields = (
"id",
"name",
"counter",
"machine_list",
"token_list",
"get_absolute_url",
)
class LaunderetteMachineSerializer(serializers.ModelSerializer):
class Meta:
model = Machine
fields = ("id", "name", "type", "is_working", "launderette")
class LaunderetteTokenSerializer(serializers.ModelSerializer):
class Meta:
model = Token
fields = (
"id",
"name",
"type",
"launderette",
"borrow_date",
"user",
"is_avaliable",
)
class LaunderettePlaceViewSet(RightModelViewSet):
"""
Manage Launderette (api/v1/launderette/place/)
"""
serializer_class = LaunderettePlaceSerializer
queryset = Launderette.objects.all()
class LaunderetteMachineViewSet(RightModelViewSet):
"""
Manage Washing Machines (api/v1/launderette/machine/)
"""
serializer_class = LaunderetteMachineSerializer
queryset = Machine.objects.all()
class LaunderetteTokenViewSet(RightModelViewSet):
"""
Manage Launderette's tokens (api/v1/launderette/token/)
"""
serializer_class = LaunderetteTokenSerializer
queryset = Token.objects.all()
@action(detail=False)
def washing(self, request):
"""
Return all washing tokens (api/v1/launderette/token/washing)
"""
self.queryset = self.queryset.filter(type="WASHING")
serializer = self.get_serializer(self.queryset, many=True)
return Response(serializer.data)
@action(detail=False)
def drying(self, request):
"""
Return all drying tokens (api/v1/launderette/token/drying)
"""
self.queryset = self.queryset.filter(type="DRYING")
serializer = self.get_serializer(self.queryset, many=True)
return Response(serializer.data)
@action(detail=False)
def avaliable(self, request):
"""
Return all avaliable tokens (api/v1/launderette/token/avaliable)
"""
self.queryset = self.queryset.filter(
borrow_date__isnull=True, user__isnull=True
)
serializer = self.get_serializer(self.queryset, many=True)
return Response(serializer.data)
@action(detail=False)
def unavaliable(self, request):
"""
Return all unavaliable tokens (api/v1/launderette/token/unavaliable)
"""
self.queryset = self.queryset.filter(
borrow_date__isnull=False, user__isnull=False
)
serializer = self.get_serializer(self.queryset, many=True)
return Response(serializer.data)

View File

@ -1,42 +0,0 @@
from typing import List
from rest_framework.decorators import api_view, renderer_classes
from rest_framework.exceptions import PermissionDenied
from rest_framework.generics import get_object_or_404
from rest_framework.renderers import JSONRenderer
from rest_framework.request import Request
from rest_framework.response import Response
from core.views import can_edit
from core.models import User
from sas.models import Picture
def all_pictures_of_user(user: User) -> List[Picture]:
return [
relation.picture
for relation in user.pictures.exclude(picture=None)
.order_by("-picture__parent__date", "id")
.select_related("picture__parent")
]
@api_view(["GET"])
@renderer_classes((JSONRenderer,))
def all_pictures_of_user_endpoint(request: Request, user: int):
requested_user: User = get_object_or_404(User, pk=user)
if not can_edit(requested_user, request.user):
raise PermissionDenied
return Response(
[
{
"name": f"{picture.parent.name} - {picture.name}",
"date": picture.date,
"author": str(picture.owner),
"full_size_url": picture.get_download_url(),
"compressed_url": picture.get_download_compressed_url(),
"thumb_url": picture.get_download_thumb_url(),
}
for picture in all_pictures_of_user(requested_user)
]
)

View File

@ -1,60 +0,0 @@
# -*- coding:utf-8 -*
#
# Copyright 2023 © AE UTBM
# ae@utbm.fr / ae.info@utbm.fr
#
# This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr.
#
# You can find the source code of the website at https://github.com/ae-utbm/sith3
#
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
# OR WITHIN THE LOCAL FILE "LICENSE"
#
#
import datetime
from rest_framework import serializers
from rest_framework.response import Response
from rest_framework.decorators import action
from core.models import User
from api.views import RightModelViewSet
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = (
"id",
"first_name",
"last_name",
"email",
"date_of_birth",
"nick_name",
"is_active",
"date_joined",
)
class UserViewSet(RightModelViewSet):
"""
Manage Users (api/v1/user/)
Only show active users
"""
serializer_class = UserSerializer
queryset = User.objects.filter(is_active=True)
@action(detail=False)
def birthday(self, request):
"""
Return all users born today (api/v1/user/birstdays)
"""
date = datetime.datetime.today()
self.queryset = self.queryset.filter(date_of_birth=date)
serializer = self.get_serializer(self.queryset, many=True)
return Response(serializer.data)

View File

@ -1,127 +0,0 @@
from rest_framework.response import Response
from rest_framework.decorators import api_view, renderer_classes
from rest_framework.renderers import JSONRenderer
from django.core.exceptions import PermissionDenied
from django.conf import settings
from rest_framework import serializers
import urllib.request
import json
from pedagogy.views import CanCreateUVFunctionMixin
@api_view(["GET"])
@renderer_classes((JSONRenderer,))
def uv_endpoint(request):
if not CanCreateUVFunctionMixin.can_create_uv(request.user):
raise PermissionDenied
params = request.query_params
if "year" not in params or "code" not in params:
raise serializers.ValidationError("Missing query parameter")
short_uv, full_uv = find_uv("fr", params["year"], params["code"])
if short_uv is None or full_uv is None:
return Response(status=204)
return Response(make_clean_uv(short_uv, full_uv))
def find_uv(lang, year, code):
"""
Uses the UTBM API to find an UV.
short_uv is the UV entry in the UV list. It is returned as it contains
information which are not in full_uv.
full_uv is the detailed representation of an UV.
"""
# query the UV list
uvs_url = settings.SITH_PEDAGOGY_UTBM_API + "/uvs/{}/{}".format(lang, year)
response = urllib.request.urlopen(uvs_url)
uvs = json.loads(response.read().decode("utf-8"))
try:
# find the first UV which matches the code
short_uv = next(uv for uv in uvs if uv["code"] == code)
except StopIteration:
return (None, None)
# get detailed information about the UV
uv_url = settings.SITH_PEDAGOGY_UTBM_API + "/uv/{}/{}/{}/{}".format(
lang, year, code, short_uv["codeFormation"]
)
response = urllib.request.urlopen(uv_url)
full_uv = json.loads(response.read().decode("utf-8"))
return (short_uv, full_uv)
def make_clean_uv(short_uv, full_uv):
"""
Cleans the data up so that it corresponds to our data representation.
"""
res = {}
res["credit_type"] = short_uv["codeCategorie"]
# probably wrong on a few UVs as we pick the first UV we find but
# availability depends on the formation
semesters = {
(True, True): "AUTUMN_AND_SPRING",
(True, False): "AUTUMN",
(False, True): "SPRING",
}
res["semester"] = semesters.get(
(short_uv["ouvertAutomne"], short_uv["ouvertPrintemps"]), "CLOSED"
)
langs = {"es": "SP", "en": "EN", "de": "DE"}
res["language"] = langs.get(full_uv["codeLangue"], "FR")
if full_uv["departement"] == "Pôle Humanités":
res["department"] = "HUMA"
else:
departments = {
"AL": "IMSI",
"AE": "EE",
"GI": "GI",
"GC": "EE",
"GM": "MC",
"TC": "TC",
"GP": "IMSI",
"ED": "EDIM",
"AI": "GI",
"AM": "MC",
}
res["department"] = departments.get(full_uv["codeFormation"], "NA")
res["credits"] = full_uv["creditsEcts"]
activities = ("CM", "TD", "TP", "THE", "TE")
for activity in activities:
res["hours_{}".format(activity)] = 0
for activity in full_uv["activites"]:
if activity["code"] in activities:
res["hours_{}".format(activity["code"])] += activity["nbh"] // 60
# wrong if the manager changes depending on the semester
semester = full_uv.get("automne", None)
if not semester:
semester = full_uv.get("printemps", {})
res["manager"] = semester.get("responsable", "")
res["title"] = full_uv["libelle"]
descriptions = {
"objectives": "objectifs",
"program": "programme",
"skills": "acquisitionCompetences",
"key_concepts": "acquisitionNotions",
}
for res_key, full_uv_key in descriptions.items():
res[res_key] = full_uv[full_uv_key]
# if not found or the API did not return a string
if type(res[res_key]) != str:
res[res_key] = ""
return res

29
biome.json Normal file
View File

@ -0,0 +1,29 @@
{
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"ignoreUnknown": false,
"ignore": ["*.min.*", "staticfiles/generated"]
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"lineWidth": 88
},
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"all": true
}
},
"javascript": {
"globals": ["Alpine", "$", "jQuery", "gettext", "interpolate"]
}
}

View File

@ -1,4 +1,3 @@
# -*- coding:utf-8 -*
# #
# Copyright 2023 © AE UTBM # Copyright 2023 © AE UTBM
# ae@utbm.fr / ae.info@utbm.fr # ae@utbm.fr / ae.info@utbm.fr
@ -6,10 +5,10 @@
# This file is part of the website of the UTBM Student Association (AE UTBM), # This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr. # https://ae.utbm.fr.
# #
# You can find the source code of the website at https://github.com/ae-utbm/sith3 # You can find the source code of the website at https://github.com/ae-utbm/sith
# #
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3) # LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE # SEE : https://raw.githubusercontent.com/ae-utbm/sith/master/LICENSE
# OR WITHIN THE LOCAL FILE "LICENSE" # OR WITHIN THE LOCAL FILE "LICENSE"
# #
# #

View File

@ -1,4 +1,3 @@
# -*- coding:utf-8 -*
# #
# Copyright 2023 © AE UTBM # Copyright 2023 © AE UTBM
# ae@utbm.fr / ae.info@utbm.fr # ae@utbm.fr / ae.info@utbm.fr
@ -6,14 +5,13 @@
# This file is part of the website of the UTBM Student Association (AE UTBM), # This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr. # https://ae.utbm.fr.
# #
# You can find the source code of the website at https://github.com/ae-utbm/sith3 # You can find the source code of the website at https://github.com/ae-utbm/sith
# #
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3) # LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE # SEE : https://raw.githubusercontent.com/ae-utbm/sith/master/LICENSE
# OR WITHIN THE LOCAL FILE "LICENSE" # OR WITHIN THE LOCAL FILE "LICENSE"
# #
# #
from ajax_select import make_ajax_form
from django.contrib import admin from django.contrib import admin
from club.models import Club, Membership from club.models import Club, Membership
@ -22,6 +20,14 @@ from club.models import Club, Membership
@admin.register(Club) @admin.register(Club)
class ClubAdmin(admin.ModelAdmin): class ClubAdmin(admin.ModelAdmin):
list_display = ("name", "unix_name", "parent", "is_active") list_display = ("name", "unix_name", "parent", "is_active")
search_fields = ("name", "unix_name")
autocomplete_fields = (
"parent",
"board_group",
"members_group",
"home",
"page",
)
@admin.register(Membership) @admin.register(Membership)
@ -33,4 +39,4 @@ class MembershipAdmin(admin.ModelAdmin):
"user__last_name", "user__last_name",
"club__name", "club__name",
) )
form = make_ajax_form(Membership, {"user": "users"}) autocomplete_fields = ("user",)

22
club/api.py Normal file
View File

@ -0,0 +1,22 @@
from typing import Annotated
from annotated_types import MinLen
from ninja_extra import ControllerBase, api_controller, paginate, route
from ninja_extra.pagination import PageNumberPaginationExtra
from ninja_extra.schemas import PaginatedResponseSchema
from club.models import Club
from club.schemas import ClubSchema
from core.api_permissions import CanAccessLookup
@api_controller("/club")
class ClubController(ControllerBase):
@route.get(
"/search",
response=PaginatedResponseSchema[ClubSchema],
permissions=[CanAccessLookup],
)
@paginate(PageNumberPaginationExtra, page_size=50)
def search_club(self, search: Annotated[str, MinLen(1)]):
return Club.objects.filter(name__icontains=search).values()

View File

@ -1,4 +1,3 @@
# -*- coding:utf-8 -*
# #
# Copyright 2016,2017 # Copyright 2016,2017
# - Skia <skia@libskia.so> # - Skia <skia@libskia.so>
@ -23,18 +22,15 @@
# #
# #
from django.conf import settings
from django import forms from django import forms
from django.conf import settings
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from ajax_select.fields import AutoCompleteSelectField, AutoCompleteSelectMultipleField from club.models import Club, Mailing, MailingSubscription, Membership
from club.models import Mailing, MailingSubscription, Club, Membership
from core.models import User from core.models import User
from core.views.forms import SelectDate, SelectDateTime from core.views.forms import SelectDate, SelectDateTime
from core.views.widgets.select import AutoCompleteSelectMultipleUser
from counter.models import Counter from counter.models import Counter
from core.views.forms import TzAwareDateTimeField
class ClubEditForm(forms.ModelForm): class ClubEditForm(forms.ModelForm):
@ -43,28 +39,27 @@ class ClubEditForm(forms.ModelForm):
fields = ["address", "logo", "short_description"] fields = ["address", "logo", "short_description"]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(ClubEditForm, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields["short_description"].widget = forms.Textarea() self.fields["short_description"].widget = forms.Textarea()
class MailingForm(forms.Form): class MailingForm(forms.Form):
""" """Form handling mailing lists right."""
Form handling mailing lists right
"""
ACTION_NEW_MAILING = 1 ACTION_NEW_MAILING = 1
ACTION_NEW_SUBSCRIPTION = 2 ACTION_NEW_SUBSCRIPTION = 2
ACTION_REMOVE_SUBSCRIPTION = 3 ACTION_REMOVE_SUBSCRIPTION = 3
subscription_users = AutoCompleteSelectMultipleField( subscription_users = forms.ModelMultipleChoiceField(
"users",
label=_("Users to add"), label=_("Users to add"),
help_text=_("Search users to add (one or more)."), help_text=_("Search users to add (one or more)."),
required=False, required=False,
widget=AutoCompleteSelectMultipleUser,
queryset=User.objects.all(),
) )
def __init__(self, club_id, user_id, mailings, *args, **kwargs): def __init__(self, club_id, user_id, mailings, *args, **kwargs):
super(MailingForm, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields["action"] = forms.TypedChoiceField( self.fields["action"] = forms.TypedChoiceField(
choices=( choices=(
@ -109,24 +104,15 @@ class MailingForm(forms.Form):
) )
def check_required(self, cleaned_data, field): def check_required(self, cleaned_data, field):
""" """If the given field doesn't exist or has no value, add a required error on it."""
If the given field doesn't exist or has no value, add a required error on it
"""
if not cleaned_data.get(field, None): if not cleaned_data.get(field, None):
self.add_error(field, _("This field is required")) self.add_error(field, _("This field is required"))
def clean_subscription_users(self): def clean_subscription_users(self):
""" """Convert given users into real users and check their validity."""
Convert given users into real users and check their validity cleaned_data = super().clean()
"""
cleaned_data = super(MailingForm, self).clean()
users = [] users = []
for user in cleaned_data["subscription_users"]: for user in cleaned_data["subscription_users"]:
user = User.objects.filter(id=user).first()
if not user:
raise forms.ValidationError(
_("One of the selected users doesn't exist"), code="invalid"
)
if not user.email: if not user.email:
raise forms.ValidationError( raise forms.ValidationError(
_("One of the selected users doesn't have an email address"), _("One of the selected users doesn't have an email address"),
@ -136,9 +122,9 @@ class MailingForm(forms.Form):
return users return users
def clean(self): def clean(self):
cleaned_data = super(MailingForm, self).clean() cleaned_data = super().clean()
if not "action" in cleaned_data: if "action" not in cleaned_data:
# If there is no action provided, we can stop here # If there is no action provided, we can stop here
raise forms.ValidationError(_("An action is required"), code="invalid") raise forms.ValidationError(_("An action is required"), code="invalid")
@ -159,15 +145,19 @@ class MailingForm(forms.Form):
class SellingsForm(forms.Form): class SellingsForm(forms.Form):
begin_date = TzAwareDateTimeField(label=_("Begin date"), required=False) begin_date = forms.DateTimeField(
end_date = TzAwareDateTimeField(label=_("End date"), required=False) label=_("Begin date"), widget=SelectDateTime, required=False
)
end_date = forms.DateTimeField(
label=_("End date"), widget=SelectDateTime, required=False
)
counters = forms.ModelMultipleChoiceField( counters = forms.ModelMultipleChoiceField(
Counter.objects.order_by("name").all(), label=_("Counter"), required=False Counter.objects.order_by("name").all(), label=_("Counter"), required=False
) )
def __init__(self, club, *args, **kwargs): def __init__(self, club, *args, **kwargs):
super(SellingsForm, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields["products"] = forms.ModelMultipleChoiceField( self.fields["products"] = forms.ModelMultipleChoiceField(
club.products.order_by("name").filter(archived=False).all(), club.products.order_by("name").filter(archived=False).all(),
label=_("Products"), label=_("Products"),
@ -181,18 +171,17 @@ class SellingsForm(forms.Form):
class ClubMemberForm(forms.Form): class ClubMemberForm(forms.Form):
""" """Form handling the members of a club."""
Form handling the members of a club
"""
error_css_class = "error" error_css_class = "error"
required_css_class = "required" required_css_class = "required"
users = AutoCompleteSelectMultipleField( users = forms.ModelMultipleChoiceField(
"users",
label=_("Users to add"), label=_("Users to add"),
help_text=_("Search users to add (one or more)."), help_text=_("Search users to add (one or more)."),
required=False, required=False,
widget=AutoCompleteSelectMultipleUser,
queryset=User.objects.all(),
) )
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -204,7 +193,7 @@ class ClubMemberForm(forms.Form):
self.club.members.filter(end_date=None).order_by("-role").all() self.club.members.filter(end_date=None).order_by("-role").all()
) )
self.request_user_membership = self.club.get_membership_for(self.request_user) self.request_user_membership = self.club.get_membership_for(self.request_user)
super(ClubMemberForm, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# Using a ModelForm binds too much the form with the model and we don't want that # Using a ModelForm binds too much the form with the model and we don't want that
# We want the view to process the model creation since they are multiple users # We want the view to process the model creation since they are multiple users
@ -240,18 +229,13 @@ class ClubMemberForm(forms.Form):
self.fields.pop("start_date") self.fields.pop("start_date")
def clean_users(self): def clean_users(self):
"""Check that the user is not trying to add an user already in the club.
Also check that the user is valid and has a valid subscription.
""" """
Check that the user is not trying to add an user already in the club cleaned_data = super().clean()
Also check that the user is valid and has a valid subscription
"""
cleaned_data = super(ClubMemberForm, self).clean()
users = [] users = []
for user_id in cleaned_data["users"]: for user in cleaned_data["users"]:
user = User.objects.filter(id=user_id).first()
if not user:
raise forms.ValidationError(
_("One of the selected users doesn't exist"), code="invalid"
)
if not user.is_subscribed: if not user.is_subscribed:
raise forms.ValidationError( raise forms.ValidationError(
_("User must be subscriber to take part to a club"), code="invalid" _("User must be subscriber to take part to a club"), code="invalid"
@ -264,10 +248,8 @@ class ClubMemberForm(forms.Form):
return users return users
def clean(self): def clean(self):
""" """Check user rights for adding an user."""
Check user rights for adding an user cleaned_data = super().clean()
"""
cleaned_data = super(ClubMemberForm, self).clean()
if "start_date" in cleaned_data and not cleaned_data["start_date"]: if "start_date" in cleaned_data and not cleaned_data["start_date"]:
# Drop start_date if allowed to edition but not specified # Drop start_date if allowed to edition but not specified

View File

@ -1,9 +1,8 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import migrations, models
import django.core.validators import django.core.validators
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@ -1,9 +1,8 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import migrations, models
from django.conf import settings
import django.db.models.deletion import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import migrations, models from django.db import migrations, models

View File

@ -1,9 +1,8 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import migrations, models
from django.conf import settings
import django.db.models.deletion import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@ -1,8 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@ -1,8 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import migrations, models
import django.utils.timezone import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import migrations, models from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import migrations, models from django.db import migrations, models

View File

@ -1,11 +1,11 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import migrations, models
from django.conf import settings
import re import re
import django.core.validators import django.core.validators
import django.db.models.deletion import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
@ -109,6 +109,6 @@ class Migration(migrations.Migration):
), ),
migrations.AlterUniqueTogether( migrations.AlterUniqueTogether(
name="mailingsubscription", name="mailingsubscription",
unique_together=set([("user", "email", "mailing")]), unique_together={("user", "email", "mailing")},
), ),
] ]

View File

@ -1,21 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import migrations, models
from club.models import Club
from core.operations import PsqlRunOnly
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models
def generate_club_pages(apps, schema_editor):
def recursive_generate_club_page(club):
club.make_page()
for child in Club.objects.filter(parent=club).all():
recursive_generate_club_page(child)
for club in Club.objects.filter(parent=None).all():
recursive_generate_club_page(club)
class Migration(migrations.Migration): class Migration(migrations.Migration):
@ -49,11 +35,4 @@ class Migration(migrations.Migration):
null=True, null=True,
), ),
), ),
PsqlRunOnly(
"SET CONSTRAINTS ALL IMMEDIATE", reverse_sql=migrations.RunSQL.noop
),
migrations.RunPython(generate_club_pages),
PsqlRunOnly(
migrations.RunSQL.noop, reverse_sql="SET CONSTRAINTS ALL IMMEDIATE"
),
] ]

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import migrations, models from django.db import migrations, models

View File

@ -1,9 +1,9 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import migrations, models
import club.models
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models
import club.models
class Migration(migrations.Migration): class Migration(migrations.Migration):
@ -15,7 +15,7 @@ class Migration(migrations.Migration):
name="owner_group", name="owner_group",
field=models.ForeignKey( field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, on_delete=django.db.models.deletion.CASCADE,
default=club.models.Club.get_default_owner_group, default=club.models.get_default_owner_group,
related_name="owned_club", related_name="owned_club",
to="core.Group", to="core.Group",
), ),

View File

@ -0,0 +1,106 @@
# Generated by Django 4.2.16 on 2024-11-20 17:08
import django.db.models.deletion
import django.db.models.functions.datetime
from django.conf import settings
from django.db import migrations, models
from django.db.migrations.state import StateApps
from django.db.models import Q
from django.utils.timezone import localdate
def migrate_meta_groups(apps: StateApps, schema_editor):
"""Attach the existing meta groups to the clubs.
Until now, the meta groups were not attached to the clubs,
nor to the users.
This creates actual foreign relationships between the clubs
and theirs groups and the users and theirs groups.
Warnings:
When the meta groups associated with the clubs aren't found,
they are created.
Thus the migration shouldn't fail, and all the clubs will
have their groups.
However, there will probably be some groups that have
not been found but exist nonetheless,
so there will be duplicates and dangling groups.
There must be a manual cleanup after this migration.
"""
Group = apps.get_model("core", "Group")
Club = apps.get_model("club", "Club")
meta_groups = Group.objects.filter(is_meta=True)
clubs = list(Club.objects.all())
for club in clubs:
club.board_group = meta_groups.get_or_create(
name=club.unix_name + settings.SITH_BOARD_SUFFIX,
defaults={"is_meta": True},
)[0]
club.members_group = meta_groups.get_or_create(
name=club.unix_name + settings.SITH_MEMBER_SUFFIX,
defaults={"is_meta": True},
)[0]
club.save()
club.refresh_from_db()
memberships = club.members.filter(
Q(end_date=None) | Q(end_date__gt=localdate())
).select_related("user")
club.members_group.users.set([m.user for m in memberships])
club.board_group.users.set(
[
m.user
for m in memberships.filter(role__gt=settings.SITH_MAXIMUM_FREE_ROLE)
]
)
# steps of the migration :
# - Create a nullable field for the board group and the member group
# - Edit those new fields to make them point to currently existing meta groups
# - When this data migration is done, make the fields non-nullable
class Migration(migrations.Migration):
dependencies = [
("core", "0040_alter_user_options_user_user_permissions_and_more"),
("club", "0011_auto_20180426_2013"),
]
operations = [
migrations.RemoveField(
model_name="club",
name="edit_groups",
),
migrations.RemoveField(
model_name="club",
name="owner_group",
),
migrations.RemoveField(
model_name="club",
name="view_groups",
),
migrations.AddField(
model_name="club",
name="board_group",
field=models.OneToOneField(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="club_board",
to="core.group",
),
),
migrations.AddField(
model_name="club",
name="members_group",
field=models.OneToOneField(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="club",
to="core.group",
),
),
migrations.RunPython(
migrate_meta_groups, reverse_code=migrations.RunPython.noop, elidable=True
),
]

View File

@ -0,0 +1,36 @@
# Generated by Django 4.2.17 on 2025-01-04 16:46
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("club", "0012_club_board_group_club_members_group")]
operations = [
migrations.AlterField(
model_name="club",
name="board_group",
field=models.OneToOneField(
on_delete=django.db.models.deletion.PROTECT,
related_name="club_board",
to="core.group",
),
),
migrations.AlterField(
model_name="club",
name="members_group",
field=models.OneToOneField(
on_delete=django.db.models.deletion.PROTECT,
related_name="club",
to="core.group",
),
),
migrations.AddConstraint(
model_name="membership",
constraint=models.CheckConstraint(
check=models.Q(("end_date__gte", models.F("start_date"))),
name="end_after_start",
),
),
]

View File

@ -1,4 +1,3 @@
# -*- coding:utf-8 -*
# #
# Copyright 2016,2017 # Copyright 2016,2017
# - Skia <skia@libskia.so> # - Skia <skia@libskia.so>
@ -22,31 +21,35 @@
# Place - Suite 330, Boston, MA 02111-1307, USA. # Place - Suite 330, Boston, MA 02111-1307, USA.
# #
# #
from typing import Optional from __future__ import annotations
from typing import Iterable, Self
from django.core.cache import cache
from django.db import models
from django.core import validators
from django.conf import settings from django.conf import settings
from django.db.models import Q from django.core import validators
from django.utils.timezone import now from django.core.cache import cache
from django.utils.translation import gettext_lazy as _ from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.exceptions import ValidationError, ObjectDoesNotExist from django.core.validators import RegexValidator, validate_email
from django.db import transaction from django.db import models, transaction
from django.db.models import Exists, F, OuterRef, Q
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from django.core.validators import RegexValidator, validate_email
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.timezone import localdate
from django.utils.translation import gettext_lazy as _
from core.models import User, MetaGroup, Group, SithFile, RealGroup, Notification, Page from core.models import Group, Notification, Page, SithFile, User
# Create your models here. # Create your models here.
# This function prevents generating migration upon settings change
def get_default_owner_group():
return settings.SITH_GROUP_ROOT_ID
class Club(models.Model): class Club(models.Model):
""" """The Club class, made as a tree to allow nice tidy organization."""
The Club class, made as a tree to allow nice tidy organization
"""
id = models.AutoField(primary_key=True, db_index=True) id = models.AutoField(primary_key=True, db_index=True)
name = models.CharField(_("name"), max_length=64) name = models.CharField(_("name"), max_length=64)
@ -76,23 +79,6 @@ class Club(models.Model):
_("short description"), max_length=1000, default="", blank=True, null=True _("short description"), max_length=1000, default="", blank=True, null=True
) )
address = models.CharField(_("address"), max_length=254) address = models.CharField(_("address"), max_length=254)
# This function prevents generating migration upon settings change
def get_default_owner_group():
return settings.SITH_GROUP_ROOT_ID
owner_group = models.ForeignKey(
Group,
related_name="owned_club",
default=get_default_owner_group,
on_delete=models.CASCADE,
)
edit_groups = models.ManyToManyField(
Group, related_name="editable_club", blank=True
)
view_groups = models.ManyToManyField(
Group, related_name="viewable_club", blank=True
)
home = models.OneToOneField( home = models.OneToOneField(
SithFile, SithFile,
related_name="home_of_club", related_name="home_of_club",
@ -104,18 +90,57 @@ class Club(models.Model):
page = models.OneToOneField( page = models.OneToOneField(
Page, related_name="club", blank=True, null=True, on_delete=models.CASCADE Page, related_name="club", blank=True, null=True, on_delete=models.CASCADE
) )
members_group = models.OneToOneField(
Group, related_name="club", on_delete=models.PROTECT
)
board_group = models.OneToOneField(
Group, related_name="club_board", on_delete=models.PROTECT
)
class Meta: class Meta:
ordering = ["name", "unix_name"] ordering = ["name", "unix_name"]
def __str__(self):
return self.name
@transaction.atomic()
def save(self, *args, **kwargs):
creation = self._state.adding
if not creation:
db_club = Club.objects.get(id=self.id)
if self.unix_name != db_club.unix_name:
self.home.name = self.unix_name
self.home.save()
if self.name != db_club.name:
self.board_group.name = f"{self.name} - Bureau"
self.board_group.save()
self.members_group.name = f"{self.name} - Membres"
self.members_group.save()
if creation:
self.board_group = Group.objects.create(
name=f"{self.name} - Bureau", is_manually_manageable=False
)
self.members_group = Group.objects.create(
name=f"{self.name} - Membres", is_manually_manageable=False
)
super().save(*args, **kwargs)
if creation:
self.make_home()
self.make_page()
cache.set(f"sith_club_{self.unix_name}", self)
def get_absolute_url(self):
return reverse("club:club_view", kwargs={"club_id": self.id})
@cached_property @cached_property
def president(self): def president(self) -> Membership | None:
"""Fetch the membership of the current president of this club."""
return self.members.filter( return self.members.filter(
role=settings.SITH_CLUB_ROLES_ID["President"], end_date=None role=settings.SITH_CLUB_ROLES_ID["President"], end_date=None
).first() ).first()
def check_loop(self): def check_loop(self):
"""Raise a validation error when a loop is found within the parent list""" """Raise a validation error when a loop is found within the parent list."""
objs = [] objs = []
cur = self cur = self
while cur.parent is not None: while cur.parent is not None:
@ -127,36 +152,18 @@ class Club(models.Model):
def clean(self): def clean(self):
self.check_loop() self.check_loop()
def _change_unixname(self, old_name, new_name): def make_home(self) -> None:
c = Club.objects.filter(unix_name=new_name).first() if self.home:
if c is None: return
# Update all the groups names home_root = SithFile.objects.filter(parent=None, name="clubs").first()
Group.objects.filter(name=old_name).update(name=new_name) root = User.objects.filter(username="root").first()
Group.objects.filter(name=old_name + settings.SITH_BOARD_SUFFIX).update( if home_root and root:
name=new_name + settings.SITH_BOARD_SUFFIX home = SithFile(parent=home_root, name=self.unix_name, owner=root)
) home.save()
Group.objects.filter(name=old_name + settings.SITH_MEMBER_SUFFIX).update( self.home = home
name=new_name + settings.SITH_MEMBER_SUFFIX self.save()
)
if self.home: def make_page(self) -> None:
self.home.name = new_name
self.home.save()
else:
raise ValidationError(_("A club with that unix_name already exists"))
def make_home(self):
if not self.home:
home_root = SithFile.objects.filter(parent=None, name="clubs").first()
root = User.objects.filter(username="root").first()
if home_root and root:
home = SithFile(parent=home_root, name=self.unix_name, owner=root)
home.save()
self.home = home
self.save()
def make_page(self):
root = User.objects.filter(username="root").first() root = User.objects.filter(username="root").first()
if not self.page: if not self.page:
club_root = Page.objects.filter(name=settings.SITH_CLUB_ROOT_PAGE).first() club_root = Page.objects.filter(name=settings.SITH_CLUB_ROOT_PAGE).first()
@ -186,74 +193,40 @@ class Club(models.Model):
self.page.parent = self.parent.page self.page.parent = self.parent.page
self.page.save(force_lock=True) self.page.save(force_lock=True)
@transaction.atomic() def delete(self, *args, **kwargs) -> tuple[int, dict[str, int]]:
def save(self, *args, **kwargs):
old = Club.objects.filter(id=self.id).first()
creation = old is None
if not creation and old.unix_name != self.unix_name:
self._change_unixname(self.unix_name)
super(Club, self).save(*args, **kwargs)
if creation:
board = MetaGroup(name=self.unix_name + settings.SITH_BOARD_SUFFIX)
board.save()
member = MetaGroup(name=self.unix_name + settings.SITH_MEMBER_SUFFIX)
member.save()
subscribers = Group.objects.filter(
name=settings.SITH_MAIN_MEMBERS_GROUP
).first()
self.make_home()
self.home.edit_groups.set([board])
self.home.view_groups.set([member, subscribers])
self.home.save()
self.make_page()
cache.set(f"sith_club_{self.unix_name}", self)
def delete(self, *args, **kwargs):
super().delete(*args, **kwargs)
# Invalidate the cache of this club and of its memberships # Invalidate the cache of this club and of its memberships
for membership in self.members.ongoing().select_related("user"): for membership in self.members.ongoing().select_related("user"):
cache.delete(f"membership_{self.id}_{membership.user.id}") cache.delete(f"membership_{self.id}_{membership.user.id}")
cache.delete(f"sith_club_{self.unix_name}") cache.delete(f"sith_club_{self.unix_name}")
self.board_group.delete()
self.members_group.delete()
return super().delete(*args, **kwargs)
def __str__(self): def get_display_name(self) -> str:
return self.name return self.name
def get_absolute_url(self): def is_owned_by(self, user: User) -> bool:
return reverse("club:club_view", kwargs={"club_id": self.id}) """Method to see if that object can be super edited by the given user."""
def get_display_name(self):
return self.name
def is_owned_by(self, user):
"""
Method to see if that object can be super edited by the given user
"""
if user.is_anonymous: if user.is_anonymous:
return False return False
return user.is_board_member return user.is_root or user.is_board_member
def get_full_logo_url(self): def get_full_logo_url(self) -> str:
return "https://%s%s" % (settings.SITH_URL, self.logo.url) return f"https://{settings.SITH_URL}{self.logo.url}"
def can_be_edited_by(self, user): def can_be_edited_by(self, user: User) -> bool:
""" """Method to see if that object can be edited by the given user."""
Method to see if that object can be edited by the given user
"""
return self.has_rights_in_club(user) return self.has_rights_in_club(user)
def can_be_viewed_by(self, user): def can_be_viewed_by(self, user: User) -> bool:
""" """Method to see if that object can be seen by the given user."""
Method to see if that object can be seen by the given user return user.was_subscribed
"""
sub = User.objects.filter(pk=user.pk).first()
if sub is None:
return False
return sub.was_subscribed
def get_membership_for(self, user: User) -> Optional["Membership"]: def get_membership_for(self, user: User) -> Membership | None:
""" """Return the current membership the given user.
Return the current membership the given user.
The result is cached. Note:
The result is cached.
""" """
if user.is_anonymous: if user.is_anonymous:
return None return None
@ -268,22 +241,17 @@ class Club(models.Model):
cache.set(f"membership_{self.id}_{user.id}", membership) cache.set(f"membership_{self.id}_{user.id}", membership)
return membership return membership
def has_rights_in_club(self, user): def has_rights_in_club(self, user: User) -> bool:
m = self.get_membership_for(user) return user.is_in_group(pk=self.board_group_id)
return m is not None and m.role > settings.SITH_MAXIMUM_FREE_ROLE
class MembershipQuerySet(models.QuerySet): class MembershipQuerySet(models.QuerySet):
def ongoing(self) -> "MembershipQuerySet": def ongoing(self) -> Self:
""" """Filter all memberships which are not finished yet."""
Filter all memberships which are not finished yet return self.filter(Q(end_date=None) | Q(end_date__gt=localdate()))
"""
# noinspection PyTypeChecker
return self.filter(Q(end_date=None) | Q(end_date__gte=timezone.now()))
def board(self) -> "MembershipQuerySet": def board(self) -> Self:
""" """Filter all memberships where the user is/was in the board.
Filter all memberships where the user is/was in the board.
Be aware that users who were in the board in the past Be aware that users who were in the board in the past
are included, even if there are no more members. are included, even if there are no more members.
@ -291,51 +259,71 @@ class MembershipQuerySet(models.QuerySet):
If you want to get the users who are currently in the board, If you want to get the users who are currently in the board,
mind combining this with the :meth:`ongoing` queryset method mind combining this with the :meth:`ongoing` queryset method
""" """
# noinspection PyTypeChecker
return self.filter(role__gt=settings.SITH_MAXIMUM_FREE_ROLE) return self.filter(role__gt=settings.SITH_MAXIMUM_FREE_ROLE)
def update(self, **kwargs): def update(self, **kwargs) -> int:
""" """Refresh the cache and edit group ownership.
Work just like the default Django's update() method,
but add a cache refresh for the elements of the queryset.
Be aware that this adds a db query to retrieve the updated objects Update the cache, when necessary, remove
users from club groups they are no more in
and add them in the club groups they should be in.
Be aware that this adds three db queries :
one to retrieve the updated memberships,
one to perform group removal and one to perform
group attribution.
""" """
nb_rows = super().update(**kwargs) nb_rows = super().update(**kwargs)
if nb_rows > 0: if nb_rows == 0:
# if at least a row was affected, refresh the cache # if no row was affected, no need to refresh the cache
for membership in self.all(): return 0
if membership.end_date is not None:
cache.set(
f"membership_{membership.club_id}_{membership.user_id}",
"not_member",
)
else:
cache.set(
f"membership_{membership.club_id}_{membership.user_id}",
membership,
)
def delete(self): cache_memberships = {}
""" memberships = set(self.select_related("club"))
Work just like the default Django's delete() method, # delete all User-Group relations and recreate the necessary ones
# It's more concise to write and more reliable
Membership._remove_club_groups(memberships)
Membership._add_club_groups(memberships)
for member in memberships:
cache_key = f"membership_{member.club_id}_{member.user_id}"
if member.end_date is None:
cache_memberships[cache_key] = member
else:
cache_memberships[cache_key] = "not_member"
cache.set_many(cache_memberships)
return nb_rows
def delete(self) -> tuple[int, dict[str, int]]:
"""Work just like the default Django's delete() method,
but add a cache invalidation for the elements of the queryset but add a cache invalidation for the elements of the queryset
before the deletion. before the deletion,
and a removal of the user from the club groups.
Be aware that this adds a db query to retrieve the deleted element. Be aware that this adds some db queries :
As this first query take place before the deletion operation,
it will be performed even if the deletion fails. - 1 to retrieve the deleted elements in order to perform
post-delete operations.
As we can't know if a delete will affect rows or not,
this query will always happen
- 1 query to remove the users from the club groups.
If the delete operation affected no row,
this query won't happen.
""" """
ids = list(self.values_list("club_id", "user_id")) memberships = set(self.all())
nb_rows, _ = super().delete() nb_rows, rows_counts = super().delete()
if nb_rows > 0: if nb_rows > 0:
for club_id, user_id in ids: Membership._remove_club_groups(memberships)
cache.set(f"membership_{club_id}_{user_id}", "not_member") cache.set_many(
{
f"membership_{m.club_id}_{m.user_id}": "not_member"
for m in memberships
}
)
return nb_rows, rows_counts
class Membership(models.Model): class Membership(models.Model):
""" """The Membership class makes the connection between User and Clubs.
The Membership class makes the connection between User and Clubs
Both Users and Clubs can have many Membership objects: Both Users and Clubs can have many Membership objects:
- a user can be a member of many clubs at a time - a user can be a member of many clubs at a time
@ -374,54 +362,142 @@ class Membership(models.Model):
objects = MembershipQuerySet.as_manager() objects = MembershipQuerySet.as_manager()
class Meta:
constraints = [
models.CheckConstraint(
check=Q(end_date__gte=F("start_date")), name="end_after_start"
),
]
def __str__(self): def __str__(self):
return ( return (
self.club.name f"{self.club.name} - {self.user.username} "
+ " - " f"- {settings.SITH_CLUB_ROLES[self.role]} "
+ self.user.username f"- {str(_('past member')) if self.end_date is not None else ''}"
+ " - "
+ str(settings.SITH_CLUB_ROLES[self.role])
+ str(" - " + str(_("past member")) if self.end_date is not None else "")
) )
def is_owned_by(self, user):
"""
Method to see if that object can be super edited by the given user
"""
if user.is_anonymous:
return False
return user.is_board_member
def can_be_edited_by(self, user: User) -> bool:
"""
Check if that object can be edited by the given user
"""
if user.is_root or user.is_board_member:
return True
membership = self.club.get_membership_for(user)
if membership is not None and membership.role >= self.role:
return True
return False
def get_absolute_url(self):
return reverse("club:club_members", kwargs={"club_id": self.club_id})
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
super().save(*args, **kwargs) super().save(*args, **kwargs)
# a save may either be an update or a creation
# and may result in either an ongoing or an ended membership.
# It could also be a retrogradation from the board to being a simple member.
# To avoid problems, the user is removed from the club groups beforehand ;
# he will be added back if necessary
self._remove_club_groups([self])
if self.end_date is None: if self.end_date is None:
self._add_club_groups([self])
cache.set(f"membership_{self.club_id}_{self.user_id}", self) cache.set(f"membership_{self.club_id}_{self.user_id}", self)
else: else:
cache.set(f"membership_{self.club_id}_{self.user_id}", "not_member") cache.set(f"membership_{self.club_id}_{self.user_id}", "not_member")
def get_absolute_url(self):
return reverse("club:club_members", kwargs={"club_id": self.club_id})
def is_owned_by(self, user: User) -> bool:
"""Method to see if that object can be super edited by the given user."""
if user.is_anonymous:
return False
return user.is_root or user.is_board_member
def can_be_edited_by(self, user: User) -> bool:
"""Check if that object can be edited by the given user."""
if user.is_root or user.is_board_member:
return True
membership = self.club.get_membership_for(user)
return membership is not None and membership.role >= self.role
def delete(self, *args, **kwargs): def delete(self, *args, **kwargs):
self._remove_club_groups([self])
super().delete(*args, **kwargs) super().delete(*args, **kwargs)
cache.delete(f"membership_{self.club_id}_{self.user_id}") cache.delete(f"membership_{self.club_id}_{self.user_id}")
@staticmethod
def _remove_club_groups(
memberships: Iterable[Membership],
) -> tuple[int, dict[str, int]]:
"""Remove users of those memberships from the club groups.
For example, if a user is in the Troll club board,
he is in the board group and the members group of the Troll.
After calling this function, he will be in neither.
Returns:
The result of the deletion queryset.
Warnings:
If this function isn't used in combination
with an actual deletion of the memberships,
it will result in an inconsistent state,
where users will be in the clubs, without
having the associated rights.
"""
clubs = {m.club_id for m in memberships}
users = {m.user_id for m in memberships}
groups = Group.objects.filter(Q(club__in=clubs) | Q(club_board__in=clubs))
return User.groups.through.objects.filter(
Q(group__in=groups) & Q(user__in=users)
).delete()
@staticmethod
def _add_club_groups(
memberships: Iterable[Membership],
) -> list[User.groups.through]:
"""Add users of those memberships to the club groups.
For example, if a user just joined the Troll club board,
he will be added in both the members group and the board group
of the club.
Returns:
The created User-Group relations.
Warnings:
If this function isn't used in combination
with an actual update/creation of the memberships,
it will result in an inconsistent state,
where users will have the rights associated to the
club, without actually being part of it.
"""
# only active membership (i.e. `end_date=None`)
# grant the attribution of club groups.
memberships = [m for m in memberships if m.end_date is None]
if not memberships:
return []
if sum(1 for m in memberships if not hasattr(m, "club")) > 1:
# if more than one membership hasn't its `club` attribute set
# it's less expensive to reload the whole query with
# a select_related than perform a distinct query
# to fetch each club.
ids = {m.id for m in memberships}
memberships = list(
Membership.objects.filter(id__in=ids).select_related("club")
)
club_groups = []
for membership in memberships:
club_groups.append(
User.groups.through(
user_id=membership.user_id,
group_id=membership.club.members_group_id,
)
)
if membership.role > settings.SITH_MAXIMUM_FREE_ROLE:
club_groups.append(
User.groups.through(
user_id=membership.user_id,
group_id=membership.club.board_group_id,
)
)
return User.groups.through.objects.bulk_create(
club_groups, ignore_conflicts=True
)
class Mailing(models.Model): class Mailing(models.Model):
""" """A Mailing list for a club.
This class correspond to a mailing list
Remember that mailing lists should be validated by UTBM Warning:
Remember that mailing lists should be validated by UTBM.
""" """
club = models.ForeignKey( club = models.ForeignKey(
@ -454,6 +530,25 @@ class Mailing(models.Model):
on_delete=models.CASCADE, on_delete=models.CASCADE,
) )
def __str__(self):
return "%s - %s" % (self.club, self.email_full)
def save(self, *args, **kwargs):
if not self.is_moderated:
unread_notif_subquery = Notification.objects.filter(
user=OuterRef("pk"), type="MAILING_MODERATION", viewed=False
)
for user in User.objects.filter(
~Exists(unread_notif_subquery),
groups__id__in=[settings.SITH_GROUP_COM_ADMIN_ID],
):
Notification(
user=user,
url=reverse("com:mailing_admin"),
type="MAILING_MODERATION",
).save(*args, **kwargs)
super().save(*args, **kwargs)
def clean(self): def clean(self):
if Mailing.objects.filter(email=self.email).exists(): if Mailing.objects.filter(email=self.email).exists():
raise ValidationError(_("This mailing list already exists.")) raise ValidationError(_("This mailing list already exists."))
@ -461,7 +556,7 @@ class Mailing(models.Model):
self.is_moderated = True self.is_moderated = True
else: else:
self.moderator = None self.moderator = None
super(Mailing, self).clean() super().clean()
@property @property
def email_full(self): def email_full(self):
@ -483,39 +578,15 @@ class Mailing(models.Model):
def delete(self, *args, **kwargs): def delete(self, *args, **kwargs):
self.subscriptions.all().delete() self.subscriptions.all().delete()
super(Mailing, self).delete() super().delete()
def fetch_format(self): def fetch_format(self):
resp = self.email + ": " destination = "".join(s.fetch_format() for s in self.subscriptions.all())
for sub in self.subscriptions.all(): return f"{self.email}: {destination}"
resp += sub.fetch_format()
return resp
def save(self):
if not self.is_moderated:
for user in (
RealGroup.objects.filter(id=settings.SITH_GROUP_COM_ADMIN_ID)
.first()
.users.all()
):
if not user.notifications.filter(
type="MAILING_MODERATION", viewed=False
).exists():
Notification(
user=user,
url=reverse("com:mailing_admin"),
type="MAILING_MODERATION",
).save()
super(Mailing, self).save()
def __str__(self):
return "%s - %s" % (self.club, self.email_full)
class MailingSubscription(models.Model): class MailingSubscription(models.Model):
""" """Link between user and mailing list."""
This class makes the link between user and mailing list
"""
mailing = models.ForeignKey( mailing = models.ForeignKey(
Mailing, Mailing,
@ -538,6 +609,9 @@ class MailingSubscription(models.Model):
class Meta: class Meta:
unique_together = (("user", "email", "mailing"),) unique_together = (("user", "email", "mailing"),)
def __str__(self):
return "(%s) - %s : %s" % (self.mailing, self.get_username, self.email)
def clean(self): def clean(self):
if not self.user and not self.email: if not self.user and not self.email:
raise ValidationError(_("At least user or email is required")) raise ValidationError(_("At least user or email is required"))
@ -552,7 +626,7 @@ class MailingSubscription(models.Model):
) )
except ObjectDoesNotExist: except ObjectDoesNotExist:
pass pass
super(MailingSubscription, self).clean() super().clean()
def is_owned_by(self, user): def is_owned_by(self, user):
if user.is_anonymous: if user.is_anonymous:
@ -580,6 +654,3 @@ class MailingSubscription(models.Model):
def fetch_format(self): def fetch_format(self):
return self.get_email + " " return self.get_email + " "
def __str__(self):
return "(%s) - %s : %s" % (self.mailing, self.get_username, self.email)

9
club/schemas.py Normal file
View File

@ -0,0 +1,9 @@
from ninja import ModelSchema
from club.models import Club
class ClubSchema(ModelSchema):
class Meta:
model = Club
fields = ["id", "name"]

View File

@ -0,0 +1,30 @@
import { AjaxSelect } from "#core:core/components/ajax-select-base";
import { registerComponent } from "#core:utils/web-components";
import type { TomOption } from "tom-select/dist/types/types";
import type { escape_html } from "tom-select/dist/types/utils";
import { type ClubSchema, clubSearchClub } from "#openapi";
@registerComponent("club-ajax-select")
export class ClubAjaxSelect extends AjaxSelect {
protected valueField = "id";
protected labelField = "name";
protected searchField = ["code", "name"];
protected async search(query: string): Promise<TomOption[]> {
const resp = await clubSearchClub({ query: { search: query } });
if (resp.data) {
return resp.data.results;
}
return [];
}
protected renderOption(item: ClubSchema, sanitize: typeof escape_html) {
return `<div class="select-item">
<span class="select-item-text">${sanitize(item.name)}</span>
</div>`;
}
protected renderItem(item: ClubSchema, sanitize: typeof escape_html) {
return `<span>${sanitize(item.name)}</span>`;
}
}

View File

@ -2,16 +2,16 @@
{% from 'core/macros.jinja' import user_profile_link %} {% from 'core/macros.jinja' import user_profile_link %}
{% block content %} {% block content %}
<div id="club_detail"> <div id="club_detail">
{% if club.logo %} {% if club.logo %}
<div class="club_logo"><img src="{{ club.logo.url }}" alt="{{ club.unix_name }}"></div> <div class="club_logo"><img src="{{ club.logo.url }}" alt="{{ club.unix_name }}"></div>
{% endif %} {% endif %}
{% if page_revision %} {% if page_revision %}
{{ page_revision|markdown }} {{ page_revision|markdown }}
{% else %} {% else %}
<h3>{% trans %}Club{% endtrans %}</h3> <h3>{% trans %}Club{% endtrans %}</h3>
{% endif %} {% endif %}
</div> </div>
{% endblock %} {% endblock %}

View File

@ -1,48 +1,48 @@
{% extends "core/base.jinja" %} {% extends "core/base.jinja" %}
{% block title %} {% block title %}
{% trans %}Club list{% endtrans %} {% trans %}Club list{% endtrans %}
{% endblock %} {% endblock %}
{% macro display_club(club) -%} {% macro display_club(club) -%}
{% if club.is_active or user.is_root %} {% if club.is_active or user.is_root %}
<li><a href="{{ url('club:club_view', club_id=club.id) }}">{{ club.name }}</a>
{% if not club.is_active %}
({% trans %}inactive{% endtrans %})
{% endif %}
{% if club.president %} - <a href="{{ url('core:user_profile', user_id=club.president.user.id) }}">{{ club.president.user }}</a>{% endif %} <li><a href="{{ url('club:club_view', club_id=club.id) }}">{{ club.name }}</a>
{% if club.short_description %}<p>{{ club.short_description|markdown }}</p>{% endif %}
{% endif %}
{%- if club.children.all()|length != 0 %} {% if not club.is_active %}
<ul> ({% trans %}inactive{% endtrans %})
{%- for c in club.children.order_by('name') %} {% endif %}
{{ display_club(c) }}
{%- endfor %} {% if club.president %} - <a href="{{ url('core:user_profile', user_id=club.president.user.id) }}">{{ club.president.user }}</a>{% endif %}
</ul> {% if club.short_description %}<p>{{ club.short_description|markdown }}</p>{% endif %}
{%- endif -%}
</li> {% endif %}
{%- if club.children.all()|length != 0 %}
<ul>
{%- for c in club.children.order_by('name') %}
{{ display_club(c) }}
{%- endfor %}
</ul>
{%- endif -%}
</li>
{%- endmacro %} {%- endmacro %}
{% block content %} {% block content %}
{% if user.is_root %} {% if user.is_root %}
<p><a href="{{ url('club:club_new') }}">{% trans %}New club{% endtrans %}</a></p> <p><a href="{{ url('club:club_new') }}">{% trans %}New club{% endtrans %}</a></p>
{% endif %} {% endif %}
{% if club_list %} {% if club_list %}
<h3>{% trans %}Club list{% endtrans %}</h3> <h3>{% trans %}Club list{% endtrans %}</h3>
<ul> <ul>
{%- for c in club_list.all().order_by('name') if c.parent is none %} {%- for c in club_list.all().order_by('name') if c.parent is none %}
{{ display_club(c) }} {{ display_club(c) }}
{%- endfor %} {%- endfor %}
</ul> </ul>
{% else %} {% else %}
{% trans %}There is no club in this website.{% endtrans %} {% trans %}There is no club in this website.{% endtrans %}
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@ -2,81 +2,81 @@
{% from 'core/macros.jinja' import user_profile_link, select_all_checkbox %} {% from 'core/macros.jinja' import user_profile_link, select_all_checkbox %}
{% block content %} {% block content %}
<h2>{% trans %}Club members{% endtrans %}</h2> <h2>{% trans %}Club members{% endtrans %}</h2>
{% if members %} {% if members %}
<form action="{{ url('club:club_members', club_id=club.id) }}" id="users_old" method="post"> <form action="{{ url('club:club_members', club_id=club.id) }}" id="users_old" method="post">
{% csrf_token %} {% csrf_token %}
{% set users_old = dict(form.users_old | groupby("choice_label")) %} {% set users_old = dict(form.users_old | groupby("choice_label")) %}
{% if users_old %} {% if users_old %}
{{ select_all_checkbox("users_old") }} {{ select_all_checkbox("users_old") }}
<p></p> <p></p>
{% endif %} {% endif %}
<table> <table>
<thead> <thead>
<tr> <tr>
<td>{% trans %}User{% endtrans %}</td> <td>{% trans %}User{% endtrans %}</td>
<td>{% trans %}Role{% endtrans %}</td> <td>{% trans %}Role{% endtrans %}</td>
<td>{% trans %}Description{% endtrans %}</td> <td>{% trans %}Description{% endtrans %}</td>
<td>{% trans %}Since{% endtrans %}</td> <td>{% trans %}Since{% endtrans %}</td>
{% if users_old %} {% if users_old %}
<td>{% trans %}Mark as old{% endtrans %}</td> <td>{% trans %}Mark as old{% endtrans %}</td>
{% endif %} {% endif %}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for m in members %} {% for m in members %}
<tr> <tr>
<td>{{ user_profile_link(m.user) }}</td> <td>{{ user_profile_link(m.user) }}</td>
<td>{{ settings.SITH_CLUB_ROLES[m.role] }}</td> <td>{{ settings.SITH_CLUB_ROLES[m.role] }}</td>
<td>{{ m.description }}</td> <td>{{ m.description }}</td>
<td>{{ m.start_date }}</td> <td>{{ m.start_date }}</td>
{% if users_old %} {% if users_old %}
<td> <td>
{% set user_old = users_old[m.user.get_display_name()] %} {% set user_old = users_old[m.user.get_display_name()] %}
{% if user_old %} {% if user_old %}
{{ user_old[0].tag() }} {{ user_old[0].tag() }}
{% endif %} {% endif %}
</td> </td>
{% endif %} {% endif %}
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
{{ form.users_old.errors }} {{ form.users_old.errors }}
{% if users_old %} {% if users_old %}
<p></p> <p></p>
<input type="submit" name="submit" value="{% trans %}Mark as old{% endtrans %}"> <input type="submit" name="submit" value="{% trans %}Mark as old{% endtrans %}">
{% endif %} {% endif %}
</form> </form>
{% else %} {% else %}
<p>{% trans %}There are no members in this club.{% endtrans %}</p> <p>{% trans %}There are no members in this club.{% endtrans %}</p>
{% endif %}
<form action="{{ url('club:club_members', club_id=club.id) }}" id="add_users" method="post">
{% csrf_token %}
{{ form.non_field_errors() }}
<p>
{{ form.users.errors }}
<label for="{{ form.users.id_for_label }}">{{ form.users.label }} :</label>
{{ form.users }}
<span class="helptext">{{ form.users.help_text }}</span>
</p>
<p>
{{ form.role.errors }}
<label for="{{ form.role.id_for_label }}">{{ form.role.label }} :</label>
{{ form.role }}
</p>
{% if form.start_date %}
<p>
{{ form.start_date.errors }}
<label for="{{ form.start_date.id_for_label }}">{{ form.start_date.label }} :</label>
{{ form.start_date }}
</p>
{% endif %} {% endif %}
<form action="{{ url('club:club_members', club_id=club.id) }}" id="add_users" method="post"> <p>
{% csrf_token %} {{ form.description.errors }}
{{ form.non_field_errors() }} <label for="{{ form.description.id_for_label }}">{{ form.description.label }} :</label>
<p> {{ form.description }}
{{ form.users.errors }} </p>
<label for="{{ form.users.id_for_label }}">{{ form.users.label }} :</label> <p><input type="submit" value="{% trans %}Add{% endtrans %}" /></p>
{{ form.users }} </form>
<span class="helptext">{{ form.users.help_text }}</span>
</p>
<p>
{{ form.role.errors }}
<label for="{{ form.role.id_for_label }}">{{ form.role.label }} :</label>
{{ form.role }}
</p>
{% if form.start_date %}
<p>
{{ form.start_date.errors }}
<label for="{{ form.start_date.id_for_label }}">{{ form.start_date.label }} :</label>
{{ form.start_date }}
</p>
{% endif %}
<p>
{{ form.description.errors }}
<label for="{{ form.description.id_for_label }}">{{ form.description.label }} :</label>
{{ form.description }}
</p>
<p><input type="submit" value="{% trans %}Add{% endtrans %}" /></p>
</form>
{% endblock %} {% endblock %}

View File

@ -2,27 +2,27 @@
{% from 'core/macros.jinja' import user_profile_link %} {% from 'core/macros.jinja' import user_profile_link %}
{% block content %} {% block content %}
<h2>{% trans %}Club old members{% endtrans %}</h2> <h2>{% trans %}Club old members{% endtrans %}</h2>
<table> <table>
<thead> <thead>
<td>{% trans %}User{% endtrans %}</td> <td>{% trans %}User{% endtrans %}</td>
<td>{% trans %}Role{% endtrans %}</td> <td>{% trans %}Role{% endtrans %}</td>
<td>{% trans %}Description{% endtrans %}</td> <td>{% trans %}Description{% endtrans %}</td>
<td>{% trans %}From{% endtrans %}</td> <td>{% trans %}From{% endtrans %}</td>
<td>{% trans %}To{% endtrans %}</td> <td>{% trans %}To{% endtrans %}</td>
</thead> </thead>
<tbody> <tbody>
{% for m in club.members.exclude(end_date=None).order_by('-role', 'description', '-end_date').all() %} {% for m in club.members.exclude(end_date=None).order_by('-role', 'description', '-end_date').all() %}
<tr> <tr>
<td>{{ user_profile_link(m.user) }}</td> <td>{{ user_profile_link(m.user) }}</td>
<td>{{ settings.SITH_CLUB_ROLES[m.role] }}</td> <td>{{ settings.SITH_CLUB_ROLES[m.role] }}</td>
<td>{{ m.description }}</td> <td>{{ m.description }}</td>
<td>{{ m.start_date }}</td> <td>{{ m.start_date }}</td>
<td>{{ m.end_date }}</td> <td>{{ m.end_date }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
{% endblock %} {% endblock %}

View File

@ -1,66 +1,94 @@
{% extends "core/base.jinja" %} {% extends "core/base.jinja" %}
{% from 'core/macros.jinja' import user_profile_link, paginate %} {% from 'core/macros.jinja' import user_profile_link %}
{# This page uses a custom macro instead of the core `paginate_jinja` and `paginate_alpine`
because it works with a somewhat dynamic form,
but was written before Alpine was introduced in the project.
TODO : rewrite the pagination used in this template an Alpine one
#}
{% macro paginate(page_obj, paginator, js_action) %}
{% set js = js_action|default('') %}
{% if page_obj.has_previous() or page_obj.has_next() %}
{% if page_obj.has_previous() %}
<a {% if js %} type="submit" onclick="{{ js }}" {% endif %} href="?page={{ page_obj.previous_page_number() }}">{% trans %}Previous{% endtrans %}</a>
{% else %}
<span class="disabled">{% trans %}Previous{% endtrans %}</span>
{% endif %}
{% for i in paginator.page_range %}
{% if page_obj.number == i %}
<span class="active">{{ i }} <span class="sr-only">({% trans %}current{% endtrans %})</span></span>
{% else %}
<a {% if js %} type="submit" onclick="{{ js }}" {% endif %} href="?page={{ i }}">{{ i }}</a>
{% endif %}
{% endfor %}
{% if page_obj.has_next() %}
<a {% if js %} type="submit" onclick="{{ js }}" {% endif %} href="?page={{ page_obj.next_page_number() }}">{% trans %}Next{% endtrans %}</a>
{% else %}
<span class="disabled">{% trans %}Next{% endtrans %}</span>
{% endif %}
{% endif %}
{% endmacro %}
{% block content %} {% block content %}
<h3>{% trans %}Sellings{% endtrans %}</h3> <h3>{% trans %}Sales{% endtrans %}</h3>
<form id="form" action="?page=1" method="post"> <form id="form" action="?page=1" method="post">
{% csrf_token %} {% csrf_token %}
{{ form }} {{ form }}
<p><input type="submit" value="{% trans %}Show{% endtrans %}" /></p> <p><input type="submit" value="{% trans %}Show{% endtrans %}" /></p>
<p><input type="submit" value="{% trans %}Download as cvs{% endtrans %}" formaction="{{ url('club:sellings_csv', club_id=object.id) }}"/></p> <p><input type="submit" value="{% trans %}Download as cvs{% endtrans %}" formaction="{{ url('club:sellings_csv', club_id=object.id) }}"/></p>
</form> </form>
<p> <p>
{% trans %}Quantity: {% endtrans %}{{ total_quantity }} {% trans %}units{% endtrans %}<br/> {% trans %}Quantity: {% endtrans %}{{ total_quantity }} {% trans %}units{% endtrans %}<br/>
{% trans %}Total: {% endtrans %}{{ total }} €<br/> {% trans %}Total: {% endtrans %}{{ total }} €<br/>
{% trans %}Benefit: {% endtrans %}{{ benefit }} {% trans %}Benefit: {% endtrans %}{{ benefit }}
</p> </p>
<table> <table>
<thead> <thead>
<tr> <tr>
<td>{% trans %}Date{% endtrans %}</td> <td>{% trans %}Date{% endtrans %}</td>
<td>{% trans %}Counter{% endtrans %}</td> <td>{% trans %}Counter{% endtrans %}</td>
<td>{% trans %}Barman{% endtrans %}</td> <td>{% trans %}Barman{% endtrans %}</td>
<td>{% trans %}Customer{% endtrans %}</td> <td>{% trans %}Customer{% endtrans %}</td>
<td>{% trans %}Label{% endtrans %}</td> <td>{% trans %}Label{% endtrans %}</td>
<td>{% trans %}Quantity{% endtrans %}</td> <td>{% trans %}Quantity{% endtrans %}</td>
<td>{% trans %}Total{% endtrans %}</td> <td>{% trans %}Total{% endtrans %}</td>
<td>{% trans %}Payment method{% endtrans %}</td> <td>{% trans %}Payment method{% endtrans %}</td>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for s in paginated_result %} {% for s in paginated_result %}
<tr> <tr>
<td>{{ s.date|localtime|date(DATETIME_FORMAT) }} {{ s.date|localtime|time(DATETIME_FORMAT) }}</td> <td>{{ s.date|localtime|date(DATETIME_FORMAT) }} {{ s.date|localtime|time(DATETIME_FORMAT) }}</td>
<td>{{ s.counter }}</td> <td>{{ s.counter }}</td>
{% if s.seller %} {% if s.seller %}
<td><a href="{{ s.seller.get_absolute_url() }}">{{ s.seller.get_display_name() }}</a></td> <td><a href="{{ s.seller.get_absolute_url() }}">{{ s.seller.get_display_name() }}</a></td>
{% else %} {% else %}
<td></td> <td></td>
{% endif %} {% endif %}
{% if s.customer %} {% if s.customer %}
<td><a href="{{ s.customer.user.get_absolute_url() }}">{{ s.customer.user.get_display_name() }}</a></td> <td><a href="{{ s.customer.user.get_absolute_url() }}">{{ s.customer.user.get_display_name() }}</a></td>
{% else %} {% else %}
<td></td> <td></td>
{% endif %} {% endif %}
<td>{{ s.label }}</td> <td>{{ s.label }}</td>
<td>{{ s.quantity }}</td> <td>{{ s.quantity }}</td>
<td>{{ s.quantity * s.unit_price }} €</td> <td>{{ s.quantity * s.unit_price }} €</td>
<td>{{ s.get_payment_method_display() }}</td> <td>{{ s.get_payment_method_display() }}</td>
{% if s.is_owned_by(user) %} {% if s.is_owned_by(user) %}
<td><a href="{{ url('counter:selling_delete', selling_id=s.id) }}">{% trans %}Delete{% endtrans %}</a></td> <td><a href="{{ url('counter:selling_delete', selling_id=s.id) }}">{% trans %}Delete{% endtrans %}</a></td>
{% endif %} {% endif %}
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
<script type="text/javascript"> <script type="text/javascript">
function formPagination(link){ function formPagination(link){
$("form").attr("action", link.href); $("form").attr("action", link.href);
link.href = "javascript:void(0)"; // block link action link.href = "javascript:void(0)"; // block link action
$("form").submit(); $("form").submit();
} }
</script> </script>
{{ paginate(paginated_result, paginator, "formPagination(this)") }} {{ paginate(paginated_result, paginator, "formPagination(this)") }}
{% endblock %} {% endblock %}

View File

@ -1,46 +1,46 @@
{% extends "core/base.jinja" %} {% extends "core/base.jinja" %}
{% block content %} {% block content %}
<h3>{% trans %}Club tools{% endtrans %}</h3> <h3>{% trans %}Club tools{% endtrans %}</h3>
<div> <div>
<h4>{% trans %}Communication:{% endtrans %}</h4> <h4>{% trans %}Communication:{% endtrans %}</h4>
<ul> <ul>
<li> <a href="{{ url('com:news_new') }}?club={{ object.id }}">{% trans %}Create a news{% endtrans %}</a></li> <li> <a href="{{ url('com:news_new') }}?club={{ object.id }}">{% trans %}Create a news{% endtrans %}</a></li>
<li> <a href="{{ url('com:weekmail_article') }}?club={{ object.id }}">{% trans %}Post in the Weekmail{% endtrans %}</a></li> <li> <a href="{{ url('com:weekmail_article') }}?club={{ object.id }}">{% trans %}Post in the Weekmail{% endtrans %}</a></li>
{% if object.trombi %} {% if object.trombi %}
<li> <a href="{{ url('trombi:detail', trombi_id=object.trombi.id) }}">{% trans %}Edit Trombi{% endtrans %}</a></li> <li> <a href="{{ url('trombi:detail', trombi_id=object.trombi.id) }}">{% trans %}Edit Trombi{% endtrans %}</a></li>
{% else %} {% else %}
<li> <a href="{{ url('trombi:create', club_id=object.id) }}">{% trans %}New Trombi{% endtrans %}</a></li> <li> <a href="{{ url('trombi:create', club_id=object.id) }}">{% trans %}New Trombi{% endtrans %}</a></li>
<li> <a href="{{ url('club:poster_list', club_id=object.id) }}">{% trans %}Posters{% endtrans %}</a></li> <li> <a href="{{ url('club:poster_list', club_id=object.id) }}">{% trans %}Posters{% endtrans %}</a></li>
{% endif %} {% endif %}
</ul> </ul>
<h4>{% trans %}Counters:{% endtrans %}</h4> <h4>{% trans %}Counters:{% endtrans %}</h4>
<ul> <ul>
{% if object.unix_name == settings.SITH_LAUNDERETTE_MANAGER['unix_name'] %} {% if object.unix_name == settings.SITH_LAUNDERETTE_MANAGER['unix_name'] %}
{% for l in Launderette.objects.all() %} {% for l in Launderette.objects.all() %}
<li><a href="{{ url('launderette:main_click', launderette_id=l.id) }}">{{ l }}</a></li> <li><a href="{{ url('launderette:main_click', launderette_id=l.id) }}">{{ l }}</a></li>
{% endfor %} {% endfor %}
{% elif object.counters.filter(type="OFFICE")|count > 0 %} {% elif object.counters.filter(type="OFFICE")|count > 0 %}
{% for c in object.counters.filter(type="OFFICE") %} {% for c in object.counters.filter(type="OFFICE") %}
<li>{{ c }}: <li>{{ c }}:
<a href="{{ url('counter:details', counter_id=c.id) }}">View</a> <a href="{{ url('counter:details', counter_id=c.id) }}">View</a>
<a href="{{ url('counter:admin', counter_id=c.id) }}">Edit</a> <a href="{{ url('counter:admin', counter_id=c.id) }}">Edit</a>
</li> </li>
{% endfor %} {% endfor %}
{% endif %} {% endif %}
</ul> </ul>
{% if object.club_account.exists() %} {% if object.club_account.exists() %}
<h4>{% trans %}Accouting: {% endtrans %}</h4> <h4>{% trans %}Accounting: {% endtrans %}</h4>
<ul> <ul>
{% for ca in object.club_account.all() %} {% for ca in object.club_account.all() %}
<li><a href="{{ url('accounting:club_details', c_account_id=ca.id) }}">{{ ca.get_display_name() }}</a></li> <li><a href="{{ url('accounting:club_details', c_account_id=ca.id) }}">{{ ca.get_display_name() }}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>
{% endif %} {% endif %}
{% if object.unix_name == settings.SITH_LAUNDERETTE_MANAGER['unix_name'] %} {% if object.unix_name == settings.SITH_LAUNDERETTE_MANAGER['unix_name'] %}
<li><a href="{{ url('launderette:launderette_list') }}">{% trans %}Manage launderettes{% endtrans %}</a></li> <li><a href="{{ url('launderette:launderette_list') }}">{% trans %}Manage launderettes{% endtrans %}</a></li>
{% endif %} {% endif %}
</div> </div>
{% endblock %} {% endblock %}

View File

@ -2,107 +2,107 @@
{% from 'core/macros.jinja' import select_all_checkbox %} {% from 'core/macros.jinja' import select_all_checkbox %}
{% block title %} {% block title %}
{% trans %}Mailing lists{% endtrans %} {% trans %}Mailing lists{% endtrans %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<b>{% trans %}Remember : mailing lists need to be moderated, if your new created list is not shown wait until moderation takes action{% endtrans %}</b> <b>{% trans %}Remember : mailing lists need to be moderated, if your new created list is not shown wait until moderation takes action{% endtrans %}</b>
{% if mailings_not_moderated %} {% if mailings_not_moderated %}
<p>{% trans %}Mailing lists waiting for moderation{% endtrans %}</p> <p>{% trans %}Mailing lists waiting for moderation{% endtrans %}</p>
<ul> <ul>
{% for mailing in mailings_not_moderated %} {% for mailing in mailings_not_moderated %}
<li>{{ mailing.email_full }}<a href="{{ url('club:mailing_delete', mailing_id=mailing.id) }}"> - {% trans %}Delete{% endtrans %}</a></li> <li>{{ mailing.email_full }}<a href="{{ url('club:mailing_delete', mailing_id=mailing.id) }}"> - {% trans %}Delete{% endtrans %}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>
{% endif %} {% endif %}
{% if mailings_moderated %} {% if mailings_moderated %}
{% for mailing in mailings_moderated %} {% for mailing in mailings_moderated %}
<h2>{% trans %}Mailing{% endtrans %} {{ mailing.email_full }} <h2>{% trans %}Mailing{% endtrans %} {{ mailing.email_full }}
{%- if user.is_owner(mailing) -%} {%- if user.is_owner(mailing) -%}
<a href="{{ url('club:mailing_delete', mailing_id=mailing.id) }}"> - {% trans %}Delete{% endtrans %}</a> <a href="{{ url('club:mailing_delete', mailing_id=mailing.id) }}"> - {% trans %}Delete{% endtrans %}</a>
{%- endif -%} {%- endif -%}
</h2> </h2>
<form method="GET" action="{{ url('club:mailing_generate', mailing_id=mailing.id) }}" style="display:inline-block;"> <form method="GET" action="{{ url('club:mailing_generate', mailing_id=mailing.id) }}" style="display:inline-block;">
<input type="submit" name="generateMalingList" value="{% trans %}Generate mailing list{% endtrans %}"> <input type="submit" name="generateMalingList" value="{% trans %}Generate mailing list{% endtrans %}">
</form> </form>
{% set form_mailing_removal = form["removal_" + mailing.id|string] %} {% set form_mailing_removal = form["removal_" + mailing.id|string] %}
{% if form_mailing_removal.field.choices %} {% if form_mailing_removal.field.choices %}
{% set ms = dict(mailing.subscriptions.all() | groupby('id')) %} {% set ms = dict(mailing.subscriptions.all() | groupby('id')) %}
<form action="{{ url('club:mailing', club_id=club.id) }}" id="{{ form_mailing_removal.auto_id }}" method="post" enctype="multipart/form-data"> <form action="{{ url('club:mailing', club_id=club.id) }}" id="{{ form_mailing_removal.auto_id }}" method="post" enctype="multipart/form-data">
<p style="margin-bottom: 1em;">{{ select_all_checkbox(form_mailing_removal.auto_id) }}</p> <p style="margin-bottom: 1em;">{{ select_all_checkbox(form_mailing_removal.auto_id) }}</p>
{% csrf_token %} {% csrf_token %}
<input hidden type="number" name="{{ form.action.name }}" value="{{ form_actions.REMOVE_SUBSCRIPTION }}" /> <input hidden type="number" name="{{ form.action.name }}" value="{{ form_actions.REMOVE_SUBSCRIPTION }}" />
<table> <table>
<thead> <thead>
<tr> <tr>
<td>{% trans %}User{% endtrans %}</td> <td>{% trans %}User{% endtrans %}</td>
<td>{% trans %}Email{% endtrans %}</td> <td>{% trans %}Email{% endtrans %}</td>
<td>{% trans %}Delete{% endtrans %}</td> <td>{% trans %}Delete{% endtrans %}</td>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for widget in form_mailing_removal.subwidgets %} {% for widget in form_mailing_removal.subwidgets %}
{% set user = ms[widget.data.value.value][0] %} {% set user = ms[widget.data.value.value][0] %}
<tr> <tr>
<td>{{ user.get_username }}</td> <td>{{ user.get_username }}</td>
<td>{{ user.get_email }}</td> <td>{{ user.get_email }}</td>
<td>{{ widget.tag() }}</td> <td>{{ widget.tag() }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
{{ form_mailing_removal.errors }} {{ form_mailing_removal.errors }}
<p><input type="submit" value="{% trans %}Remove from mailing list{% endtrans %}" /></p> <p><input type="submit" value="{% trans %}Remove from mailing list{% endtrans %}" /></p>
</form> </form>
{% else %} {% else %}
<p><b>{% trans %}There is no subscriber for this mailing list{% endtrans %}</b></p> <p><b>{% trans %}There is no subscriber for this mailing list{% endtrans %}</b></p>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% else %} {% else %}
<p>{% trans %}No mailing list existing for this club{% endtrans %}</p> <p>{% trans %}No mailing list existing for this club{% endtrans %}</p>
{% endif %} {% endif %}
<p>{{ form.non_field_errors() }}</p> <p>{{ form.non_field_errors() }}</p>
{% if mailings_moderated %} {% if mailings_moderated %}
<h2>{% trans %}New member{% endtrans %}</h2> <h2>{% trans %}New member{% endtrans %}</h2>
<form action="{{ url('club:mailing', club_id=club.id) }}" method="post" enctype="multipart/form-data">
{% csrf_token %}
<p>
{{ form.subscription_mailing.errors }}
<label for="{{ form.subscription_mailing.id_for_label }}">{{ form.subscription_mailing.label }}</label>
{{ form.subscription_mailing }}
</p>
<p>
{{ form.subscription_users.errors }}
<label for="{{ form.subscription_users.id_for_label }}">{{ form.subscription_users.label }}</label>
{{ form.subscription_users }}
<span class="helptext">{{ form.subscription_users.help_text }}</span>
</p>
<p>
{{ form.subscription_email.errors }}
<label for="{{ form.subscription_email.id_for_label }}">{{ form.subscription_email.label }}</label>
{{ form.subscription_email }}
</p>
<input hidden type="number" name="{{ form.action.name }}" value="{{ form_actions.NEW_SUBSCRIPTION }}" />
<p><input type="submit" value="{% trans %}Add to mailing list{% endtrans %}" /></p>
</form>
{% endif %}
<h2>{% trans %}New mailing{% endtrans %}</h2>
<form action="{{ url('club:mailing', club_id=club.id) }}" method="post" enctype="multipart/form-data"> <form action="{{ url('club:mailing', club_id=club.id) }}" method="post" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
<p> <p>
{{ form.mailing_email.errors }} {{ form.subscription_mailing.errors }}
<label for="{{ form.mailing_email.id_for_label }}">{{ form.mailing_email.label }}</label> <label for="{{ form.subscription_mailing.id_for_label }}">{{ form.subscription_mailing.label }}</label>
{{ form.mailing_email }} {{ form.subscription_mailing }}
</p> </p>
<input hidden type="number" name="{{ form.action.name }}" value="{{ form_actions.NEW_MALING }}" /> <p>
<p><input type="submit" value="{% trans %}Create mailing list{% endtrans %}" /></p> {{ form.subscription_users.errors }}
<label for="{{ form.subscription_users.id_for_label }}">{{ form.subscription_users.label }}</label>
{{ form.subscription_users }}
<span class="helptext">{{ form.subscription_users.help_text }}</span>
</p>
<p>
{{ form.subscription_email.errors }}
<label for="{{ form.subscription_email.id_for_label }}">{{ form.subscription_email.label }}</label>
{{ form.subscription_email }}
</p>
<input hidden type="number" name="{{ form.action.name }}" value="{{ form_actions.NEW_SUBSCRIPTION }}" />
<p><input type="submit" value="{% trans %}Add to mailing list{% endtrans %}" /></p>
</form> </form>
{% endif %}
<h2>{% trans %}New mailing{% endtrans %}</h2>
<form action="{{ url('club:mailing', club_id=club.id) }}" method="post" enctype="multipart/form-data">
{% csrf_token %}
<p>
{{ form.mailing_email.errors }}
<label for="{{ form.mailing_email.id_for_label }}">{{ form.mailing_email.label }}</label>
{{ form.mailing_email }}
</p>
<input hidden type="number" name="{{ form.action.name }}" value="{{ form_actions.NEW_MALING }}" />
<p><input type="submit" value="{% trans %}Create mailing list{% endtrans %}" /></p>
</form>
{% endblock %} {% endblock %}

View File

@ -2,11 +2,11 @@
{% from 'core/macros_pages.jinja' import page_history %} {% from 'core/macros_pages.jinja' import page_history %}
{% block content %} {% block content %}
{% if club.page %} {% if club.page %}
{{ page_history(club.page) }} {{ page_history(club.page) }}
{% else %} {% else %}
{% trans %}No page existing for this club{% endtrans %} {% trans %}No page existing for this club{% endtrans %}
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@ -2,7 +2,7 @@
{% from 'core/macros_pages.jinja' import page_edit_form %} {% from 'core/macros_pages.jinja' import page_edit_form %}
{% block content %} {% block content %}
{{ page_edit_form(page, form, url('club:club_edit_page', club_id=page.club.id), csrf_token) }} {{ page_edit_form(page, form, url('club:club_edit_page', club_id=page.club.id), csrf_token) }}
{% endblock %} {% endblock %}

View File

@ -1,48 +1,48 @@
{% extends "core/base.jinja" %} {% extends "core/base.jinja" %}
{% block title %} {% block title %}
{% trans %}Club stats{% endtrans %} {% trans %}Club stats{% endtrans %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{% if club_list %} {% if club_list %}
<h3>{% trans %}Club stats{% endtrans %}</h3> <h3>{% trans %}Club stats{% endtrans %}</h3>
<form action="" method="GET"> <form action="" method="GET">
{% csrf_token %} {% csrf_token %}
<p> <p>
<select name="branch"> <select name="branch">
{% for b in settings.SITH_PROFILE_DEPARTMENTS %} {% for b in settings.SITH_PROFILE_DEPARTMENTS %}
<option value="{{ b[0] }}">{{ b[0] }}</option> <option value="{{ b[0] }}">{{ b[0] }}</option>
{% endfor %} {% endfor %}
</select> </select>
</p> </p>
<p><input type="submit" value="{% trans %}Show{% endtrans %}" /></p> <p><input type="submit" value="{% trans %}Show{% endtrans %}" /></p>
</form> </form>
<table> <table>
<thead> <thead>
<tr> <tr>
<td>Club</td> <td>Club</td>
<td>Member number</td> <td>Member number</td>
<td>Old member number</td> <td>Old member number</td>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for c in club_list.order_by('id') %} {% for c in club_list.order_by('id') %}
{% set members = c.members.all() %} {% set members = c.members.all() %}
{% if request.GET['branch'] %} {% if request.GET['branch'] %}
{% set members = members.filter(user__department=request.GET['branch']) %} {% set members = members.filter(user__department=request.GET['branch']) %}
{% endif %} {% endif %}
<tr> <tr>
<td>{{ c.get_display_name() }}</td> <td>{{ c.get_display_name() }}</td>
<td>{{ members.filter(end_date=None, role__gt=settings.SITH_MAXIMUM_FREE_ROLE).count() }}</td> <td>{{ members.filter(end_date=None, role__gt=settings.SITH_MAXIMUM_FREE_ROLE).count() }}</td>
<td>{{ members.exclude(end_date=None, role__gt=settings.SITH_MAXIMUM_FREE_ROLE).count() }}</td> <td>{{ members.exclude(end_date=None, role__gt=settings.SITH_MAXIMUM_FREE_ROLE).count() }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
{% else %} {% else %}
{% trans %}There is no club in this website.{% endtrans %} {% trans %}There is no club in this website.{% endtrans %}
{% endif %} {% endif %}
{% endblock %} {% endblock %}

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,3 @@
# -*- coding:utf-8 -*
# #
# Copyright 2016,2017 # Copyright 2016,2017
# - Skia <skia@libskia.so> # - Skia <skia@libskia.so>
@ -25,7 +24,32 @@
from django.urls import path from django.urls import path
from club.views import * from club.views import (
ClubCreateView,
ClubEditPropView,
ClubEditView,
ClubListView,
ClubMailingView,
ClubMembersView,
ClubOldMembersView,
ClubPageEditView,
ClubPageHistView,
ClubRevView,
ClubSellingCSVView,
ClubSellingView,
ClubStatView,
ClubToolsView,
ClubView,
MailingAutoGenerationView,
MailingDeleteView,
MailingSubscriptionDeleteView,
MembershipDeleteView,
MembershipSetOldView,
PosterCreateView,
PosterDeleteView,
PosterEditView,
PosterListView,
)
urlpatterns = [ urlpatterns = [
path("", ClubListView.as_view(), name="club_list"), path("", ClubListView.as_view(), name="club_list"),
@ -33,32 +57,20 @@ urlpatterns = [
path("stats/", ClubStatView.as_view(), name="club_stats"), path("stats/", ClubStatView.as_view(), name="club_stats"),
path("<int:club_id>/", ClubView.as_view(), name="club_view"), path("<int:club_id>/", ClubView.as_view(), name="club_view"),
path( path(
"<int:club_id>/rev/<int:rev_id>/", "<int:club_id>/rev/<int:rev_id>/", ClubRevView.as_view(), name="club_view_rev"
ClubRevView.as_view(),
name="club_view_rev",
), ),
path("<int:club_id>/hist/", ClubPageHistView.as_view(), name="club_hist"), path("<int:club_id>/hist/", ClubPageHistView.as_view(), name="club_hist"),
path("<int:club_id>/edit/", ClubEditView.as_view(), name="club_edit"), path("<int:club_id>/edit/", ClubEditView.as_view(), name="club_edit"),
path( path("<int:club_id>/edit/page/", ClubPageEditView.as_view(), name="club_edit_page"),
"<int:club_id>/edit/page/",
ClubPageEditView.as_view(),
name="club_edit_page",
),
path("<int:club_id>/members/", ClubMembersView.as_view(), name="club_members"), path("<int:club_id>/members/", ClubMembersView.as_view(), name="club_members"),
path( path(
"<int:club_id>/elderlies/", "<int:club_id>/elderlies/",
ClubOldMembersView.as_view(), ClubOldMembersView.as_view(),
name="club_old_members", name="club_old_members",
), ),
path("<int:club_id>/sellings/", ClubSellingView.as_view(), name="club_sellings"),
path( path(
"<int:club_id>/sellings/", "<int:club_id>/sellings/csv/", ClubSellingCSVView.as_view(), name="sellings_csv"
ClubSellingView.as_view(),
name="club_sellings",
),
path(
"<int:club_id>/sellings/csv/",
ClubSellingCSVView.as_view(),
name="sellings_csv",
), ),
path("<int:club_id>/prop/", ClubEditPropView.as_view(), name="club_prop"), path("<int:club_id>/prop/", ClubEditPropView.as_view(), name="club_prop"),
path("<int:club_id>/tools/", ClubToolsView.as_view(), name="tools"), path("<int:club_id>/tools/", ClubToolsView.as_view(), name="tools"),
@ -90,9 +102,7 @@ urlpatterns = [
), ),
path("<int:club_id>/poster/", PosterListView.as_view(), name="poster_list"), path("<int:club_id>/poster/", PosterListView.as_view(), name="poster_list"),
path( path(
"<int:club_id>/poster/create/", "<int:club_id>/poster/create/", PosterCreateView.as_view(), name="poster_create"
PosterCreateView.as_view(),
name="poster_create",
), ),
path( path(
"<int:club_id>/poster/<int:poster_id>/edit/", "<int:club_id>/poster/<int:poster_id>/edit/",

View File

@ -1,4 +1,3 @@
# -*- coding:utf-8 -*
# #
# Copyright 2016,2017 # Copyright 2016,2017
# - Skia <skia@libskia.so> # - Skia <skia@libskia.so>
@ -26,50 +25,42 @@
import csv import csv
from django.conf import settings from django.conf import settings
from django import forms from django.core.exceptions import NON_FIELD_ERRORS, PermissionDenied, ValidationError
from django.views.generic import ListView, DetailView, TemplateView, View from django.core.paginator import InvalidPage, Paginator
from django.views.generic.edit import DeleteView from django.db.models import Sum
from django.views.generic.detail import SingleObjectMixin
from django.views.generic.edit import UpdateView, CreateView
from django.http import ( from django.http import (
HttpResponseRedirect,
HttpResponse,
Http404, Http404,
HttpResponseRedirect,
StreamingHttpResponse, StreamingHttpResponse,
) )
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.utils.translation import gettext as _t from django.utils.translation import gettext as _t
from django.core.exceptions import PermissionDenied, ValidationError, NON_FIELD_ERRORS from django.utils.translation import gettext_lazy as _
from django.core.paginator import Paginator, InvalidPage from django.views.generic import DetailView, ListView, TemplateView, View
from django.shortcuts import get_object_or_404, redirect from django.views.generic.edit import CreateView, DeleteView, UpdateView
from django.db.models import Sum
from club.forms import ClubEditForm, ClubMemberForm, MailingForm, SellingsForm
from core.views import ( from club.models import Club, Mailing, MailingSubscription, Membership
CanCreateMixin, from com.views import (
CanViewMixin, PosterCreateBaseView,
CanEditMixin, PosterDeleteBaseView,
CanEditPropMixin, PosterEditBaseView,
UserIsRootMixin, PosterListBaseView,
TabedViewMixin,
PageEditViewBase,
DetailFormView,
) )
from core.models import PageRev from core.models import PageRev
from core.views import (
from counter.models import Selling CanCreateMixin,
CanEditMixin,
from com.views import ( CanEditPropMixin,
PosterListBaseView, CanViewMixin,
PosterCreateBaseView, DetailFormView,
PosterEditBaseView, PageEditViewBase,
PosterDeleteBaseView, TabedViewMixin,
UserIsRootMixin,
) )
from counter.models import Selling
from club.models import Club, Membership, Mailing, MailingSubscription
from club.forms import MailingForm, ClubEditForm, ClubMemberForm, SellingsForm
class ClubTabsMixin(TabedViewMixin): class ClubTabsMixin(TabedViewMixin):
@ -80,14 +71,13 @@ class ClubTabsMixin(TabedViewMixin):
return self.object.get_display_name() return self.object.get_display_name()
def get_list_of_tabs(self): def get_list_of_tabs(self):
tab_list = [] tab_list = [
tab_list.append(
{ {
"url": reverse("club:club_view", kwargs={"club_id": self.object.id}), "url": reverse("club:club_view", kwargs={"club_id": self.object.id}),
"slug": "infos", "slug": "infos",
"name": _("Infos"), "name": _("Infos"),
} }
) ]
if self.request.user.can_view(self.object): if self.request.user.can_view(self.object):
tab_list.append( tab_list.append(
{ {
@ -184,18 +174,14 @@ class ClubTabsMixin(TabedViewMixin):
class ClubListView(ListView): class ClubListView(ListView):
""" """List the Clubs."""
List the Clubs
"""
model = Club model = Club
template_name = "club/club_list.jinja" template_name = "club/club_list.jinja"
class ClubView(ClubTabsMixin, DetailView): class ClubView(ClubTabsMixin, DetailView):
""" """Front page of a Club."""
Front page of a Club
"""
model = Club model = Club
pk_url_kwarg = "club_id" pk_url_kwarg = "club_id"
@ -203,24 +189,22 @@ class ClubView(ClubTabsMixin, DetailView):
current_tab = "infos" current_tab = "infos"
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs = super(ClubView, self).get_context_data(**kwargs) kwargs = super().get_context_data(**kwargs)
if self.object.page and self.object.page.revisions.exists(): if self.object.page and self.object.page.revisions.exists():
kwargs["page_revision"] = self.object.page.revisions.last().content kwargs["page_revision"] = self.object.page.revisions.last().content
return kwargs return kwargs
class ClubRevView(ClubView): class ClubRevView(ClubView):
""" """Display a specific page revision."""
Display a specific page revision
"""
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
obj = self.get_object() obj = self.get_object()
self.revision = get_object_or_404(PageRev, pk=kwargs["rev_id"], page__club=obj) self.revision = get_object_or_404(PageRev, pk=kwargs["rev_id"], page__club=obj)
return super(ClubRevView, self).dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs = super(ClubRevView, self).get_context_data(**kwargs) kwargs = super().get_context_data(**kwargs)
kwargs["page_revision"] = self.revision.content kwargs["page_revision"] = self.revision.content
return kwargs return kwargs
@ -233,7 +217,7 @@ class ClubPageEditView(ClubTabsMixin, PageEditViewBase):
self.club = get_object_or_404(Club, pk=kwargs["club_id"]) self.club = get_object_or_404(Club, pk=kwargs["club_id"])
if not self.club.page: if not self.club.page:
raise Http404 raise Http404
return super(ClubPageEditView, self).dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
def get_object(self): def get_object(self):
self.page = self.club.page self.page = self.club.page
@ -244,9 +228,7 @@ class ClubPageEditView(ClubTabsMixin, PageEditViewBase):
class ClubPageHistView(ClubTabsMixin, CanViewMixin, DetailView): class ClubPageHistView(ClubTabsMixin, CanViewMixin, DetailView):
""" """Modification hostory of the page."""
Modification hostory of the page
"""
model = Club model = Club
pk_url_kwarg = "club_id" pk_url_kwarg = "club_id"
@ -255,9 +237,7 @@ class ClubPageHistView(ClubTabsMixin, CanViewMixin, DetailView):
class ClubToolsView(ClubTabsMixin, CanEditMixin, DetailView): class ClubToolsView(ClubTabsMixin, CanEditMixin, DetailView):
""" """Tools page of a Club."""
Tools page of a Club
"""
model = Club model = Club
pk_url_kwarg = "club_id" pk_url_kwarg = "club_id"
@ -266,9 +246,7 @@ class ClubToolsView(ClubTabsMixin, CanEditMixin, DetailView):
class ClubMembersView(ClubTabsMixin, CanViewMixin, DetailFormView): class ClubMembersView(ClubTabsMixin, CanViewMixin, DetailFormView):
""" """View of a club's members."""
View of a club's members
"""
model = Club model = Club
pk_url_kwarg = "club_id" pk_url_kwarg = "club_id"
@ -277,22 +255,20 @@ class ClubMembersView(ClubTabsMixin, CanViewMixin, DetailFormView):
current_tab = "members" current_tab = "members"
def get_form_kwargs(self): def get_form_kwargs(self):
kwargs = super(ClubMembersView, self).get_form_kwargs() kwargs = super().get_form_kwargs()
kwargs["request_user"] = self.request.user kwargs["request_user"] = self.request.user
kwargs["club"] = self.get_object() kwargs["club"] = self.get_object()
kwargs["club_members"] = self.members kwargs["club_members"] = self.members
return kwargs return kwargs
def get_context_data(self, *args, **kwargs): def get_context_data(self, *args, **kwargs):
kwargs = super(ClubMembersView, self).get_context_data(*args, **kwargs) kwargs = super().get_context_data(*args, **kwargs)
kwargs["members"] = self.members kwargs["members"] = self.members
return kwargs return kwargs
def form_valid(self, form): def form_valid(self, form):
""" """Check user rights."""
Check user rights resp = super().form_valid(form)
"""
resp = super(ClubMembersView, self).form_valid(form)
data = form.clean() data = form.clean()
users = data.pop("users", []) users = data.pop("users", [])
@ -307,7 +283,7 @@ class ClubMembersView(ClubTabsMixin, CanViewMixin, DetailFormView):
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
self.members = self.get_object().members.ongoing().order_by("-role") self.members = self.get_object().members.ongoing().order_by("-role")
return super(ClubMembersView, self).dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
def get_success_url(self, **kwargs): def get_success_url(self, **kwargs):
return reverse_lazy( return reverse_lazy(
@ -316,9 +292,7 @@ class ClubMembersView(ClubTabsMixin, CanViewMixin, DetailFormView):
class ClubOldMembersView(ClubTabsMixin, CanViewMixin, DetailView): class ClubOldMembersView(ClubTabsMixin, CanViewMixin, DetailView):
""" """Old members of a club."""
Old members of a club
"""
model = Club model = Club
pk_url_kwarg = "club_id" pk_url_kwarg = "club_id"
@ -327,9 +301,7 @@ class ClubOldMembersView(ClubTabsMixin, CanViewMixin, DetailView):
class ClubSellingView(ClubTabsMixin, CanEditMixin, DetailFormView): class ClubSellingView(ClubTabsMixin, CanEditMixin, DetailFormView):
""" """Sellings of a club."""
Sellings of a club
"""
model = Club model = Club
pk_url_kwarg = "club_id" pk_url_kwarg = "club_id"
@ -341,12 +313,12 @@ class ClubSellingView(ClubTabsMixin, CanEditMixin, DetailFormView):
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
try: try:
self.asked_page = int(request.GET.get("page", 1)) self.asked_page = int(request.GET.get("page", 1))
except ValueError: except ValueError as e:
raise Http404 raise Http404 from e
return super(ClubSellingView, self).dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
def get_form_kwargs(self): def get_form_kwargs(self):
kwargs = super(ClubSellingView, self).get_form_kwargs() kwargs = super().get_form_kwargs()
kwargs["club"] = self.object kwargs["club"] = self.object
return kwargs return kwargs
@ -354,7 +326,7 @@ class ClubSellingView(ClubTabsMixin, CanEditMixin, DetailFormView):
return self.get(request, *args, **kwargs) return self.get(request, *args, **kwargs)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs = super(ClubSellingView, self).get_context_data(**kwargs) kwargs = super().get_context_data(**kwargs)
qs = Selling.objects.filter(club=self.object) qs = Selling.objects.filter(club=self.object)
kwargs["result"] = qs[:0] kwargs["result"] = qs[:0]
@ -398,19 +370,17 @@ class ClubSellingView(ClubTabsMixin, CanEditMixin, DetailFormView):
kwargs["paginator"] = Paginator(kwargs["result"], self.paginate_by) kwargs["paginator"] = Paginator(kwargs["result"], self.paginate_by)
try: try:
kwargs["paginated_result"] = kwargs["paginator"].page(self.asked_page) kwargs["paginated_result"] = kwargs["paginator"].page(self.asked_page)
except InvalidPage: except InvalidPage as e:
raise Http404 raise Http404 from e
return kwargs return kwargs
class ClubSellingCSVView(ClubSellingView): class ClubSellingCSVView(ClubSellingView):
""" """Generate sellings in csv for a given period."""
Generate sellings in csv for a given period
"""
class StreamWriter: class StreamWriter:
"""Implements a file-like interface for streaming the CSV""" """Implements a file-like interface for streaming the CSV."""
def write(self, value): def write(self, value):
"""Write the value by returning it, instead of storing in a buffer.""" """Write the value by returning it, instead of storing in a buffer."""
@ -426,7 +396,8 @@ class ClubSellingCSVView(ClubSellingView):
row.append(selling.customer.user.get_display_name()) row.append(selling.customer.user.get_display_name())
else: else:
row.append("") row.append("")
row = row + [ row = [
*row,
selling.label, selling.label,
selling.quantity, selling.quantity,
selling.quantity * selling.unit_price, selling.quantity * selling.unit_price,
@ -437,7 +408,7 @@ class ClubSellingCSVView(ClubSellingView):
row.append(selling.product.purchase_price) row.append(selling.product.purchase_price)
row.append(selling.product.selling_price - selling.product.purchase_price) row.append(selling.product.selling_price - selling.product.purchase_price)
else: else:
row = row + ["", "", ""] row = [*row, "", "", ""]
return row return row
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
@ -484,9 +455,7 @@ class ClubSellingCSVView(ClubSellingView):
class ClubEditView(ClubTabsMixin, CanEditMixin, UpdateView): class ClubEditView(ClubTabsMixin, CanEditMixin, UpdateView):
""" """Edit a Club's main informations (for the club's members)."""
Edit a Club's main informations (for the club's members)
"""
model = Club model = Club
pk_url_kwarg = "club_id" pk_url_kwarg = "club_id"
@ -496,9 +465,7 @@ class ClubEditView(ClubTabsMixin, CanEditMixin, UpdateView):
class ClubEditPropView(ClubTabsMixin, CanEditPropMixin, UpdateView): class ClubEditPropView(ClubTabsMixin, CanEditPropMixin, UpdateView):
""" """Edit the properties of a Club object (for the Sith admins)."""
Edit the properties of a Club object (for the Sith admins)
"""
model = Club model = Club
pk_url_kwarg = "club_id" pk_url_kwarg = "club_id"
@ -508,9 +475,7 @@ class ClubEditPropView(ClubTabsMixin, CanEditPropMixin, UpdateView):
class ClubCreateView(CanCreateMixin, CreateView): class ClubCreateView(CanCreateMixin, CreateView):
""" """Create a club (for the Sith admin)."""
Create a club (for the Sith admin)
"""
model = Club model = Club
pk_url_kwarg = "club_id" pk_url_kwarg = "club_id"
@ -519,9 +484,7 @@ class ClubCreateView(CanCreateMixin, CreateView):
class MembershipSetOldView(CanEditMixin, DetailView): class MembershipSetOldView(CanEditMixin, DetailView):
""" """Set a membership as beeing old."""
Set a membership as beeing old
"""
model = Membership model = Membership
pk_url_kwarg = "membership_id" pk_url_kwarg = "membership_id"
@ -550,9 +513,7 @@ class MembershipSetOldView(CanEditMixin, DetailView):
class MembershipDeleteView(UserIsRootMixin, DeleteView): class MembershipDeleteView(UserIsRootMixin, DeleteView):
""" """Delete a membership (for admins only)."""
Delete a membership (for admins only)
"""
model = Membership model = Membership
pk_url_kwarg = "membership_id" pk_url_kwarg = "membership_id"
@ -566,15 +527,13 @@ class ClubStatView(TemplateView):
template_name = "club/stats.jinja" template_name = "club/stats.jinja"
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs = super(ClubStatView, self).get_context_data(**kwargs) kwargs = super().get_context_data(**kwargs)
kwargs["club_list"] = Club.objects.all() kwargs["club_list"] = Club.objects.all()
return kwargs return kwargs
class ClubMailingView(ClubTabsMixin, CanEditMixin, DetailFormView): class ClubMailingView(ClubTabsMixin, CanEditMixin, DetailFormView):
""" """A list of mailing for a given club."""
A list of mailing for a given club
"""
model = Club model = Club
form_class = MailingForm form_class = MailingForm
@ -583,7 +542,7 @@ class ClubMailingView(ClubTabsMixin, CanEditMixin, DetailFormView):
current_tab = "mailing" current_tab = "mailing"
def get_form_kwargs(self): def get_form_kwargs(self):
kwargs = super(ClubMailingView, self).get_form_kwargs() kwargs = super().get_form_kwargs()
kwargs["club_id"] = self.get_object().id kwargs["club_id"] = self.get_object().id
kwargs["user_id"] = self.request.user.id kwargs["user_id"] = self.request.user.id
kwargs["mailings"] = self.mailings kwargs["mailings"] = self.mailings
@ -591,10 +550,10 @@ class ClubMailingView(ClubTabsMixin, CanEditMixin, DetailFormView):
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
self.mailings = Mailing.objects.filter(club_id=self.get_object().id).all() self.mailings = Mailing.objects.filter(club_id=self.get_object().id).all()
return super(ClubMailingView, self).dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs = super(ClubMailingView, self).get_context_data(**kwargs) kwargs = super().get_context_data(**kwargs)
kwargs["club"] = self.get_object() kwargs["club"] = self.get_object()
kwargs["user"] = self.request.user kwargs["user"] = self.request.user
kwargs["mailings"] = self.mailings kwargs["mailings"] = self.mailings
@ -611,10 +570,8 @@ class ClubMailingView(ClubTabsMixin, CanEditMixin, DetailFormView):
} }
return kwargs return kwargs
def add_new_mailing(self, cleaned_data) -> ValidationError: def add_new_mailing(self, cleaned_data) -> ValidationError | None:
""" """Create a new mailing list from the form."""
Create a new mailing list from the form
"""
mailing = Mailing( mailing = Mailing(
club=self.get_object(), club=self.get_object(),
email=cleaned_data["mailing_email"], email=cleaned_data["mailing_email"],
@ -628,10 +585,8 @@ class ClubMailingView(ClubTabsMixin, CanEditMixin, DetailFormView):
mailing.save() mailing.save()
return None return None
def add_new_subscription(self, cleaned_data) -> ValidationError: def add_new_subscription(self, cleaned_data) -> ValidationError | None:
""" """Add mailing subscriptions for each user given and/or for the specified email in form."""
Add mailing subscriptions for each user given and/or for the specified email in form
"""
users_to_save = [] users_to_save = []
for user in cleaned_data["subscription_users"]: for user in cleaned_data["subscription_users"]:
@ -665,20 +620,16 @@ class ClubMailingView(ClubTabsMixin, CanEditMixin, DetailFormView):
return None return None
def remove_subscription(self, cleaned_data): def remove_subscription(self, cleaned_data):
""" """Remove specified users from a mailing list."""
Remove specified users from a mailing list
"""
fields = [ fields = [
cleaned_data[key] val for key, val in cleaned_data.items() if key.startswith("removal_")
for key in cleaned_data.keys()
if key.startswith("removal_")
] ]
for field in fields: for field in fields:
for sub in field: for sub in field:
sub.delete() sub.delete()
def form_valid(self, form): def form_valid(self, form):
resp = super(ClubMailingView, self).form_valid(form) resp = super().form_valid(form)
cleaned_data = form.clean() cleaned_data = form.clean()
error = None error = None
@ -710,7 +661,7 @@ class MailingDeleteView(CanEditMixin, DeleteView):
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
self.club_id = self.get_object().club.id self.club_id = self.get_object().club.id
return super(MailingDeleteView, self).dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
def get_success_url(self, **kwargs): def get_success_url(self, **kwargs):
if self.redirect_page: if self.redirect_page:
@ -726,9 +677,7 @@ class MailingSubscriptionDeleteView(CanEditMixin, DeleteView):
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
self.club_id = self.get_object().mailing.club.id self.club_id = self.get_object().mailing.club.id
return super(MailingSubscriptionDeleteView, self).dispatch( return super().dispatch(request, *args, **kwargs)
request, *args, **kwargs
)
def get_success_url(self, **kwargs): def get_success_url(self, **kwargs):
return reverse_lazy("club:mailing", kwargs={"club_id": self.club_id}) return reverse_lazy("club:mailing", kwargs={"club_id": self.club_id})
@ -739,7 +688,7 @@ class MailingAutoGenerationView(View):
self.mailing = get_object_or_404(Mailing, pk=kwargs["mailing_id"]) self.mailing = get_object_or_404(Mailing, pk=kwargs["mailing_id"])
if not request.user.can_edit(self.mailing): if not request.user.can_edit(self.mailing):
raise PermissionDenied raise PermissionDenied
return super(MailingAutoGenerationView, self).dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
club = self.mailing.club club = self.mailing.club
@ -753,25 +702,25 @@ class MailingAutoGenerationView(View):
class PosterListView(ClubTabsMixin, PosterListBaseView, CanViewMixin): class PosterListView(ClubTabsMixin, PosterListBaseView, CanViewMixin):
"""List communication posters""" """List communication posters."""
def get_object(self): def get_object(self):
return self.club return self.club
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs = super(PosterListView, self).get_context_data(**kwargs) kwargs = super().get_context_data(**kwargs)
kwargs["app"] = "club" kwargs["app"] = "club"
kwargs["club"] = self.club kwargs["club"] = self.club
return kwargs return kwargs
class PosterCreateView(PosterCreateBaseView, CanCreateMixin): class PosterCreateView(PosterCreateBaseView, CanCreateMixin):
"""Create communication poster""" """Create communication poster."""
pk_url_kwarg = "club_id" pk_url_kwarg = "club_id"
def get_object(self): def get_object(self):
obj = super(PosterCreateView, self).get_object() obj = super().get_object()
if not obj: if not obj:
return self.club return self.club
return obj return obj
@ -781,19 +730,19 @@ class PosterCreateView(PosterCreateBaseView, CanCreateMixin):
class PosterEditView(ClubTabsMixin, PosterEditBaseView, CanEditMixin): class PosterEditView(ClubTabsMixin, PosterEditBaseView, CanEditMixin):
"""Edit communication poster""" """Edit communication poster."""
def get_success_url(self): def get_success_url(self):
return reverse_lazy("club:poster_list", kwargs={"club_id": self.club.id}) return reverse_lazy("club:poster_list", kwargs={"club_id": self.club.id})
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs = super(PosterEditView, self).get_context_data(**kwargs) kwargs = super().get_context_data(**kwargs)
kwargs["app"] = "club" kwargs["app"] = "club"
return kwargs return kwargs
class PosterDeleteView(PosterDeleteBaseView, ClubTabsMixin, CanEditMixin): class PosterDeleteView(PosterDeleteBaseView, ClubTabsMixin, CanEditMixin):
"""Delete communication poster""" """Delete communication poster."""
def get_success_url(self): def get_success_url(self):
return reverse_lazy("club:poster_list", kwargs={"club_id": self.club.id}) return reverse_lazy("club:poster_list", kwargs={"club_id": self.club.id})

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