559 Commits

Author SHA1 Message Date
Sli
2a9cbfdf77 Add test case in user preferences for other subscriber accessing and accessing trombi user 2024-12-16 11:03:55 +01:00
Sli
0a460b46b9 Add tests for user preferences 2024-12-16 10:34:35 +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
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
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
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
876 changed files with 44378 additions and 69157 deletions

View File

@ -6,26 +6,20 @@ runs:
- name: Install apt packages
uses: awalsh128/cache-apt-pkgs-action@latest
with:
packages: gettext libgraphviz-dev
packages: gettext
version: 1.0 # increment to reset cache
- name: Install dependencies
run: |
sudo apt update
sudo apt install gettext libgraphviz-dev
shell: bash
- name: Set up python
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: "3.10"
python-version: "3.12"
- name: Load cached Poetry installation
id: cached-poetry
uses: actions/cache@v3
with:
path: ~/.local
key: poetry-0 # increment to reset cache
key: poetry-3 # increment to reset cache
- name: Install Poetry
if: steps.cached-poetry.outputs.cache-hit != 'true'

View File

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

View File

@ -1,4 +1,4 @@
name: Sith 3 CI
name: Sith CI
on:
push:
@ -8,21 +8,23 @@ on:
workflow_dispatch:
jobs:
ruff:
name: Ruff lint & format
pre-commit:
name: Launch pre-commits checks (ruff)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: ruff format
uses: chartboost/ruff-action@v1 # format
with:
args: format --diff
- name: ruff check
uses: chartboost/ruff-action@v1 # lint
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- uses: pre-commit/action@v3.0.1
with:
extra_args: --all-files
tests:
name: Run tests and generate coverage report
runs-on: ubuntu-latest
strategy:
fail-fast: false # don't interrupt the other test processes
matrix:
pytest-mark: [slow, not slow]
steps:
- name: Check out repository
uses: actions/checkout@v4
@ -30,7 +32,7 @@ jobs:
- uses: ./.github/actions/setup_xapian
- uses: ./.github/actions/compile_messages
- name: Run tests
run: poetry run coverage run -m pytest
run: poetry run coverage run -m pytest -m "${{ matrix.pytest-mark }}"
- name: Generate coverage report
run: |
poetry run coverage report

View File

@ -14,7 +14,7 @@ jobs:
steps:
- name: SSH Remote Commands
uses: appleboy/ssh-action@dce9d565de8d876c11d93fa4fe677c0285a66d78
uses: appleboy/ssh-action@v1.1.0
with:
# Proxy
proxy_host : ${{secrets.PROXY_HOST}}
@ -31,16 +31,17 @@ jobs:
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: |
export PATH="/home/sith/.local/bin:$PATH"
pushd ${{secrets.SITH_PATH}}
cd ${{secrets.SITH_PATH}}
git pull
poetry install
git fetch
git reset --hard origin/master
poetry install --with prod --without docs,tests
npm install
poetry run ./manage.py install_xapian
poetry run ./manage.py migrate
echo "yes" | poetry run ./manage.py collectstatic
poetry run ./manage.py compilestatic
poetry run ./manage.py collectstatic --clear --noinput
poetry run ./manage.py compilemessages
sudo systemctl restart uwsgi
@ -51,14 +52,14 @@ jobs:
timeout-minutes: 30
needs: deployment
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Sentry Release
uses: getsentry/action-release@v1.2.0
uses: getsentry/action-release@v1.7.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: 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: poetry run mkdocs gh-deploy --force

View File

@ -1,4 +1,4 @@
name: Sith3 taiste
name: Sith taiste
on:
push:
@ -13,7 +13,7 @@ jobs:
steps:
- name: SSH Remote Commands
uses: appleboy/ssh-action@dce9d565de8d876c11d93fa4fe677c0285a66d78
uses: appleboy/ssh-action@v1.1.0
with:
# Proxy
proxy_host : ${{secrets.PROXY_HOST}}
@ -30,34 +30,17 @@ jobs:
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: |
export PATH="$HOME/.poetry/bin:$PATH"
pushd ${{secrets.SITH_PATH}}
cd ${{secrets.SITH_PATH}}
git pull
poetry install
git fetch
git reset --hard origin/taiste
poetry install --with prod --without docs,tests
npm install
poetry run ./manage.py install_xapian
poetry run ./manage.py migrate
echo "yes" | poetry run ./manage.py collectstatic
poetry run ./manage.py compilestatic
poetry run ./manage.py collectstatic --clear --noinput
poetry run ./manage.py compilemessages
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

7
.gitignore vendored
View File

@ -1,4 +1,4 @@
db.sqlite3
*.sqlite3
*.log
*.pyc
*.mo
@ -17,4 +17,7 @@ sith/settings_custom.py
sith/search_indexes/
.coverage
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"]

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,31 +0,0 @@
# Contributors
Thanks to everyone who has contributed to this project! We appreciate your time and effort.
## Copyright
> All contributions to this project are subject to the copyright owned by the Association des Étudiants de l'Université de Technologie de Belfort-Montbéliard (AE UTBM), for both past and future years. By making a contribution to this project, you acknowledge and agree that AE UTBM has the exclusive right to use, distribute, and modify your contribution, as well as to license others to do the same, in any way they see fit.
## License
This project was previously released under the MIT license, but it has since been changed to the GPL v3 license. Any contributions made to this project before the switch to GPL v3 are still subject to the MIT license, while contributions made after the switch are subject to the GPL v3 license.
## List of Contributors
- [@Hyask](https://github.com/Hyask) — Florent "Skia" Jacquet <skia@hya.sk>
- [@klmp200](https://github.com/klmp200) — Antoine "Sli" Bartuccio <antoine@bartuccio.fr>
- [@nab-os](https://github.com/nab-os) (AKA Gnikwo) — Sasha "Nabos" Ballet <nabos@glargh.fr>
- [@Krophil](https://github.com/Krophil) — Pierre "Krophil'" Brunet <pierre.brunet@krophil.fr>
- [@guillaume-renaud](https://github.com/guillaume-renaud) — Guillaume "Lo-J" RENAUD <renaudg779@gmail.com>
- [@imperosol](https://github.com/imperosol) — Thomas "Maréchal" Girod <thgirod@hotmail.com>
- [@TheoDurr](https://github.com/TheoDurr) — Théo "Ailé" Durr <git@theodurr.fr>
- [@RTrioux](https://github.com/RTrioux) — Robin "Vial" Trioux
- [@TheRolfFR](https://github.com/TheRolfFR) — Yann "Réseau" Le Vaguerès
- [@Magador](https://github.com/Magador) — Lucie "Magador" Lenglet
- [@lsacienne](https://github.com/lsacienne) — Alexandre "L'Sacienne" Viala
- [@Juknum](https://github.com/Juknum) — Julien "Tinople" Constant
- [@Tartofraise](https://github.com/Tartofraise) — Ryan "Soldat" Hadj-Mebarek
- [@FrancescoWitz](https://github.com/FrancescoWitz) — Francesco "Och" WITZ
- [@maxence-leblanc](https://github.com/maxence-leblanc) — Maxence "Juste" LEBLANC
_If you've contributed to this project and your name isn't on the list, please let us know so we can add you. And if you've contributed anonymously, thank you! We appreciate your contributions just as much as those from named contributors._

View File

@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2016 Skia
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,40 +1,21 @@
<p align="center">
<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>
# Sith
<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>
<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.
</p>
</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>
#### If you want to contribute, here's how we recommend to read the docs:
* 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.
* Keep in mind that this documentation is thought to be read in order.
> This project is licensed under GNU GPL, see the LICENSE file at the top of the repository for more details.

View File

@ -1,19 +1,14 @@
# -*- coding:utf-8 -*-
#
# Copyright 2023 © AE UTBM
# ae@utbm.fr / ae.info@utbm.fr
# All contributors are listed in the CONTRIBUTORS file.
#
# This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr.
#
# You can find the whole source code 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)
# 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"
#
# PREVIOUSLY LICENSED UNDER THE MIT LICENSE,
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE.old
# OR WITHIN THE LOCAL FILE "LICENSE.old"
#

View File

@ -1,26 +1,30 @@
# -*- coding:utf-8 -*-
#
# Copyright 2023 © AE UTBM
# ae@utbm.fr / ae.info@utbm.fr
# All contributors are listed in the CONTRIBUTORS file.
#
# This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr.
#
# You can find the whole source code 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)
# 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"
#
# PREVIOUSLY LICENSED UNDER THE MIT LICENSE,
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE.old
# OR WITHIN THE LOCAL FILE "LICENSE.old"
#
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(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,4 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import django.core.validators

View File

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

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import phonenumber_field.modelfields

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import django.db.models.deletion
@ -46,6 +45,6 @@ class Migration(migrations.Migration):
),
),
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 django.db import migrations, models

View File

@ -1,21 +1,16 @@
# -*- coding:utf-8 -*-
#
# Copyright 2023 © AE UTBM
# ae@utbm.fr / ae.info@utbm.fr
# All contributors are listed in the CONTRIBUTORS file.
#
# This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr.
#
# You can find the whole source code 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)
# 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"
#
# PREVIOUSLY LICENSED UNDER THE MIT LICENSE,
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE.old
# OR WITHIN THE LOCAL FILE "LICENSE.old"
#
from decimal import Decimal
@ -34,22 +29,32 @@ from core.models import SithFile, User
class CurrencyField(models.DecimalField):
"""
This is a custom database field used for currency
"""
"""Custom database field used for currency."""
def __init__(self, *args, **kwargs):
kwargs["max_digits"] = 12
kwargs["decimal_places"] = 2
super(CurrencyField, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
def to_python(self, value):
try:
return super(CurrencyField, self).to_python(value).quantize(Decimal("0.01"))
return super().to_python(value).quantize(Decimal("0.01"))
except AttributeError:
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
@ -66,31 +71,8 @@ class Company(models.Model):
class Meta:
verbose_name = _("company")
def is_owned_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
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 __str__(self):
return self.name
def get_absolute_url(self):
return reverse("accounting:co_edit", kwargs={"co_id": self.id})
@ -98,8 +80,21 @@ class Company(models.Model):
def get_display_name(self):
return self.name
def __str__(self):
return self.name
def is_owned_by(self, user):
"""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):
@ -117,24 +112,20 @@ class BankAccount(models.Model):
verbose_name = _("Bank account")
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):
"""
Method to see if that object can be edited by the given user
"""
"""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
m = self.club.get_membership_for(user)
if 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
return m is not None and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]
class ClubAccount(models.Model):
@ -156,48 +147,33 @@ class ClubAccount(models.Model):
verbose_name = _("Club account")
ordering = ["bank_account", "name"]
def is_owned_by(self, user):
"""
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 __str__(self):
return self.name
def get_absolute_url(self):
return reverse("accounting:club_details", kwargs={"c_account_id": self.id})
def __str__(self):
return self.name
def is_owned_by(self, user):
"""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):
return _("%(club_account)s on %(bank_account)s") % {
@ -207,9 +183,7 @@ class ClubAccount(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"))
end_date = models.DateField(_("end date"), null=True, blank=True, default=None)
@ -229,36 +203,28 @@ class GeneralJournal(models.Model):
verbose_name = _("General journal")
ordering = ["-start_date"]
def is_owned_by(self, user):
"""
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 __str__(self):
return self.name
def get_absolute_url(self):
return reverse("accounting:journal_details", kwargs={"j_id": self.id})
def __str__(self):
return self.name
def is_owned_by(self, user):
"""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):
self.amount = 0
@ -276,9 +242,7 @@ class GeneralJournal(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"))
journal = models.ForeignKey(
@ -361,6 +325,18 @@ class Operation(models.Model):
unique_together = ("number", "journal")
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):
if attr == "target":
return self.get_target()
@ -368,7 +344,7 @@ class Operation(models.Model):
return object.__getattribute__(self, attr)
def clean(self):
super(Operation, self).clean()
super().clean()
if self.date is None:
raise ValidationError(_("The date must be set."))
elif self.date < self.journal.start_date:
@ -414,16 +390,8 @@ class Operation(models.Model):
tar = Company.objects.filter(id=self.target_id).first()
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):
"""
Method to see if that object can be edited by the given user
"""
"""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):
@ -431,40 +399,22 @@ class Operation(models.Model):
if self.journal.closed:
return False
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 True
return False
return m is not None and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]
def can_be_edited_by(self, user):
"""
Method to see if that object can be edited by the given 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
if self.journal.closed:
return False
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 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,
)
return m is not None and m.role == settings.SITH_CLUB_ROLES_ID["Treasurer"]
class AccountingType(models.Model):
"""
Class describing the accounting types.
"""Accounting types.
Thoses are numbers used in accounting to classify operations
Those are numbers used in accounting to classify operations
"""
code = models.CharField(
@ -491,27 +441,21 @@ class AccountingType(models.Model):
verbose_name = _("accounting type")
ordering = ["movement_type", "code"]
def is_owned_by(self, user):
"""
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 __str__(self):
return self.code + " - " + self.get_movement_type_display() + " - " + self.label
def get_absolute_url(self):
return reverse("accounting:type_list")
def __str__(self):
return self.code + " - " + self.get_movement_type_display() + " - " + self.label
def is_owned_by(self, user):
"""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 describing the simplified accounting types.
"""
"""Simplified version of `AccountingType`."""
label = models.CharField(_("label"), max_length=128)
accounting_type = models.ForeignKey(
@ -525,6 +469,15 @@ class SimplifiedAccountingType(models.Model):
verbose_name = _("simplified type")
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
def movement_type(self):
return self.accounting_type.movement_type
@ -532,21 +485,9 @@ class SimplifiedAccountingType(models.Model):
def get_movement_type_display(self):
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):
"""Label allow a club to sort its operations"""
"""Label allow a club to sort its operations."""
name = models.CharField(_("label"), max_length=64)
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" %}
{% block title %}
{% trans %}Accounting type list{% endtrans %}
{% trans %}Accounting type list{% endtrans %}
{% endblock %}
{% block content %}
<div id="accounting">
<p>
<a href="{{ url('accounting:bank_list') }}">{% trans %}Accounting{% endtrans %}</a> >
{% trans %}Accounting types{% endtrans %}
</p>
<hr>
<p><a href="{{ url('accounting:type_new') }}">{% trans %}New accounting type{% endtrans %}</a></p>
{% if accountingtype_list %}
<h3>{% trans %}Accounting type list{% endtrans %}</h3>
<ul>
{% for a in accountingtype_list %}
<li><a href="{{ url('accounting:type_edit', type_id=a.id) }}">{{ a }}</a></li>
{% endfor %}
</ul>
{% else %}
{% trans %}There is no types in this website.{% endtrans %}
{% endif %}
</div>
<div id="accounting">
<p>
<a href="{{ url('accounting:bank_list') }}">{% trans %}Accounting{% endtrans %}</a> >
{% trans %}Accounting types{% endtrans %}
</p>
<hr>
<p><a href="{{ url('accounting:type_new') }}">{% trans %}New accounting type{% endtrans %}</a></p>
{% if accountingtype_list %}
<h3>{% trans %}Accounting type list{% endtrans %}</h3>
<ul>
{% for a in accountingtype_list %}
<li><a href="{{ url('accounting:type_edit', type_id=a.id) }}">{{ a }}</a></li>
{% endfor %}
</ul>
{% else %}
{% trans %}There is no types in this website.{% endtrans %}
{% endif %}
</div>
{% endblock %}

View File

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

View File

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

View File

@ -1,68 +1,68 @@
{% extends "core/base.jinja" %}
{% block title %}
{% trans %}Club account:{% endtrans %} {{ object.name }}
{% trans %}Club account:{% endtrans %} {{ object.name }}
{% endblock %}
{% block content %}
<div id="accounting">
<p>
<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> >
{{ object }}
</p>
<hr>
<h2>{% trans %}Club account:{% endtrans %} {{ object.name }}</h2>
{% if user.is_root and not object.journals.exists() %}
<a href="{{ url('accounting:club_delete', c_account_id=object.id) }}">{% trans %}Delete{% endtrans %}</a>
{% endif %}
{% 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>
{% endif %}
<p><a href="{{ url('accounting:label_list', clubaccount_id=object.id) }}">{% trans %}Label list{% endtrans %}</a></p>
{% if not object.has_open_journal() %}
<p><a href="{{ url('accounting:journal_new') }}?parent={{ object.id }}">{% trans %}New journal{% endtrans %}</a></p>
{% else %}
<p>{% trans %}You can not create new journal while you still have one opened{% endtrans %}</p>
{% endif %}
<table>
<thead>
<tr>
<td>{% trans %}Name{% endtrans %}</td>
<td>{% trans %}Start{% endtrans %}</td>
<td>{% trans %}End{% endtrans %}</td>
<td>{% trans %}Amount{% endtrans %}</td>
<td>{% trans %}Effective amount{% endtrans %}</td>
<td>{% trans %}Closed{% endtrans %}</td>
<td>{% trans %}Actions{% endtrans %}</td>
</tr>
</thead>
<tbody>
{% for j in object.journals.all() %}
<tr>
<td>{{ j.name }}</td>
<td>{{ j.start_date }}</td>
{% if j.end_date %}
<td>{{ j.end_date }}</td>
{% else %}
<td> - </td>
{% endif %}
<td>{{ j.amount }} €</td>
<td>{{ j.effective_amount }} €</td>
{% if j.closed %}
<td>{% trans %}Yes{% endtrans %}</td>
{% else %}
<td>{% trans %}No{% endtrans %}</td>
{% endif %}
<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>
{% 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>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div id="accounting">
<p>
<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> >
{{ object }}
</p>
<hr>
<h2>{% trans %}Club account:{% endtrans %} {{ object.name }}</h2>
{% if user.is_root and not object.journals.exists() %}
<a href="{{ url('accounting:club_delete', c_account_id=object.id) }}">{% trans %}Delete{% endtrans %}</a>
{% endif %}
{% 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>
{% endif %}
<p><a href="{{ url('accounting:label_list', clubaccount_id=object.id) }}">{% trans %}Label list{% endtrans %}</a></p>
{% if not object.has_open_journal() %}
<p><a href="{{ url('accounting:journal_new') }}?parent={{ object.id }}">{% trans %}New journal{% endtrans %}</a></p>
{% else %}
<p>{% trans %}You can not create new journal while you still have one opened{% endtrans %}</p>
{% endif %}
<table>
<thead>
<tr>
<td>{% trans %}Name{% endtrans %}</td>
<td>{% trans %}Start{% endtrans %}</td>
<td>{% trans %}End{% endtrans %}</td>
<td>{% trans %}Amount{% endtrans %}</td>
<td>{% trans %}Effective amount{% endtrans %}</td>
<td>{% trans %}Closed{% endtrans %}</td>
<td>{% trans %}Actions{% endtrans %}</td>
</tr>
</thead>
<tbody>
{% for j in object.journals.all() %}
<tr>
<td>{{ j.name }}</td>
<td>{{ j.start_date }}</td>
{% if j.end_date %}
<td>{{ j.end_date }}</td>
{% else %}
<td> - </td>
{% endif %}
<td>{{ j.amount }} €</td>
<td>{{ j.effective_amount }} €</td>
{% if j.closed %}
<td>{% trans %}Yes{% endtrans %}</td>
{% else %}
<td>{% trans %}No{% endtrans %}</td>
{% endif %}
<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>
{% 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>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

View File

@ -1,30 +1,30 @@
{% extends "core/base.jinja" %}
{% block title %}
{% trans %}Company list{% endtrans %}
{% trans %}Company list{% endtrans %}
{% endblock %}
{% block content %}
<div id="accounting">
{% if user.is_root
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>
{% endif %}
<br/>
<table>
<thead>
<tr>
<td>{% trans %}Companies{% endtrans %}</td>
</tr>
</thead>
<tbody>
{% for o in object_list %}
<tr>
<td><a href="{{ url('accounting:co_edit', co_id=o.id) }}">{{ o.get_display_name() }}</a></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div id="accounting">
{% if user.is_root
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>
{% endif %}
<br/>
<table>
<thead>
<tr>
<td>{% trans %}Companies{% endtrans %}</td>
</tr>
</thead>
<tbody>
{% for o in object_list %}
<tr>
<td><a href="{{ url('accounting:co_edit', co_id=o.id) }}">{{ o.get_display_name() }}</a></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

View File

@ -1,103 +1,103 @@
{% extends "core/base.jinja" %}
{% block title %}
{% trans %}General journal:{% endtrans %} {{ object.name }}
{% trans %}General journal:{% endtrans %} {{ object.name }}
{% endblock %}
{% block content %}
<div id="accounting">
<p>
<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:club_details', c_account_id=object.club_account.id) }}">{{ object.club_account }}</a> >
{{ object.name }}
</p>
<hr>
<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_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><strong>{% trans %}Amount: {% endtrans %}</strong>{{ object.amount }} € -
<strong>{% trans %}Effective amount: {% endtrans %}</strong>{{ object.effective_amount }} €</p>
{% if object.closed %}
<p>{% trans %}Journal is closed, you can not create operation{% endtrans %}</p>
{% else %}
<p><a href="{{ url('accounting:op_new', j_id=object.id) }}">{% trans %}New operation{% endtrans %}</a></p>
</br>
{% endif %}
<div class="journal-table">
<table>
<thead>
<div id="accounting">
<p>
<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:club_details', c_account_id=object.club_account.id) }}">{{ object.club_account }}</a> >
{{ object.name }}
</p>
<hr>
<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_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><strong>{% trans %}Amount: {% endtrans %}</strong>{{ object.amount }} € -
<strong>{% trans %}Effective amount: {% endtrans %}</strong>{{ object.effective_amount }} €</p>
{% if object.closed %}
<p>{% trans %}Journal is closed, you can not create operation{% endtrans %}</p>
{% else %}
<p><a href="{{ url('accounting:op_new', j_id=object.id) }}">{% trans %}New operation{% endtrans %}</a></p>
</br>
{% endif %}
<div class="journal-table">
<table>
<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>
<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>
<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.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>
{% else %}
{% else %}
<td><a href="{{ o.target.get_absolute_url() }}">{{ o.target.get_display_name() }}</a></td>
{% endif %}
<td>{{ o.accounting_type.code }}</td>
<td>{{ o.accounting_type.label }}</td>
{% if o.done %}
{% endif %}
<td>{{ o.accounting_type.code }}</td>
<td>{{ o.accounting_type.label }}</td>
{% if o.done %}
<td>{% trans %}Yes{% endtrans %}</td>
{% else %}
{% else %}
<td>{% trans %}No{% endtrans %}</td>
{% endif %}
<td>{{ o.remark }}
{% endif %}
<td>{{ o.remark }}
{% if not o.linked_operation and o.target_type == "ACCOUNT" and not o.target.has_open_journal() %}
<p><strong>
{% trans %}Warning: this operation has no linked operation because the targeted club account has no opened journal.{% endtrans %}
</strong></p>
<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 %}
</strong></p>
<p><strong>
{% trans %}Warning: this operation has no linked operation because the targeted club account has no opened journal.{% endtrans %}
</strong></p>
<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 %}
</strong></p>
{% endif %}
</td>
{% if o.invoice %}
</td>
{% if o.invoice %}
<td><a href="{{ url('core:download', file_id=o.invoice.id) }}">{{ o.invoice.name }}</a></td>
{% else %}
{% else %}
<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 %}
<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>
<td><a href="{{ url('accounting:op_pdf', op_id=o.id) }}">{% trans %}Generate{% endtrans %}</a></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
</td>
<td><a href="{{ url('accounting:op_pdf', op_id=o.id) }}">{% trans %}Generate{% endtrans %}</a></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}

View File

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

View File

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

View File

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

View File

@ -1,36 +1,36 @@
{% extends "core/base.jinja" %}
{% block title %}
{% trans %}Label list{% endtrans %}
{% trans %}Label list{% endtrans %}
{% endblock %}
{% block content %}
<div id="accounting">
<p>
<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:club_details', c_account_id=object.id) }}">{{ object }}</a>
</p>
<hr>
<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) %}
<p><a href="{{ url('accounting:label_new') }}?parent={{ object.id }}">{% trans %}New label{% endtrans %}</a></p>
{% endif %}
{% if object.labels.all() %}
<h3>{% trans %}Label list{% endtrans %}</h3>
<ul>
{% for l in object.labels.all() %}
<li><a href="{{ url('accounting:label_edit', label_id=l.id) }}">{{ l }}</a>
<div id="accounting">
<p>
<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:club_details', c_account_id=object.id) }}">{{ object }}</a>
</p>
<hr>
<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) %}
<p><a href="{{ url('accounting:label_new') }}?parent={{ object.id }}">{% trans %}New label{% endtrans %}</a></p>
{% endif %}
{% if object.labels.all() %}
<h3>{% trans %}Label list{% endtrans %}</h3>
<ul>
{% for l in object.labels.all() %}
<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) %}
-
<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 %}
</li>
{% endfor %}
</ul>
{% else %}
{% trans %}There is no label in this club account.{% endtrans %}
{% endif %}
</div>
</li>
{% endfor %}
</ul>
{% else %}
{% trans %}There is no label in this club account.{% endtrans %}
{% endif %}
</div>
{% endblock %}

View File

@ -1,123 +1,123 @@
{% extends "core/base.jinja" %}
{% block title %}
{% trans %}Edit operation{% endtrans %}
{% trans %}Edit operation{% endtrans %}
{% endblock %}
{% block content %}
<div id="accounting">
<div id="accounting">
<p>
<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: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> >
{% trans %}Edit operation{% endtrans %}
<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: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> >
{% trans %}Edit operation{% endtrans %}
</p>
<hr>
<h2>{% trans %}Edit operation{% endtrans %}</h2>
<form action="" method="post">
{% csrf_token %}
{{ form.non_field_errors() }}
{{ form.journal }}
{{ form.target_id }}
<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>
<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>
<p>{{ form.target_type.errors }}<label for="{{ form.target_type.name }}">{{ form.target_type.label }}</label> {{ form.target_type }}</p>
{{ form.user }}
{{ form.club }}
{{ form.club_account }}
{{ form.company }}
{{ form.target_label }}
<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.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> {{
{% csrf_token %}
{{ form.non_field_errors() }}
{{ form.journal }}
{{ form.target_id }}
<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>
<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>
<p>{{ form.target_type.errors }}<label for="{{ form.target_type.name }}">{{ form.target_type.label }}</label> {{ form.target_type }}</p>
{{ form.user }}
{{ form.club }}
{{ form.club_account }}
{{ form.company }}
{{ form.target_label }}
<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.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> {{
form.cheque_number }}</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 }}">{{
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.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 }}">{{
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> {{
form.accounting_type }}</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>
{% if form.instance.linked_operation %}
<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>
{% if form.instance.linked_operation %}
{% set obj = form.instance.linked_operation %}
<p><strong>{% trans %}Linked operation:{% endtrans %}</strong><br>
<a href="{{ url('accounting:bank_details', b_account_id=obj.journal.club_account.bank_account.id) }}">
{{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:journal_details', j_id=obj.journal.id) }}">{{ obj.journal }}</a> >
{{ obj.number }}
<a href="{{ url('accounting:bank_details', b_account_id=obj.journal.club_account.bank_account.id) }}">
{{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:journal_details', j_id=obj.journal.id) }}">{{ obj.journal }}</a> >
{{ obj.number }}
</p>
{% endif %}
<p><input type="submit" value="{% trans %}Save{% endtrans %}" /></p>
{% endif %}
<p><input type="submit" value="{% trans %}Save{% endtrans %}" /></p>
</form>
{% endblock %}
{% endblock %}
{% block script %}
{{ super() }}
<script>
{% block script %}
{{ super() }}
<script>
$( function() {
var target_type = $('#id_target_type');
var user = $('#id_user_wrapper');
var club = $('#id_club_wrapper');
var club_account = $('#id_club_account_wrapper');
var company = $('#id_company_wrapper');
var other = $('#id_target_label');
var need_link = $('#id_need_link_full');
function update_targets () {
if (target_type.val() == "USER") {
console.log(user);
user.show();
club.hide();
club_account.hide();
company.hide();
other.hide();
need_link.hide();
} else if (target_type.val() == "ACCOUNT") {
club_account.show();
need_link.show();
user.hide();
club.hide();
company.hide();
other.hide();
} else if (target_type.val() == "CLUB") {
club.show();
user.hide();
club_account.hide();
company.hide();
other.hide();
need_link.hide();
} else if (target_type.val() == "COMPANY") {
company.show();
user.hide();
club_account.hide();
club.hide();
other.hide();
need_link.hide();
} else if (target_type.val() == "OTHER") {
other.show();
user.hide();
club.hide();
club_account.hide();
company.hide();
need_link.hide();
} else {
company.hide();
user.hide();
club_account.hide();
club.hide();
other.hide();
need_link.hide();
}
}
update_targets();
target_type.change(update_targets);
} );
</script>
</div>
var target_type = $('#id_target_type');
var user = $('user-ajax-select');
var club = $('club-ajax-select');
var club_account = $('club-account-ajax-select');
var company = $('company-ajax-select');
var other = $('#id_target_label');
var need_link = $('#id_need_link_full');
function update_targets () {
if (target_type.val() == "USER") {
console.log(user);
user.show();
club.hide();
club_account.hide();
company.hide();
other.hide();
need_link.hide();
} else if (target_type.val() == "ACCOUNT") {
club_account.show();
need_link.show();
user.hide();
club.hide();
company.hide();
other.hide();
} else if (target_type.val() == "CLUB") {
club.show();
user.hide();
club_account.hide();
company.hide();
other.hide();
need_link.hide();
} else if (target_type.val() == "COMPANY") {
company.show();
user.hide();
club_account.hide();
club.hide();
other.hide();
need_link.hide();
} else if (target_type.val() == "OTHER") {
other.show();
user.hide();
club.hide();
club_account.hide();
company.hide();
need_link.hide();
} else {
company.hide();
user.hide();
club_account.hide();
club.hide();
other.hide();
need_link.hide();
}
}
update_targets();
target_type.change(update_targets);
} );
</script>
</div>
{% endblock %}

View File

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

View File

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

View File

@ -1,21 +1,16 @@
# -*- coding:utf-8 -*-
#
# Copyright 2023 © AE UTBM
# ae@utbm.fr / ae.info@utbm.fr
# All contributors are listed in the CONTRIBUTORS file.
#
# This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr.
#
# You can find the whole source code 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)
# 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"
#
# PREVIOUSLY LICENSED UNDER THE MIT LICENSE,
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE.old
# OR WITHIN THE LOCAL FILE "LICENSE.old"
#
from datetime import date, timedelta
@ -33,7 +28,7 @@ from accounting.models import (
from core.models import User
class RefoundAccountTest(TestCase):
class TestRefoundAccount(TestCase):
@classmethod
def setUpTestData(cls):
cls.skia = User.objects.get(username="skia")
@ -72,7 +67,7 @@ class RefoundAccountTest(TestCase):
assert self.skia.customer.amount == 0
class JournalTest(TestCase):
class TestJournal(TestCase):
@classmethod
def setUpTestData(cls):
cls.journal = GeneralJournal.objects.get(id=1)
@ -96,7 +91,7 @@ class JournalTest(TestCase):
assert "<td>M\xc3\xa9thode de paiement</td>" not in str(response_get.content)
class OperationTest(TestCase):
class TestOperation(TestCase):
def setUp(self):
self.tomorrow_formatted = (date.today() + timedelta(days=1)).strftime(
"%d/%m/%Y"
@ -107,7 +102,7 @@ class OperationTest(TestCase):
code="443", label="Ce code n'existe pas", movement_type="CREDIT"
)
at.save()
l = Label.objects.create(club_account=self.journal.club_account, name="bob")
label = Label.objects.create(club_account=self.journal.club_account, name="bob")
self.client.force_login(User.objects.get(username="comptable"))
self.op1 = Operation(
journal=self.journal,
@ -116,7 +111,7 @@ class OperationTest(TestCase):
remark="Test bilan",
mode="CASH",
done=True,
label=l,
label=label,
accounting_type=at,
target_type="USER",
target_id=self.skia.id,
@ -129,7 +124,7 @@ class OperationTest(TestCase):
remark="Test bilan",
mode="CASH",
done=True,
label=l,
label=label,
accounting_type=at,
target_type="USER",
target_id=self.skia.id,
@ -269,33 +264,26 @@ class OperationTest(TestCase):
)
self.assertContains(response, "Total : 5575.72", status_code=200)
self.assertContains(response, "Total : 71.42")
self.assertContains(
response,
"""
<td><a href="/user/1/">S&#39; Kia</a></td>
<td>3.00</td>""",
content = response.content.decode()
self.assertInHTML(
"""<td><a href="/user/1/">S&#39; Kia</a></td><td>3.00</td>""", content
)
self.assertContains(
response,
"""
<td><a href="/user/1/">S&#39; Kia</a></td>
<td>823.00</td>""",
self.assertInHTML(
"""<td><a href="/user/1/">S&#39; Kia</a></td><td>823.00</td>""", content
)
def test_accounting_statement(self):
response = self.client.get(
reverse("accounting:journal_accounting_statement", args=[self.journal.id])
)
self.assertContains(
response,
assert response.status_code == 200
self.assertInHTML(
"""
<tr>
<td>443 - Crédit - Ce code n&#39;existe pas</td>
<td>3.00</td>
</tr>""",
status_code=200,
response.content.decode(),
)
self.assertContains(
response,

View File

@ -1,26 +1,55 @@
# -*- coding:utf-8 -*-
#
# Copyright 2023 © AE UTBM
# ae@utbm.fr / ae.info@utbm.fr
# All contributors are listed in the CONTRIBUTORS file.
#
# This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr.
#
# You can find the whole source code 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)
# 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"
#
# PREVIOUSLY LICENSED UNDER THE MIT LICENSE,
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE.old
# OR WITHIN THE LOCAL FILE "LICENSE.old"
#
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 = [
# Accounting types

View File

@ -1,26 +1,20 @@
# -*- coding:utf-8 -*-
#
# Copyright 2023 © AE UTBM
# ae@utbm.fr / ae.info@utbm.fr
# All contributors are listed in the CONTRIBUTORS file.
#
# This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr.
#
# You can find the whole source code 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)
# 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"
#
# PREVIOUSLY LICENSED UNDER THE MIT LICENSE,
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE.old
# OR WITHIN THE LOCAL FILE "LICENSE.old"
#
import collections
from ajax_select.fields import AutoCompleteSelectField
from django import forms
from django.conf import settings
from django.core.exceptions import PermissionDenied, ValidationError
@ -44,6 +38,13 @@ from accounting.models import (
Operation,
SimplifiedAccountingType,
)
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,
@ -52,15 +53,14 @@ from core.views import (
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
class BankAccountListView(CanViewMixin, ListView):
"""
A list view for the admins
"""
"""A list view for the admins."""
model = BankAccount
template_name = "accounting/bank_account_list.jinja"
@ -71,18 +71,14 @@ class BankAccountListView(CanViewMixin, ListView):
class SimplifiedAccountingTypeListView(CanViewMixin, ListView):
"""
A list view for the admins
"""
"""A list view for the admins."""
model = SimplifiedAccountingType
template_name = "accounting/simplifiedaccountingtype_list.jinja"
class SimplifiedAccountingTypeEditView(CanViewMixin, UpdateView):
"""
An edit view for the admins
"""
"""An edit view for the admins."""
model = SimplifiedAccountingType
pk_url_kwarg = "type_id"
@ -91,9 +87,7 @@ class SimplifiedAccountingTypeEditView(CanViewMixin, UpdateView):
class SimplifiedAccountingTypeCreateView(CanCreateMixin, CreateView):
"""
Create an accounting type (for the admins)
"""
"""Create an accounting type (for the admins)."""
model = SimplifiedAccountingType
fields = ["label", "accounting_type"]
@ -104,18 +98,14 @@ class SimplifiedAccountingTypeCreateView(CanCreateMixin, CreateView):
class AccountingTypeListView(CanViewMixin, ListView):
"""
A list view for the admins
"""
"""A list view for the admins."""
model = AccountingType
template_name = "accounting/accountingtype_list.jinja"
class AccountingTypeEditView(CanViewMixin, UpdateView):
"""
An edit view for the admins
"""
"""An edit view for the admins."""
model = AccountingType
pk_url_kwarg = "type_id"
@ -124,9 +114,7 @@ class AccountingTypeEditView(CanViewMixin, UpdateView):
class AccountingTypeCreateView(CanCreateMixin, CreateView):
"""
Create an accounting type (for the admins)
"""
"""Create an accounting type (for the admins)."""
model = AccountingType
fields = ["code", "label", "movement_type"]
@ -137,9 +125,7 @@ class AccountingTypeCreateView(CanCreateMixin, CreateView):
class BankAccountEditView(CanViewMixin, UpdateView):
"""
An edit view for the admins
"""
"""An edit view for the admins."""
model = BankAccount
pk_url_kwarg = "b_account_id"
@ -148,9 +134,7 @@ class BankAccountEditView(CanViewMixin, UpdateView):
class BankAccountDetailView(CanViewMixin, DetailView):
"""
A detail view, listing every club account
"""
"""A detail view, listing every club account."""
model = BankAccount
pk_url_kwarg = "b_account_id"
@ -158,9 +142,7 @@ class BankAccountDetailView(CanViewMixin, DetailView):
class BankAccountCreateView(CanCreateMixin, CreateView):
"""
Create a bank account (for the admins)
"""
"""Create a bank account (for the admins)."""
model = BankAccount
fields = ["name", "club", "iban", "number"]
@ -170,9 +152,7 @@ class BankAccountCreateView(CanCreateMixin, CreateView):
class BankAccountDeleteView(
CanEditPropMixin, DeleteView
): # TODO change Delete to Close
"""
Delete a bank account (for the admins)
"""
"""Delete a bank account (for the admins)."""
model = BankAccount
pk_url_kwarg = "b_account_id"
@ -184,9 +164,7 @@ class BankAccountDeleteView(
class ClubAccountEditView(CanViewMixin, UpdateView):
"""
An edit view for the admins
"""
"""An edit view for the admins."""
model = ClubAccount
pk_url_kwarg = "c_account_id"
@ -195,9 +173,7 @@ class ClubAccountEditView(CanViewMixin, UpdateView):
class ClubAccountDetailView(CanViewMixin, DetailView):
"""
A detail view, listing every journal
"""
"""A detail view, listing every journal."""
model = ClubAccount
pk_url_kwarg = "c_account_id"
@ -205,17 +181,15 @@ class ClubAccountDetailView(CanViewMixin, DetailView):
class ClubAccountCreateView(CanCreateMixin, CreateView):
"""
Create a club account (for the admins)
"""
"""Create a club account (for the admins)."""
model = ClubAccount
fields = ["name", "club", "bank_account"]
template_name = "core/create.jinja"
def get_initial(self):
ret = super(ClubAccountCreateView, self).get_initial()
if "parent" in self.request.GET.keys():
ret = super().get_initial()
if "parent" in self.request.GET:
obj = BankAccount.objects.filter(id=int(self.request.GET["parent"])).first()
if obj is not None:
ret["bank_account"] = obj.id
@ -225,9 +199,7 @@ class ClubAccountCreateView(CanCreateMixin, CreateView):
class ClubAccountDeleteView(
CanEditPropMixin, DeleteView
): # TODO change Delete to Close
"""
Delete a club account (for the admins)
"""
"""Delete a club account (for the admins)."""
model = ClubAccount
pk_url_kwarg = "c_account_id"
@ -287,9 +259,7 @@ class JournalTabsMixin(TabedViewMixin):
class JournalCreateView(CanCreateMixin, CreateView):
"""
Create a general journal
"""
"""Create a general journal."""
model = GeneralJournal
form_class = modelform_factory(
@ -300,8 +270,8 @@ class JournalCreateView(CanCreateMixin, CreateView):
template_name = "core/create.jinja"
def get_initial(self):
ret = super(JournalCreateView, self).get_initial()
if "parent" in self.request.GET.keys():
ret = super().get_initial()
if "parent" in self.request.GET:
obj = ClubAccount.objects.filter(id=int(self.request.GET["parent"])).first()
if obj is not None:
ret["club_account"] = obj.id
@ -309,9 +279,7 @@ class JournalCreateView(CanCreateMixin, CreateView):
class JournalDetailView(JournalTabsMixin, CanViewMixin, DetailView):
"""
A detail view, listing every operation
"""
"""A detail view, listing every operation."""
model = GeneralJournal
pk_url_kwarg = "j_id"
@ -320,9 +288,7 @@ class JournalDetailView(JournalTabsMixin, CanViewMixin, DetailView):
class JournalEditView(CanEditMixin, UpdateView):
"""
Update a general journal
"""
"""Update a general journal."""
model = GeneralJournal
pk_url_kwarg = "j_id"
@ -331,9 +297,7 @@ class JournalEditView(CanEditMixin, UpdateView):
class JournalDeleteView(CanEditPropMixin, DeleteView):
"""
Delete a club account (for the admins)
"""
"""Delete a club account (for the admins)."""
model = GeneralJournal
pk_url_kwarg = "j_id"
@ -343,7 +307,7 @@ class JournalDeleteView(CanEditPropMixin, DeleteView):
def dispatch(self, request, *args, **kwargs):
self.object = self.get_object()
if self.object.operations.count() == 0:
return super(JournalDeleteView, self).dispatch(request, *args, **kwargs)
return super().dispatch(request, *args, **kwargs)
else:
raise PermissionDenied
@ -377,12 +341,30 @@ class OperationForm(forms.ModelForm):
"invoice": SelectFile,
}
user = AutoCompleteSelectField("users", help_text=None, required=False)
club_account = AutoCompleteSelectField(
"club_accounts", help_text=None, required=False
user = forms.ModelChoiceField(
help_text=None,
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(
label=_("Link this operation to the target account"),
required=False,
@ -391,7 +373,7 @@ class OperationForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
club_account = kwargs.pop("club_account", None)
super(OperationForm, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
if club_account:
self.fields["label"].queryset = club_account.labels.order_by("name").all()
if self.instance.target_type == "USER":
@ -404,8 +386,8 @@ class OperationForm(forms.ModelForm):
self.fields["company"].initial = self.instance.target_id
def clean(self):
self.cleaned_data = super(OperationForm, self).clean()
if "target_type" in self.cleaned_data.keys():
self.cleaned_data = super().clean()
if "target_type" in self.cleaned_data:
if (
self.cleaned_data.get("user") is None
and self.cleaned_data.get("club") is None
@ -434,7 +416,7 @@ class OperationForm(forms.ModelForm):
return self.cleaned_data
def save(self):
ret = super(OperationForm, self).save()
ret = super().save()
if (
self.instance.target_type == "ACCOUNT"
and not self.instance.linked_operation
@ -472,9 +454,7 @@ class OperationForm(forms.ModelForm):
class OperationCreateView(CanCreateMixin, CreateView):
"""
Create an operation
"""
"""Create an operation."""
model = Operation
form_class = OperationForm
@ -486,23 +466,21 @@ class OperationCreateView(CanCreateMixin, CreateView):
return self.form_class(club_account=ca, **self.get_form_kwargs())
def get_initial(self):
ret = super(OperationCreateView, self).get_initial()
ret = super().get_initial()
if self.journal is not None:
ret["journal"] = self.journal.id
return ret
def get_context_data(self, **kwargs):
"""Add journal to the context"""
kwargs = super(OperationCreateView, self).get_context_data(**kwargs)
"""Add journal to the context."""
kwargs = super().get_context_data(**kwargs)
if self.journal:
kwargs["object"] = self.journal
return kwargs
class OperationEditView(CanEditMixin, UpdateView):
"""
An edit view, working as detail for the moment
"""
"""An edit view, working as detail for the moment."""
model = Operation
pk_url_kwarg = "op_id"
@ -510,16 +488,14 @@ class OperationEditView(CanEditMixin, UpdateView):
template_name = "accounting/operation_edit.jinja"
def get_context_data(self, **kwargs):
"""Add journal to the context"""
kwargs = super(OperationEditView, self).get_context_data(**kwargs)
"""Add journal to the context."""
kwargs = super().get_context_data(**kwargs)
kwargs["object"] = self.object.journal
return kwargs
class OperationPDFView(CanViewMixin, DetailView):
"""
Display the PDF of a given operation
"""
"""Display the PDF of a given operation."""
model = Operation
pk_url_kwarg = "op_id"
@ -671,9 +647,7 @@ class OperationPDFView(CanViewMixin, DetailView):
class JournalNatureStatementView(JournalTabsMixin, CanViewMixin, DetailView):
"""
Display a statement sorted by labels
"""
"""Display a statement sorted by labels."""
model = GeneralJournal
pk_url_kwarg = "j_id"
@ -684,19 +658,17 @@ class JournalNatureStatementView(JournalTabsMixin, CanViewMixin, DetailView):
ret = collections.OrderedDict()
statement = collections.OrderedDict()
total_sum = 0
for sat in [None] + list(
SimplifiedAccountingType.objects.order_by("label").all()
):
sum = queryset.filter(
for sat in [
None,
*list(SimplifiedAccountingType.objects.order_by("label")),
]:
amount = queryset.filter(
accounting_type__movement_type=movement_type, simpleaccounting_type=sat
).aggregate(amount_sum=Sum("amount"))["amount_sum"]
if sat:
sat = sat.label
else:
sat = ""
if sum:
total_sum += sum
statement[sat] = sum
label = sat.label if sat is not None else ""
if amount:
total_sum += amount
statement[label] = amount
ret[movement_type] = statement
ret[movement_type + "_sum"] = total_sum
return ret
@ -719,28 +691,23 @@ class JournalNatureStatementView(JournalTabsMixin, CanViewMixin, DetailView):
self.statement(self.object.operations.filter(label=None).all(), "DEBIT")
)
statement[_("No label operations")] = no_label_statement
for l in labels:
for label in labels:
l_stmt = collections.OrderedDict()
l_stmt.update(
self.statement(self.object.operations.filter(label=l).all(), "CREDIT")
)
l_stmt.update(
self.statement(self.object.operations.filter(label=l).all(), "DEBIT")
)
statement[l] = l_stmt
journals = self.object.operations.filter(label=label).all()
l_stmt.update(self.statement(journals, "CREDIT"))
l_stmt.update(self.statement(journals, "DEBIT"))
statement[label] = l_stmt
return statement
def get_context_data(self, **kwargs):
"""Add infos to the context"""
kwargs = super(JournalNatureStatementView, self).get_context_data(**kwargs)
"""Add infos to the context."""
kwargs = super().get_context_data(**kwargs)
kwargs["statement"] = self.big_statement()
return kwargs
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
pk_url_kwarg = "j_id"
@ -770,8 +737,8 @@ class JournalPersonStatementView(JournalTabsMixin, CanViewMixin, DetailView):
return sum(self.statement(movement_type).values())
def get_context_data(self, **kwargs):
"""Add journal to the context"""
kwargs = super(JournalPersonStatementView, self).get_context_data(**kwargs)
"""Add journal to the context."""
kwargs = super().get_context_data(**kwargs)
kwargs["credit_statement"] = self.statement("CREDIT")
kwargs["debit_statement"] = self.statement("DEBIT")
kwargs["total_credit"] = self.total("CREDIT")
@ -780,9 +747,7 @@ class JournalPersonStatementView(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
pk_url_kwarg = "j_id"
@ -800,8 +765,8 @@ class JournalAccountingStatementView(JournalTabsMixin, CanViewMixin, DetailView)
return statement
def get_context_data(self, **kwargs):
"""Add journal to the context"""
kwargs = super(JournalAccountingStatementView, self).get_context_data(**kwargs)
"""Add journal to the context."""
kwargs = super().get_context_data(**kwargs)
kwargs["statement"] = self.statement()
return kwargs
@ -815,9 +780,7 @@ class CompanyListView(CanViewMixin, ListView):
class CompanyCreateView(CanCreateMixin, CreateView):
"""
Create a company
"""
"""Create a company."""
model = Company
fields = ["name"]
@ -826,9 +789,7 @@ class CompanyCreateView(CanCreateMixin, CreateView):
class CompanyEditView(CanCreateMixin, UpdateView):
"""
Edit a company
"""
"""Edit a company."""
model = Company
pk_url_kwarg = "co_id"
@ -856,8 +817,8 @@ class LabelCreateView(
template_name = "core/create.jinja"
def get_initial(self):
ret = super(LabelCreateView, self).get_initial()
if "parent" in self.request.GET.keys():
ret = super().get_initial()
if "parent" in self.request.GET:
obj = ClubAccount.objects.filter(id=int(self.request.GET["parent"])).first()
if obj is not None:
ret["club_account"] = obj.id
@ -881,15 +842,17 @@ class LabelDeleteView(CanEditMixin, DeleteView):
class CloseCustomerAccountForm(forms.Form):
user = AutoCompleteSelectField(
"users", label=_("Refound this account"), help_text=None, required=True
user = forms.ModelChoiceField(
label=_("Refound this account"),
help_text=None,
required=True,
widget=AutoCompleteSelectUser,
queryset=User.objects.all(),
)
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"
form_class = CloseCustomerAccountForm
@ -901,19 +864,19 @@ class RefoundAccountView(FormView):
raise PermissionDenied
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):
return res
def post(self, request, *arg, **kwargs):
self.operator = 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):
self.customer = form.cleaned_data["user"]
self.create_selling()
return super(RefoundAccountView, self).form_valid(form)
return super().form_valid(form)
def get_success_url(self):
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,19 +0,0 @@
# -*- coding:utf-8 -*-
#
# Copyright 2023 © AE UTBM
# ae@utbm.fr / ae.info@utbm.fr
# All contributors are listed in the CONTRIBUTORS file.
#
# This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr.
#
# You can find the whole source code 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"
#
# PREVIOUSLY LICENSED UNDER THE MIT LICENSE,
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE.old
# OR WITHIN THE LOCAL FILE "LICENSE.old"
#

View File

@ -1,21 +0,0 @@
# -*- coding:utf-8 -*-
#
# Copyright 2023 © AE UTBM
# ae@utbm.fr / ae.info@utbm.fr
# All contributors are listed in the CONTRIBUTORS file.
#
# This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr.
#
# You can find the whole source code 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"
#
# PREVIOUSLY LICENSED UNDER THE MIT LICENSE,
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE.old
# OR WITHIN THE LOCAL FILE "LICENSE.old"
#
# Register your models here.

View File

@ -1,21 +0,0 @@
# -*- coding:utf-8 -*-
#
# Copyright 2023 © AE UTBM
# ae@utbm.fr / ae.info@utbm.fr
# All contributors are listed in the CONTRIBUTORS file.
#
# This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr.
#
# You can find the whole source code 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"
#
# PREVIOUSLY LICENSED UNDER THE MIT LICENSE,
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE.old
# OR WITHIN THE LOCAL FILE "LICENSE.old"
#
# Create your models here.

View File

@ -1,21 +0,0 @@
# -*- coding:utf-8 -*-
#
# Copyright 2023 © AE UTBM
# ae@utbm.fr / ae.info@utbm.fr
# All contributors are listed in the CONTRIBUTORS file.
#
# This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr.
#
# You can find the whole source code 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"
#
# PREVIOUSLY LICENSED UNDER THE MIT LICENSE,
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE.old
# OR WITHIN THE LOCAL FILE "LICENSE.old"
#
# Create your tests here.

View File

@ -1,54 +0,0 @@
# -*- coding:utf-8 -*-
#
# Copyright 2023 © AE UTBM
# ae@utbm.fr / ae.info@utbm.fr
# All contributors are listed in the CONTRIBUTORS file.
#
# This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr.
#
# You can find the whole source code 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"
#
# PREVIOUSLY LICENSED UNDER THE MIT LICENSE,
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE.old
# OR WITHIN THE LOCAL FILE "LICENSE.old"
#
from django.urls import include, path, re_path
from rest_framework import routers
from api.views import *
# 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,77 +0,0 @@
# -*- coding:utf-8 -*-
#
# Copyright 2023 © AE UTBM
# ae@utbm.fr / ae.info@utbm.fr
# All contributors are listed in the CONTRIBUTORS file.
#
# This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr.
#
# You can find the whole source code 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"
#
# PREVIOUSLY LICENSED UNDER THE MIT LICENSE,
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE.old
# OR WITHIN THE LOCAL FILE "LICENSE.old"
#
from django.core.exceptions import PermissionDenied
from django.db.models.query import QuerySet
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
from core.views import can_edit, can_view
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 .club import *
from .counter import *
from .group import *
from .launderette import *
from .sas import *
from .user import *
from .uv import *

View File

@ -1,38 +0,0 @@
# -*- coding:utf-8 -*-
#
# Copyright 2023 © AE UTBM
# ae@utbm.fr / ae.info@utbm.fr
# All contributors are listed in the CONTRIBUTORS file.
#
# This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr.
#
# You can find the whole source code 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"
#
# PREVIOUSLY LICENSED UNDER THE MIT LICENSE,
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE.old
# OR WITHIN THE LOCAL FILE "LICENSE.old"
#
from rest_framework.decorators import api_view, renderer_classes
from rest_framework.renderers import StaticHTMLRenderer
from rest_framework.response import Response
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,58 +0,0 @@
# -*- coding:utf-8 -*-
#
# Copyright 2023 © AE UTBM
# ae@utbm.fr / ae.info@utbm.fr
# All contributors are listed in the CONTRIBUTORS file.
#
# This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr.
#
# You can find the whole source code 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"
#
# PREVIOUSLY LICENSED UNDER THE MIT LICENSE,
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE.old
# OR WITHIN THE LOCAL FILE "LICENSE.old"
#
from django.conf import settings
from django.core.exceptions import PermissionDenied
from rest_framework import serializers
from rest_framework.decorators import api_view, renderer_classes
from rest_framework.renderers import StaticHTMLRenderer
from rest_framework.response import Response
from api.views import RightModelViewSet
from club.models import Club, Mailing
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,55 +0,0 @@
# -*- coding:utf-8 -*-
#
# Copyright 2023 © AE UTBM
# ae@utbm.fr / ae.info@utbm.fr
# All contributors are listed in the CONTRIBUTORS file.
#
# This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr.
#
# You can find the whole source code 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"
#
# PREVIOUSLY LICENSED UNDER THE MIT LICENSE,
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE.old
# OR WITHIN THE LOCAL FILE "LICENSE.old"
#
from rest_framework import serializers
from rest_framework.decorators import action
from rest_framework.response import Response
from api.views import RightModelViewSet
from counter.models import Counter
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,38 +0,0 @@
# -*- coding:utf-8 -*-
#
# Copyright 2023 © AE UTBM
# ae@utbm.fr / ae.info@utbm.fr
# All contributors are listed in the CONTRIBUTORS file.
#
# This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr.
#
# You can find the whole source code 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"
#
# PREVIOUSLY LICENSED UNDER THE MIT LICENSE,
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE.old
# OR WITHIN THE LOCAL FILE "LICENSE.old"
#
from rest_framework import serializers
from api.views import RightModelViewSet
from core.models import RealGroup
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,131 +0,0 @@
# -*- coding:utf-8 -*-
#
# Copyright 2023 © AE UTBM
# ae@utbm.fr / ae.info@utbm.fr
# All contributors are listed in the CONTRIBUTORS file.
#
# This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr.
#
# You can find the whole source code 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"
#
# PREVIOUSLY LICENSED UNDER THE MIT LICENSE,
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE.old
# OR WITHIN THE LOCAL FILE "LICENSE.old"
#
from rest_framework import serializers
from rest_framework.decorators import action
from rest_framework.response import Response
from api.views import RightModelViewSet
from launderette.models import Launderette, Machine, Token
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,43 +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.models import User
from core.views import can_edit
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,63 +0,0 @@
# -*- coding:utf-8 -*-
#
# Copyright 2023 © AE UTBM
# ae@utbm.fr / ae.info@utbm.fr
# All contributors are listed in the CONTRIBUTORS file.
#
# This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr.
#
# You can find the whole source code 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"
#
# PREVIOUSLY LICENSED UNDER THE MIT LICENSE,
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE.old
# OR WITHIN THE LOCAL FILE "LICENSE.old"
#
import datetime
from rest_framework import serializers
from rest_framework.decorators import action
from rest_framework.response import Response
from api.views import RightModelViewSet
from core.models import User
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,128 +0,0 @@
import json
import urllib.request
from django.conf import settings
from django.core.exceptions import PermissionDenied
from rest_framework import serializers
from rest_framework.decorators import api_view, renderer_classes
from rest_framework.renderers import JSONRenderer
from rest_framework.response import Response
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,19 +1,14 @@
# -*- coding:utf-8 -*-
#
# Copyright 2023 © AE UTBM
# ae@utbm.fr / ae.info@utbm.fr
# All contributors are listed in the CONTRIBUTORS file.
#
# This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr.
#
# You can find the whole source code 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)
# 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"
#
# PREVIOUSLY LICENSED UNDER THE MIT LICENSE,
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE.old
# OR WITHIN THE LOCAL FILE "LICENSE.old"
#

View File

@ -1,23 +1,17 @@
# -*- coding:utf-8 -*-
#
# Copyright 2023 © AE UTBM
# ae@utbm.fr / ae.info@utbm.fr
# All contributors are listed in the CONTRIBUTORS file.
#
# This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr.
#
# You can find the whole source code 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)
# 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"
#
# PREVIOUSLY LICENSED UNDER THE MIT LICENSE,
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE.old
# OR WITHIN THE LOCAL FILE "LICENSE.old"
#
from ajax_select import make_ajax_form
from django.contrib import admin
from club.models import Club, Membership
@ -37,4 +31,4 @@ class MembershipAdmin(admin.ModelAdmin):
"user__last_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,31 +1,35 @@
# -*- coding:utf-8 -*-
#
# Copyright 2023 © AE UTBM
# ae@utbm.fr / ae.info@utbm.fr
# All contributors are listed in the CONTRIBUTORS file.
# Copyright 2016,2017
# - Skia <skia@libskia.so>
# - Sli <antoine@bartuccio.fr>
#
# This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr.
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
# http://ae.utbm.fr.
#
# You can find the whole source code at https://github.com/ae-utbm/sith3
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License a published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
#
# 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"
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple
# Place - Suite 330, Boston, MA 02111-1307, USA.
#
# PREVIOUSLY LICENSED UNDER THE MIT LICENSE,
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE.old
# OR WITHIN THE LOCAL FILE "LICENSE.old"
#
from ajax_select.fields import AutoCompleteSelectMultipleField
from django import forms
from django.conf import settings
from django.utils.translation import gettext_lazy as _
from club.models import Club, Mailing, MailingSubscription, Membership
from core.models import User
from core.views.forms import SelectDate, TzAwareDateTimeField
from core.views.forms import SelectDate, SelectDateTime
from core.views.widgets.select import AutoCompleteSelectMultipleUser
from counter.models import Counter
@ -35,28 +39,27 @@ class ClubEditForm(forms.ModelForm):
fields = ["address", "logo", "short_description"]
def __init__(self, *args, **kwargs):
super(ClubEditForm, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
self.fields["short_description"].widget = forms.Textarea()
class MailingForm(forms.Form):
"""
Form handling mailing lists right
"""
"""Form handling mailing lists right."""
ACTION_NEW_MAILING = 1
ACTION_NEW_SUBSCRIPTION = 2
ACTION_REMOVE_SUBSCRIPTION = 3
subscription_users = AutoCompleteSelectMultipleField(
"users",
subscription_users = forms.ModelMultipleChoiceField(
label=_("Users to add"),
help_text=_("Search users to add (one or more)."),
required=False,
widget=AutoCompleteSelectMultipleUser,
queryset=User.objects.all(),
)
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(
choices=(
@ -101,24 +104,15 @@ class MailingForm(forms.Form):
)
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):
self.add_error(field, _("This field is required"))
def clean_subscription_users(self):
"""
Convert given users into real users and check their validity
"""
cleaned_data = super(MailingForm, self).clean()
"""Convert given users into real users and check their validity."""
cleaned_data = super().clean()
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:
raise forms.ValidationError(
_("One of the selected users doesn't have an email address"),
@ -128,9 +122,9 @@ class MailingForm(forms.Form):
return users
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
raise forms.ValidationError(_("An action is required"), code="invalid")
@ -151,15 +145,19 @@ class MailingForm(forms.Form):
class SellingsForm(forms.Form):
begin_date = TzAwareDateTimeField(label=_("Begin date"), required=False)
end_date = TzAwareDateTimeField(label=_("End date"), required=False)
begin_date = forms.DateTimeField(
label=_("Begin date"), widget=SelectDateTime, required=False
)
end_date = forms.DateTimeField(
label=_("End date"), widget=SelectDateTime, required=False
)
counters = forms.ModelMultipleChoiceField(
Counter.objects.order_by("name").all(), label=_("Counter"), required=False
)
def __init__(self, club, *args, **kwargs):
super(SellingsForm, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
self.fields["products"] = forms.ModelMultipleChoiceField(
club.products.order_by("name").filter(archived=False).all(),
label=_("Products"),
@ -173,18 +171,17 @@ class SellingsForm(forms.Form):
class ClubMemberForm(forms.Form):
"""
Form handling the members of a club
"""
"""Form handling the members of a club."""
error_css_class = "error"
required_css_class = "required"
users = AutoCompleteSelectMultipleField(
"users",
users = forms.ModelMultipleChoiceField(
label=_("Users to add"),
help_text=_("Search users to add (one or more)."),
required=False,
widget=AutoCompleteSelectMultipleUser,
queryset=User.objects.all(),
)
def __init__(self, *args, **kwargs):
@ -196,7 +193,7 @@ class ClubMemberForm(forms.Form):
self.club.members.filter(end_date=None).order_by("-role").all()
)
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
# We want the view to process the model creation since they are multiple users
@ -232,18 +229,13 @@ class ClubMemberForm(forms.Form):
self.fields.pop("start_date")
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
Also check that the user is valid and has a valid subscription
"""
cleaned_data = super(ClubMemberForm, self).clean()
cleaned_data = super().clean()
users = []
for user_id 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"
)
for user in cleaned_data["users"]:
if not user.is_subscribed:
raise forms.ValidationError(
_("User must be subscriber to take part to a club"), code="invalid"
@ -256,10 +248,8 @@ class ClubMemberForm(forms.Form):
return users
def clean(self):
"""
Check user rights for adding an user
"""
cleaned_data = super(ClubMemberForm, self).clean()
"""Check user rights for adding an user."""
cleaned_data = super().clean()
if "start_date" in cleaned_data and not cleaned_data["start_date"]:
# Drop start_date if allowed to edition but not specified

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import django.core.validators

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import django.utils.timezone

View File

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

View File

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

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import re
@ -110,6 +109,6 @@ class Migration(migrations.Migration):
),
migrations.AlterUniqueTogether(
name="mailingsubscription",
unique_together=set([("user", "email", "mailing")]),
unique_together={("user", "email", "mailing")},
),
]

View File

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

View File

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

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import django.db.models.deletion
@ -16,7 +15,7 @@ class Migration(migrations.Migration):
name="owner_group",
field=models.ForeignKey(
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",
to="core.Group",
),

View File

@ -1,23 +1,29 @@
# -*- coding:utf-8 -*-
#
# Copyright 2023 © AE UTBM
# ae@utbm.fr / ae.info@utbm.fr
# All contributors are listed in the CONTRIBUTORS file.
# Copyright 2016,2017
# - Skia <skia@libskia.so>
# - Sli <antoine@bartuccio.fr>
#
# This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr.
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
# http://ae.utbm.fr.
#
# You can find the whole source code at https://github.com/ae-utbm/sith3
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License a published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
#
# 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"
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# PREVIOUSLY LICENSED UNDER THE MIT LICENSE,
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE.old
# OR WITHIN THE LOCAL FILE "LICENSE.old"
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple
# Place - Suite 330, Boston, MA 02111-1307, USA.
#
from typing import Optional
#
from __future__ import annotations
from typing import Self
from django.conf import settings
from django.core import validators
@ -29,6 +35,7 @@ from django.db.models import Q
from django.urls import reverse
from django.utils import timezone
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 Group, MetaGroup, Notification, Page, RealGroup, SithFile, User
@ -36,10 +43,13 @@ from core.models import Group, MetaGroup, Notification, Page, RealGroup, SithFil
# 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):
"""
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)
name = models.CharField(_("name"), max_length=64)
@ -70,10 +80,6 @@ class Club(models.Model):
)
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",
@ -101,6 +107,34 @@ class Club(models.Model):
class Meta:
ordering = ["name", "unix_name"]
def __str__(self):
return self.name
@transaction.atomic()
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().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 get_absolute_url(self):
return reverse("club:club_view", kwargs={"club_id": self.id})
@cached_property
def president(self):
return self.members.filter(
@ -108,7 +142,7 @@ class Club(models.Model):
).first()
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 = []
cur = self
while cur.parent is not None:
@ -179,48 +213,18 @@ class Club(models.Model):
self.page.parent = self.parent.page
self.page.save(force_lock=True)
@transaction.atomic()
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
for membership in self.members.ongoing().select_related("user"):
cache.delete(f"membership_{self.id}_{membership.user.id}")
cache.delete(f"sith_club_{self.unix_name}")
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse("club:club_view", kwargs={"club_id": self.id})
super().delete(*args, **kwargs)
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
"""
"""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
@ -229,24 +233,21 @@ class Club(models.Model):
return "https://%s%s" % (settings.SITH_URL, self.logo.url)
def can_be_edited_by(self, user):
"""
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)
def can_be_viewed_by(self, user):
"""
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."""
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"]:
"""
Return the current membership the given user.
The result is cached.
def get_membership_for(self, user: User) -> Membership | None:
"""Return the current membership the given user.
Note:
The result is cached.
"""
if user.is_anonymous:
return None
@ -267,16 +268,12 @@ class Club(models.Model):
class MembershipQuerySet(models.QuerySet):
def ongoing(self) -> "MembershipQuerySet":
"""
Filter all memberships which are not finished yet
"""
# noinspection PyTypeChecker
return self.filter(Q(end_date=None) | Q(end_date__gte=timezone.now()))
def ongoing(self) -> Self:
"""Filter all memberships which are not finished yet."""
return self.filter(Q(end_date=None) | Q(end_date__gt=localdate()))
def board(self) -> "MembershipQuerySet":
"""
Filter all memberships where the user is/was in the board.
def board(self) -> Self:
"""Filter all memberships where the user is/was in the board.
Be aware that users who were in the board in the past
are included, even if there are no more members.
@ -284,13 +281,12 @@ class MembershipQuerySet(models.QuerySet):
If you want to get the users who are currently in the board,
mind combining this with the :meth:`ongoing` queryset method
"""
# noinspection PyTypeChecker
return self.filter(role__gt=settings.SITH_MAXIMUM_FREE_ROLE)
def update(self, **kwargs):
"""
Work just like the default Django's update() method,
but add a cache refresh for the elements of the queryset.
"""Refresh the cache for the elements of the queryset.
Besides that, does the same job as a regular update method.
Be aware that this adds a db query to retrieve the updated objects
"""
@ -310,8 +306,7 @@ class MembershipQuerySet(models.QuerySet):
)
def delete(self):
"""
Work just like the default Django's delete() method,
"""Work just like the default Django's delete() method,
but add a cache invalidation for the elements of the queryset
before the deletion.
@ -327,8 +322,7 @@ class MembershipQuerySet(models.QuerySet):
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:
- a user can be a member of many clubs at a time
@ -369,36 +363,11 @@ class Membership(models.Model):
def __str__(self):
return (
self.club.name
+ " - "
+ self.user.username
+ " - "
+ str(settings.SITH_CLUB_ROLES[self.role])
+ str(" - " + str(_("past member")) if self.end_date is not None else "")
f"{self.club.name} - {self.user.username} "
f"- {settings.SITH_CLUB_ROLES[self.role]} "
f"- {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):
super().save(*args, **kwargs)
if self.end_date is None:
@ -406,15 +375,32 @@ class Membership(models.Model):
else:
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):
"""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)
return membership is not None and membership.role >= self.role
def delete(self, *args, **kwargs):
super().delete(*args, **kwargs)
cache.delete(f"membership_{self.club_id}_{self.user_id}")
class Mailing(models.Model):
"""
This class correspond to a mailing list
Remember that mailing lists should be validated by UTBM
"""A Mailing list for a club.
Warning:
Remember that mailing lists should be validated by UTBM.
"""
club = models.ForeignKey(
@ -447,6 +433,26 @@ class Mailing(models.Model):
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:
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(*args, **kwargs)
super().save(*args, **kwargs)
def clean(self):
if Mailing.objects.filter(email=self.email).exists():
raise ValidationError(_("This mailing list already exists."))
@ -454,7 +460,7 @@ class Mailing(models.Model):
self.is_moderated = True
else:
self.moderator = None
super(Mailing, self).clean()
super().clean()
@property
def email_full(self):
@ -476,39 +482,15 @@ class Mailing(models.Model):
def delete(self, *args, **kwargs):
self.subscriptions.all().delete()
super(Mailing, self).delete()
super().delete()
def fetch_format(self):
resp = self.email + ": "
for sub in self.subscriptions.all():
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)
destination = "".join(s.fetch_format() for s in self.subscriptions.all())
return f"{self.email}: {destination}"
class MailingSubscription(models.Model):
"""
This class makes the link between user and mailing list
"""
"""Link between user and mailing list."""
mailing = models.ForeignKey(
Mailing,
@ -531,6 +513,9 @@ class MailingSubscription(models.Model):
class Meta:
unique_together = (("user", "email", "mailing"),)
def __str__(self):
return "(%s) - %s : %s" % (self.mailing, self.get_username, self.email)
def clean(self):
if not self.user and not self.email:
raise ValidationError(_("At least user or email is required"))
@ -545,7 +530,7 @@ class MailingSubscription(models.Model):
)
except ObjectDoesNotExist:
pass
super(MailingSubscription, self).clean()
super().clean()
def is_owned_by(self, user):
if user.is_anonymous:
@ -573,6 +558,3 @@ class MailingSubscription(models.Model):
def fetch_format(self):
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 %}
{% block content %}
<div id="club_detail">
{% if club.logo %}
<div class="club_logo"><img src="{{ club.logo.url }}" alt="{{ club.unix_name }}"></div>
{% endif %}
{% if page_revision %}
{{ page_revision|markdown }}
{% else %}
<h3>{% trans %}Club{% endtrans %}</h3>
{% endif %}
</div>
<div id="club_detail">
{% if club.logo %}
<div class="club_logo"><img src="{{ club.logo.url }}" alt="{{ club.unix_name }}"></div>
{% endif %}
{% if page_revision %}
{{ page_revision|markdown }}
{% else %}
<h3>{% trans %}Club{% endtrans %}</h3>
{% endif %}
</div>
{% endblock %}

View File

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

View File

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

View File

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

View File

@ -1,66 +1,94 @@
{% 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 %}
<h3>{% trans %}Sales{% endtrans %}</h3>
<form id="form" action="?page=1" method="post">
<h3>{% trans %}Sales{% endtrans %}</h3>
<form id="form" action="?page=1" method="post">
{% csrf_token %}
{{ form }}
<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>
</form>
<p>
{% trans %}Quantity: {% endtrans %}{{ total_quantity }} {% trans %}units{% endtrans %}<br/>
{% trans %}Total: {% endtrans %}{{ total }} €<br/>
{% trans %}Benefit: {% endtrans %}{{ benefit }}
</p>
<table>
</form>
<p>
{% trans %}Quantity: {% endtrans %}{{ total_quantity }} {% trans %}units{% endtrans %}<br/>
{% trans %}Total: {% endtrans %}{{ total }} €<br/>
{% trans %}Benefit: {% endtrans %}{{ benefit }}
</p>
<table>
<thead>
<tr>
<td>{% trans %}Date{% endtrans %}</td>
<td>{% trans %}Counter{% endtrans %}</td>
<td>{% trans %}Barman{% endtrans %}</td>
<td>{% trans %}Customer{% endtrans %}</td>
<td>{% trans %}Label{% endtrans %}</td>
<td>{% trans %}Quantity{% endtrans %}</td>
<td>{% trans %}Total{% endtrans %}</td>
<td>{% trans %}Payment method{% endtrans %}</td>
</tr>
<tr>
<td>{% trans %}Date{% endtrans %}</td>
<td>{% trans %}Counter{% endtrans %}</td>
<td>{% trans %}Barman{% endtrans %}</td>
<td>{% trans %}Customer{% endtrans %}</td>
<td>{% trans %}Label{% endtrans %}</td>
<td>{% trans %}Quantity{% endtrans %}</td>
<td>{% trans %}Total{% endtrans %}</td>
<td>{% trans %}Payment method{% endtrans %}</td>
</tr>
</thead>
<tbody>
{% for s in paginated_result %}
{% for s in paginated_result %}
<tr>
<td>{{ s.date|localtime|date(DATETIME_FORMAT) }} {{ s.date|localtime|time(DATETIME_FORMAT) }}</td>
<td>{{ s.counter }}</td>
{% if s.seller %}
<td>{{ s.date|localtime|date(DATETIME_FORMAT) }} {{ s.date|localtime|time(DATETIME_FORMAT) }}</td>
<td>{{ s.counter }}</td>
{% if s.seller %}
<td><a href="{{ s.seller.get_absolute_url() }}">{{ s.seller.get_display_name() }}</a></td>
{% else %}
{% else %}
<td></td>
{% endif %}
{% if s.customer %}
{% endif %}
{% if s.customer %}
<td><a href="{{ s.customer.user.get_absolute_url() }}">{{ s.customer.user.get_display_name() }}</a></td>
{% else %}
{% else %}
<td></td>
{% endif %}
<td>{{ s.label }}</td>
<td>{{ s.quantity }}</td>
<td>{{ s.quantity * s.unit_price }} €</td>
<td>{{ s.get_payment_method_display() }}</td>
{% if s.is_owned_by(user) %}
<td><a href="{{ url('counter:selling_delete', selling_id=s.id) }}">{% trans %}Delete{% endtrans %}</a></td>
{% endif %}
{% endif %}
<td>{{ s.label }}</td>
<td>{{ s.quantity }}</td>
<td>{{ s.quantity * s.unit_price }} €</td>
<td>{{ s.get_payment_method_display() }}</td>
{% if s.is_owned_by(user) %}
<td><a href="{{ url('counter:selling_delete', selling_id=s.id) }}">{% trans %}Delete{% endtrans %}</a></td>
{% endif %}
</tr>
{% endfor %}
{% endfor %}
</tbody>
</table>
<script type="text/javascript">
</table>
<script type="text/javascript">
function formPagination(link){
$("form").attr("action", link.href);
link.href = "javascript:void(0)"; // block link action
$("form").submit();
$("form").attr("action", link.href);
link.href = "javascript:void(0)"; // block link action
$("form").submit();
}
</script>
{{ paginate(paginated_result, paginator, "formPagination(this)") }}
</script>
{{ paginate(paginated_result, paginator, "formPagination(this)") }}
{% endblock %}

View File

@ -1,46 +1,46 @@
{% extends "core/base.jinja" %}
{% block content %}
<h3>{% trans %}Club tools{% endtrans %}</h3>
<div>
<h3>{% trans %}Club tools{% endtrans %}</h3>
<div>
<h4>{% trans %}Communication:{% endtrans %}</h4>
<ul>
<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>
{% if object.trombi %}
<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>
{% if object.trombi %}
<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('club:poster_list', club_id=object.id) }}">{% trans %}Posters{% endtrans %}</a></li>
{% endif %}
{% endif %}
</ul>
<h4>{% trans %}Counters:{% endtrans %}</h4>
<ul>
{% if object.unix_name == settings.SITH_LAUNDERETTE_MANAGER['unix_name'] %}
{% for l in Launderette.objects.all() %}
<li><a href="{{ url('launderette:main_click', launderette_id=l.id) }}">{{ l }}</a></li>
{% endfor %}
{% elif object.counters.filter(type="OFFICE")|count > 0 %}
{% if object.unix_name == settings.SITH_LAUNDERETTE_MANAGER['unix_name'] %}
{% for l in Launderette.objects.all() %}
<li><a href="{{ url('launderette:main_click', launderette_id=l.id) }}">{{ l }}</a></li>
{% endfor %}
{% elif object.counters.filter(type="OFFICE")|count > 0 %}
{% 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:admin', counter_id=c.id) }}">Edit</a>
</li>
</li>
{% endfor %}
{% endif %}
{% endif %}
</ul>
{% if object.club_account.exists() %}
<h4>{% trans %}Accounting: {% endtrans %}</h4>
<ul>
<h4>{% trans %}Accounting: {% endtrans %}</h4>
<ul>
{% 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 %}
</ul>
</ul>
{% endif %}
{% 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 %}
</div>
</div>
{% endblock %}

View File

@ -2,107 +2,107 @@
{% from 'core/macros.jinja' import select_all_checkbox %}
{% block title %}
{% trans %}Mailing lists{% endtrans %}
{% trans %}Mailing lists{% endtrans %}
{% endblock %}
{% 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 %}
<p>{% trans %}Mailing lists waiting for moderation{% endtrans %}</p>
<ul>
{% 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>
{% endfor %}
</ul>
{% endif %}
{% if mailings_not_moderated %}
<p>{% trans %}Mailing lists waiting for moderation{% endtrans %}</p>
<ul>
{% 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>
{% endfor %}
</ul>
{% endif %}
{% if mailings_moderated %}
{% if 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) -%}
<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 -%}
</h2>
<form method="GET" action="{{ url('club:mailing_generate', mailing_id=mailing.id) }}" style="display:inline-block;">
</h2>
<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 %}">
</form>
{% set form_mailing_removal = form["removal_" + mailing.id|string] %}
{% if form_mailing_removal.field.choices %}
{% 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">
<p style="margin-bottom: 1em;">{{ select_all_checkbox(form_mailing_removal.auto_id) }}</p>
{% csrf_token %}
<input hidden type="number" name="{{ form.action.name }}" value="{{ form_actions.REMOVE_SUBSCRIPTION }}" />
<table>
</form>
{% set form_mailing_removal = form["removal_" + mailing.id|string] %}
{% if form_mailing_removal.field.choices %}
{% 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">
<p style="margin-bottom: 1em;">{{ select_all_checkbox(form_mailing_removal.auto_id) }}</p>
{% csrf_token %}
<input hidden type="number" name="{{ form.action.name }}" value="{{ form_actions.REMOVE_SUBSCRIPTION }}" />
<table>
<thead>
<tr>
<td>{% trans %}User{% endtrans %}</td>
<td>{% trans %}Email{% endtrans %}</td>
<td>{% trans %}Delete{% endtrans %}</td>
</tr>
<tr>
<td>{% trans %}User{% endtrans %}</td>
<td>{% trans %}Email{% endtrans %}</td>
<td>{% trans %}Delete{% endtrans %}</td>
</tr>
</thead>
<tbody>
{% for widget in form_mailing_removal.subwidgets %}
{% for widget in form_mailing_removal.subwidgets %}
{% set user = ms[widget.data.value.value][0] %}
<tr>
<td>{{ user.get_username }}</td>
<td>{{ user.get_email }}</td>
<td>{{ widget.tag() }}</td>
<td>{{ user.get_username }}</td>
<td>{{ user.get_email }}</td>
<td>{{ widget.tag() }}</td>
</tr>
{% endfor %}
{% endfor %}
</tbody>
</table>
{{ form_mailing_removal.errors }}
<p><input type="submit" value="{% trans %}Remove from mailing list{% endtrans %}" /></p>
</form>
</table>
{{ form_mailing_removal.errors }}
<p><input type="submit" value="{% trans %}Remove from mailing list{% endtrans %}" /></p>
</form>
{% else %}
{% else %}
<p><b>{% trans %}There is no subscriber for this mailing list{% endtrans %}</b></p>
{% endif %}
{% endif %}
{% endfor %}
{% else %}
{% else %}
<p>{% trans %}No mailing list existing for this club{% endtrans %}</p>
{% endif %}
{% endif %}
<p>{{ form.non_field_errors() }}</p>
{% if mailings_moderated %}
<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>
<p>{{ form.non_field_errors() }}</p>
{% if mailings_moderated %}
<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.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>
{% 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">
{% 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 %}

View File

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

View File

@ -2,7 +2,7 @@
{% from 'core/macros_pages.jinja' import page_edit_form %}
{% 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 %}

View File

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

View File

@ -1,21 +1,16 @@
# -*- coding:utf-8 -*-
#
# Copyright 2023 © AE UTBM
# ae@utbm.fr / ae.info@utbm.fr
# All contributors are listed in the CONTRIBUTORS file.
#
# This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr.
#
# You can find the whole source code 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)
# 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"
#
# PREVIOUSLY LICENSED UNDER THE MIT LICENSE,
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE.old
# OR WITHIN THE LOCAL FILE "LICENSE.old"
#
from datetime import timedelta
@ -24,18 +19,19 @@ from django.core.cache import cache
from django.test import TestCase
from django.urls import reverse
from django.utils import timezone
from django.utils.timezone import localtime, now
from django.utils.timezone import localdate, localtime, now
from django.utils.translation import gettext as _
from club.forms import MailingForm
from club.models import Club, Mailing, Membership
from core.baker_recipes import subscriber_user
from core.models import AnonymousUser, User
from sith.settings import SITH_BAR_MANAGER, SITH_MAIN_CLUB_ID
class ClubTest(TestCase):
"""
Set up data for test cases related to clubs and membership
class TestClub(TestCase):
"""Set up data for test cases related to clubs and membership.
The generated dataset is the one created by the populate command,
plus the following modifications :
@ -50,6 +46,8 @@ class ClubTest(TestCase):
def setUpTestData(cls):
# subscribed users - initial members
cls.skia = User.objects.get(username="skia")
# by default, Skia is in the AE, which creates side effect
cls.skia.memberships.all().delete()
cls.richard = User.objects.get(username="rbatsbak")
cls.comptable = User.objects.get(username="comptable")
cls.sli = User.objects.get(username="sli")
@ -66,45 +64,38 @@ class ClubTest(TestCase):
cls.public = User.objects.get(username="public")
cls.ae = Club.objects.filter(pk=SITH_MAIN_CLUB_ID)[0]
def setUp(self):
# by default, Skia is in the AE, which creates side effect
self.skia.memberships.all().delete()
# create a fake club
self.club = Club.objects.create(
cls.club = Club.objects.create(
name="Fake Club",
unix_name="fake-club",
address="5 rue de la République, 90000 Belfort",
)
self.members_url = reverse(
"club:club_members", kwargs={"club_id": self.club.id}
)
cls.members_url = reverse("club:club_members", kwargs={"club_id": cls.club.id})
a_month_ago = now() - timedelta(days=30)
yesterday = now() - timedelta(days=1)
Membership.objects.create(
club=self.club, user=self.skia, start_date=a_month_ago, role=3
club=cls.club, user=cls.skia, start_date=a_month_ago, role=3
)
Membership.objects.create(club=self.club, user=self.richard, role=1)
Membership.objects.create(club=cls.club, user=cls.richard, role=1)
Membership.objects.create(
club=self.club, user=self.comptable, start_date=a_month_ago, role=10
club=cls.club, user=cls.comptable, start_date=a_month_ago, role=10
)
# sli was a member but isn't anymore
Membership.objects.create(
club=self.club,
user=self.sli,
club=cls.club,
user=cls.sli,
start_date=a_month_ago,
end_date=yesterday,
role=2,
)
def setUp(self):
cache.clear()
class MembershipQuerySetTest(ClubTest):
class TestMembershipQuerySet(TestClub):
def test_ongoing(self):
"""
Test that the ongoing queryset method returns the memberships that
"""Test that the ongoing queryset method returns the memberships that
are not ended.
"""
current_members = list(self.club.members.ongoing().order_by("id"))
@ -116,10 +107,21 @@ class MembershipQuerySetTest(ClubTest):
expected.sort(key=lambda i: i.id)
assert current_members == expected
def test_ongoing_with_membership_ending_today(self):
"""Test that a membership ending the present day is considered as ended."""
today = localdate()
self.richard.memberships.filter(club=self.club).update(end_date=today)
current_members = list(self.club.members.ongoing().order_by("id"))
expected = [
self.skia.memberships.get(club=self.club),
self.comptable.memberships.get(club=self.club),
]
expected.sort(key=lambda i: i.id)
assert current_members == expected
def test_board(self):
"""
Test that the board queryset method returns the memberships
of user in the club board
"""Test that the board queryset method returns the memberships
of user in the club board.
"""
board_members = list(self.club.members.board().order_by("id"))
expected = [
@ -132,9 +134,8 @@ class MembershipQuerySetTest(ClubTest):
assert board_members == expected
def test_ongoing_board(self):
"""
Test that combining ongoing and board returns users
who are currently board members of the club
"""Test that combining ongoing and board returns users
who are currently board members of the club.
"""
members = list(self.club.members.ongoing().board().order_by("id"))
expected = [
@ -145,9 +146,7 @@ class MembershipQuerySetTest(ClubTest):
assert members == expected
def test_update_invalidate_cache(self):
"""
Test that the `update` queryset method properly invalidate cache
"""
"""Test that the `update` queryset method properly invalidate cache."""
mem_skia = self.skia.memberships.get(club=self.club)
cache.set(f"membership_{mem_skia.club_id}_{mem_skia.user_id}", mem_skia)
self.skia.memberships.update(end_date=localtime(now()).date())
@ -166,10 +165,7 @@ class MembershipQuerySetTest(ClubTest):
assert new_mem.role == 5
def test_delete_invalidate_cache(self):
"""
Test that the `delete` queryset properly invalidate cache
"""
"""Test that the `delete` queryset properly invalidate cache."""
mem_skia = self.skia.memberships.get(club=self.club)
mem_comptable = self.comptable.memberships.get(club=self.club)
cache.set(f"membership_{mem_skia.club_id}_{mem_skia.user_id}", mem_skia)
@ -180,21 +176,16 @@ class MembershipQuerySetTest(ClubTest):
# should delete the subscriptions of skia and comptable
self.club.members.ongoing().board().delete()
assert (
cache.get(f"membership_{mem_skia.club_id}_{mem_skia.user_id}")
== "not_member"
)
assert (
cache.get(f"membership_{mem_comptable.club_id}_{mem_comptable.user_id}")
== "not_member",
)
for membership in (mem_skia, mem_comptable):
cached_mem = cache.get(
f"membership_{membership.club_id}_{membership.user_id}"
)
assert cached_mem == "not_member"
class ClubModelTest(ClubTest):
class TestClubModel(TestClub):
def assert_membership_started_today(self, user: User, role: int):
"""
Assert that the given membership is active and started today
"""
"""Assert that the given membership is active and started today."""
membership = user.memberships.ongoing().filter(club=self.club).first()
assert membership is not None
assert localtime(now()).date() == membership.start_date
@ -207,17 +198,14 @@ class ClubModelTest(ClubTest):
assert user.is_in_group(name=board_group)
def assert_membership_ended_today(self, user: User):
"""
Assert that the given user have a membership which ended today
"""
"""Assert that the given user have a membership which ended today."""
today = localtime(now()).date()
assert user.memberships.filter(club=self.club, end_date=today).exists()
assert self.club.get_membership_for(user) is None
def test_access_unauthorized(self):
"""
Test that users who never subscribed and anonymous users
cannot see the page
"""Test that users who never subscribed and anonymous users
cannot see the page.
"""
response = self.client.post(self.members_url)
assert response.status_code == 403
@ -227,8 +215,7 @@ class ClubModelTest(ClubTest):
assert response.status_code == 403
def test_display(self):
"""
Test that a GET request return a page where the requested
"""Test that a GET request return a page where the requested
information are displayed.
"""
self.client.force_login(self.skia)
@ -263,27 +250,23 @@ class ClubModelTest(ClubTest):
self.assertInHTML(expected_html, response.content.decode())
def test_root_add_one_club_member(self):
"""
Test that root users can add members to clubs, one at a time
"""
"""Test that root users can add members to clubs, one at a time."""
self.client.force_login(self.root)
response = self.client.post(
self.members_url,
{"users": self.subscriber.id, "role": 3},
{"users": [self.subscriber.id], "role": 3},
)
self.assertRedirects(response, self.members_url)
self.subscriber.refresh_from_db()
self.assert_membership_started_today(self.subscriber, role=3)
def test_root_add_multiple_club_member(self):
"""
Test that root users can add multiple members at once to clubs
"""
"""Test that root users can add multiple members at once to clubs."""
self.client.force_login(self.root)
response = self.client.post(
self.members_url,
{
"users": f"|{self.subscriber.id}|{self.krophil.id}|",
"users": (self.subscriber.id, self.krophil.id),
"role": 3,
},
)
@ -293,8 +276,7 @@ class ClubModelTest(ClubTest):
self.assert_membership_started_today(self.krophil, role=3)
def test_add_unauthorized_members(self):
"""
Test that users who are not currently subscribed
"""Test that users who are not currently subscribed
cannot be members of clubs.
"""
self.client.force_login(self.root)
@ -314,9 +296,8 @@ class ClubModelTest(ClubTest):
assert '<ul class="errorlist"><li>' in response.content.decode()
def test_add_members_already_members(self):
"""
Test that users who are already members of a club
cannot be added again to this club
"""Test that users who are already members of a club
cannot be added again to this club.
"""
self.client.force_login(self.root)
current_membership = self.skia.memberships.ongoing().get(club=self.club)
@ -332,8 +313,7 @@ class ClubModelTest(ClubTest):
assert self.club.get_membership_for(self.skia) == new_membership
def test_add_not_existing_users(self):
"""
Test that not existing users cannot be added in clubs.
"""Test that not existing users cannot be added in clubs.
If one user in the request is invalid, no membership creation at all
can take place.
"""
@ -350,7 +330,7 @@ class ClubModelTest(ClubTest):
response = self.client.post(
self.members_url,
{
"users": f"|{self.subscriber.id}|{9999}|",
"users": (self.subscriber.id, 9999),
"start_date": "12/06/2016",
"role": 3,
},
@ -361,9 +341,7 @@ class ClubModelTest(ClubTest):
assert self.club.members.count() == nb_memberships
def test_president_add_members(self):
"""
Test that the president of the club can add members
"""
"""Test that the president of the club can add members."""
president = self.club.members.get(role=10).user
nb_club_membership = self.club.members.count()
nb_subscriber_memberships = self.subscriber.memberships.count()
@ -380,8 +358,7 @@ class ClubModelTest(ClubTest):
self.assert_membership_started_today(self.subscriber, role=9)
def test_add_member_greater_role(self):
"""
Test that a member of the club member cannot create
"""Test that a member of the club member cannot create
a membership with a greater role than its own.
"""
self.client.force_login(self.skia)
@ -400,9 +377,7 @@ class ClubModelTest(ClubTest):
assert not self.subscriber.memberships.filter(club=self.club).exists()
def test_add_member_without_role(self):
"""
Test that trying to add members without specifying their role fails
"""
"""Test that trying to add members without specifying their role fails."""
self.client.force_login(self.root)
response = self.client.post(
self.members_url,
@ -414,9 +389,7 @@ class ClubModelTest(ClubTest):
)
def test_end_membership_self(self):
"""
Test that a member can end its own membership
"""
"""Test that a member can end its own membership."""
self.client.force_login(self.skia)
self.client.post(
self.members_url,
@ -426,9 +399,8 @@ class ClubModelTest(ClubTest):
self.assert_membership_ended_today(self.skia)
def test_end_membership_lower_role(self):
"""
Test that board members of the club can end memberships
of users with lower roles
"""Test that board members of the club can end memberships
of users with lower roles.
"""
# remainder : skia has role 3, comptable has role 10, richard has role 1
self.client.force_login(self.skia)
@ -441,9 +413,8 @@ class ClubModelTest(ClubTest):
self.assert_membership_ended_today(self.richard)
def test_end_membership_higher_role(self):
"""
Test that board members of the club cannot end memberships
of users with higher roles
"""Test that board members of the club cannot end memberships
of users with higher roles.
"""
membership = self.comptable.memberships.filter(club=self.club).first()
self.client.force_login(self.skia)
@ -460,16 +431,15 @@ class ClubModelTest(ClubTest):
assert membership.end_date is None
def test_end_membership_as_main_club_board(self):
"""
Test that board members of the main club can end the membership
of anyone
"""Test that board members of the main club can end the membership
of anyone.
"""
# make subscriber a board member
self.subscriber.memberships.all().delete()
Membership.objects.create(club=self.ae, user=self.subscriber, role=3)
subscriber = subscriber_user.make()
Membership.objects.create(club=self.ae, user=subscriber, role=3)
nb_memberships = self.club.members.count()
self.client.force_login(self.subscriber)
nb_memberships = self.club.members.ongoing().count()
self.client.force_login(subscriber)
response = self.client.post(
self.members_url,
{"users_old": self.comptable.id},
@ -479,10 +449,8 @@ class ClubModelTest(ClubTest):
assert self.club.members.ongoing().count() == nb_memberships - 1
def test_end_membership_as_root(self):
"""
Test that root users can end the membership of anyone
"""
nb_memberships = self.club.members.count()
"""Test that root users can end the membership of anyone."""
nb_memberships = self.club.members.ongoing().count()
self.client.force_login(self.root)
response = self.client.post(
self.members_url,
@ -491,12 +459,9 @@ class ClubModelTest(ClubTest):
self.assertRedirects(response, self.members_url)
self.assert_membership_ended_today(self.comptable)
assert self.club.members.ongoing().count() == nb_memberships - 1
assert self.club.members.count() == nb_memberships
def test_end_membership_as_foreigner(self):
"""
Test that users who are not in this club cannot end its memberships
"""
"""Test that users who are not in this club cannot end its memberships."""
nb_memberships = self.club.members.count()
membership = self.richard.memberships.filter(club=self.club).first()
self.client.force_login(self.subscriber)
@ -510,9 +475,8 @@ class ClubModelTest(ClubTest):
assert membership == new_mem
def test_delete_remove_from_meta_group(self):
"""
Test that when a club is deleted, all its members are removed from the
associated metagroup
"""Test that when a club is deleted, all its members are removed from the
associated metagroup.
"""
memberships = self.club.members.select_related("user")
users = [membership.user for membership in memberships]
@ -523,9 +487,7 @@ class ClubModelTest(ClubTest):
assert not user.is_in_group(name=meta_group)
def test_add_to_meta_group(self):
"""
Test that when a membership begins, the user is added to the meta group
"""
"""Test that when a membership begins, the user is added to the meta group."""
group_members = self.club.unix_name + settings.SITH_MEMBER_SUFFIX
board_members = self.club.unix_name + settings.SITH_BOARD_SUFFIX
assert not self.subscriber.is_in_group(name=group_members)
@ -535,9 +497,7 @@ class ClubModelTest(ClubTest):
assert self.subscriber.is_in_group(name=board_members)
def test_remove_from_meta_group(self):
"""
Test that when a membership ends, the user is removed from meta group
"""
"""Test that when a membership ends, the user is removed from meta group."""
group_members = self.club.unix_name + settings.SITH_MEMBER_SUFFIX
board_members = self.club.unix_name + settings.SITH_BOARD_SUFFIX
assert self.comptable.is_in_group(name=group_members)
@ -547,9 +507,7 @@ class ClubModelTest(ClubTest):
assert not self.comptable.is_in_group(name=board_members)
def test_club_owner(self):
"""
Test that a club is owned only by board members of the main club
"""
"""Test that a club is owned only by board members of the main club."""
anonymous = AnonymousUser()
assert not self.club.is_owned_by(anonymous)
assert not self.club.is_owned_by(self.subscriber)
@ -560,8 +518,8 @@ class ClubModelTest(ClubTest):
assert self.club.is_owned_by(self.sli)
class MailingFormTest(TestCase):
"""Perform validation tests for MailingForm"""
class TestMailingForm(TestCase):
"""Perform validation tests for MailingForm."""
@classmethod
def setUpTestData(cls):
@ -671,7 +629,7 @@ class MailingFormTest(TestCase):
self.mail_url,
{
"action": MailingForm.ACTION_NEW_SUBSCRIPTION,
"subscription_users": "|%s|%s|" % (self.comunity.id, self.rbatsbak.id),
"subscription_users": (self.comunity.id, self.rbatsbak.id),
"subscription_mailing": Mailing.objects.get(email="mde").id,
},
)
@ -757,16 +715,17 @@ class MailingFormTest(TestCase):
self.mail_url,
{
"action": MailingForm.ACTION_NEW_SUBSCRIPTION,
"subscription_users": "|789|",
"subscription_users": [789],
"subscription_mailing": Mailing.objects.get(email="mde").id,
},
)
assert response.status_code == 200
self.assertInHTML(
_("One of the selected users doesn't exist"), response.content.decode()
_("You must specify at least an user or an email address"),
response.content.decode(),
)
# An user has no email adress
# An user has no email address
self.krophil.email = ""
self.krophil.save()
@ -824,8 +783,11 @@ class MailingFormTest(TestCase):
self.mail_url,
{
"action": MailingForm.ACTION_NEW_SUBSCRIPTION,
"subscription_users": "|%s|%s|%s|"
% (self.comunity.id, self.rbatsbak.id, self.krophil.id),
"subscription_users": (
self.comunity.id,
self.rbatsbak.id,
self.krophil.id,
),
"subscription_mailing": mde.id,
},
)
@ -876,10 +838,8 @@ class MailingFormTest(TestCase):
assert "krophil@git.an" not in content
class ClubSellingViewTest(TestCase):
"""
Perform basics tests to ensure that the page is available
"""
class TestClubSellingView(TestCase):
"""Perform basics tests to ensure that the page is available."""
@classmethod
def setUpTestData(cls):
@ -887,9 +847,7 @@ class ClubSellingViewTest(TestCase):
cls.skia = User.objects.get(username="skia")
def test_page_not_internal_error(self):
"""
Test that the page does not return and internal error
"""
"""Test that the page does not return and internal error."""
self.client.force_login(self.skia)
response = self.client.get(
reverse("club:club_sellings", kwargs={"club_id": self.ae.id})

View File

@ -1,26 +1,55 @@
# -*- coding:utf-8 -*-
#
# Copyright 2023 © AE UTBM
# ae@utbm.fr / ae.info@utbm.fr
# All contributors are listed in the CONTRIBUTORS file.
# Copyright 2016,2017
# - Skia <skia@libskia.so>
# - Sli <antoine@bartuccio.fr>
#
# This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr.
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
# http://ae.utbm.fr.
#
# You can find the whole source code at https://github.com/ae-utbm/sith3
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License a published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
#
# 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"
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple
# Place - Suite 330, Boston, MA 02111-1307, USA.
#
# PREVIOUSLY LICENSED UNDER THE MIT LICENSE,
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE.old
# OR WITHIN THE LOCAL FILE "LICENSE.old"
#
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 = [
path("", ClubListView.as_view(), name="club_list"),
@ -28,32 +57,20 @@ urlpatterns = [
path("stats/", ClubStatView.as_view(), name="club_stats"),
path("<int:club_id>/", ClubView.as_view(), name="club_view"),
path(
"<int:club_id>/rev/<int:rev_id>/",
ClubRevView.as_view(),
name="club_view_rev",
"<int:club_id>/rev/<int:rev_id>/", ClubRevView.as_view(), name="club_view_rev"
),
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/page/",
ClubPageEditView.as_view(),
name="club_edit_page",
),
path("<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>/elderlies/",
ClubOldMembersView.as_view(),
name="club_old_members",
),
path("<int:club_id>/sellings/", ClubSellingView.as_view(), name="club_sellings"),
path(
"<int:club_id>/sellings/",
ClubSellingView.as_view(),
name="club_sellings",
),
path(
"<int:club_id>/sellings/csv/",
ClubSellingCSVView.as_view(),
name="sellings_csv",
"<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>/tools/", ClubToolsView.as_view(), name="tools"),
@ -85,9 +102,7 @@ urlpatterns = [
),
path("<int:club_id>/poster/", PosterListView.as_view(), name="poster_list"),
path(
"<int:club_id>/poster/create/",
PosterCreateView.as_view(),
name="poster_create",
"<int:club_id>/poster/create/", PosterCreateView.as_view(), name="poster_create"
),
path(
"<int:club_id>/poster/<int:poster_id>/edit/",

View File

@ -1,21 +1,25 @@
# -*- coding:utf-8 -*-
#
# Copyright 2023 © AE UTBM
# ae@utbm.fr / ae.info@utbm.fr
# All contributors are listed in the CONTRIBUTORS file.
# Copyright 2016,2017
# - Skia <skia@libskia.so>
# - Sli <antoine@bartuccio.fr>
#
# This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr.
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
# http://ae.utbm.fr.
#
# You can find the whole source code at https://github.com/ae-utbm/sith3
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License a published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
#
# 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"
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple
# Place - Suite 330, Boston, MA 02111-1307, USA.
#
# PREVIOUSLY LICENSED UNDER THE MIT LICENSE,
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE.old
# OR WITHIN THE LOCAL FILE "LICENSE.old"
#
import csv
@ -171,18 +175,14 @@ class ClubTabsMixin(TabedViewMixin):
class ClubListView(ListView):
"""
List the Clubs
"""
"""List the Clubs."""
model = Club
template_name = "club/club_list.jinja"
class ClubView(ClubTabsMixin, DetailView):
"""
Front page of a Club
"""
"""Front page of a Club."""
model = Club
pk_url_kwarg = "club_id"
@ -190,24 +190,22 @@ class ClubView(ClubTabsMixin, DetailView):
current_tab = "infos"
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():
kwargs["page_revision"] = self.object.page.revisions.last().content
return kwargs
class ClubRevView(ClubView):
"""
Display a specific page revision
"""
"""Display a specific page revision."""
def dispatch(self, request, *args, **kwargs):
obj = self.get_object()
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):
kwargs = super(ClubRevView, self).get_context_data(**kwargs)
kwargs = super().get_context_data(**kwargs)
kwargs["page_revision"] = self.revision.content
return kwargs
@ -220,7 +218,7 @@ class ClubPageEditView(ClubTabsMixin, PageEditViewBase):
self.club = get_object_or_404(Club, pk=kwargs["club_id"])
if not self.club.page:
raise Http404
return super(ClubPageEditView, self).dispatch(request, *args, **kwargs)
return super().dispatch(request, *args, **kwargs)
def get_object(self):
self.page = self.club.page
@ -231,9 +229,7 @@ class ClubPageEditView(ClubTabsMixin, PageEditViewBase):
class ClubPageHistView(ClubTabsMixin, CanViewMixin, DetailView):
"""
Modification hostory of the page
"""
"""Modification hostory of the page."""
model = Club
pk_url_kwarg = "club_id"
@ -242,9 +238,7 @@ class ClubPageHistView(ClubTabsMixin, CanViewMixin, DetailView):
class ClubToolsView(ClubTabsMixin, CanEditMixin, DetailView):
"""
Tools page of a Club
"""
"""Tools page of a Club."""
model = Club
pk_url_kwarg = "club_id"
@ -253,9 +247,7 @@ class ClubToolsView(ClubTabsMixin, CanEditMixin, DetailView):
class ClubMembersView(ClubTabsMixin, CanViewMixin, DetailFormView):
"""
View of a club's members
"""
"""View of a club's members."""
model = Club
pk_url_kwarg = "club_id"
@ -264,22 +256,20 @@ class ClubMembersView(ClubTabsMixin, CanViewMixin, DetailFormView):
current_tab = "members"
def get_form_kwargs(self):
kwargs = super(ClubMembersView, self).get_form_kwargs()
kwargs = super().get_form_kwargs()
kwargs["request_user"] = self.request.user
kwargs["club"] = self.get_object()
kwargs["club_members"] = self.members
return 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
return kwargs
def form_valid(self, form):
"""
Check user rights
"""
resp = super(ClubMembersView, self).form_valid(form)
"""Check user rights."""
resp = super().form_valid(form)
data = form.clean()
users = data.pop("users", [])
@ -294,7 +284,7 @@ class ClubMembersView(ClubTabsMixin, CanViewMixin, DetailFormView):
def dispatch(self, request, *args, **kwargs):
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):
return reverse_lazy(
@ -303,9 +293,7 @@ class ClubMembersView(ClubTabsMixin, CanViewMixin, DetailFormView):
class ClubOldMembersView(ClubTabsMixin, CanViewMixin, DetailView):
"""
Old members of a club
"""
"""Old members of a club."""
model = Club
pk_url_kwarg = "club_id"
@ -314,9 +302,7 @@ class ClubOldMembersView(ClubTabsMixin, CanViewMixin, DetailView):
class ClubSellingView(ClubTabsMixin, CanEditMixin, DetailFormView):
"""
Sellings of a club
"""
"""Sellings of a club."""
model = Club
pk_url_kwarg = "club_id"
@ -328,12 +314,12 @@ class ClubSellingView(ClubTabsMixin, CanEditMixin, DetailFormView):
def dispatch(self, request, *args, **kwargs):
try:
self.asked_page = int(request.GET.get("page", 1))
except ValueError:
raise Http404
return super(ClubSellingView, self).dispatch(request, *args, **kwargs)
except ValueError as e:
raise Http404 from e
return super().dispatch(request, *args, **kwargs)
def get_form_kwargs(self):
kwargs = super(ClubSellingView, self).get_form_kwargs()
kwargs = super().get_form_kwargs()
kwargs["club"] = self.object
return kwargs
@ -341,7 +327,7 @@ class ClubSellingView(ClubTabsMixin, CanEditMixin, DetailFormView):
return self.get(request, *args, **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)
kwargs["result"] = qs[:0]
@ -385,19 +371,17 @@ class ClubSellingView(ClubTabsMixin, CanEditMixin, DetailFormView):
kwargs["paginator"] = Paginator(kwargs["result"], self.paginate_by)
try:
kwargs["paginated_result"] = kwargs["paginator"].page(self.asked_page)
except InvalidPage:
raise Http404
except InvalidPage as e:
raise Http404 from e
return kwargs
class ClubSellingCSVView(ClubSellingView):
"""
Generate sellings in csv for a given period
"""
"""Generate sellings in csv for a given period."""
class StreamWriter:
"""Implements a file-like interface for streaming the CSV"""
"""Implements a file-like interface for streaming the CSV."""
def write(self, value):
"""Write the value by returning it, instead of storing in a buffer."""
@ -413,7 +397,8 @@ class ClubSellingCSVView(ClubSellingView):
row.append(selling.customer.user.get_display_name())
else:
row.append("")
row = row + [
row = [
*row,
selling.label,
selling.quantity,
selling.quantity * selling.unit_price,
@ -424,7 +409,7 @@ class ClubSellingCSVView(ClubSellingView):
row.append(selling.product.purchase_price)
row.append(selling.product.selling_price - selling.product.purchase_price)
else:
row = row + ["", "", ""]
row = [*row, "", "", ""]
return row
def get(self, request, *args, **kwargs):
@ -471,9 +456,7 @@ class ClubSellingCSVView(ClubSellingView):
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
pk_url_kwarg = "club_id"
@ -483,9 +466,7 @@ class ClubEditView(ClubTabsMixin, CanEditMixin, 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
pk_url_kwarg = "club_id"
@ -495,9 +476,7 @@ class ClubEditPropView(ClubTabsMixin, CanEditPropMixin, UpdateView):
class ClubCreateView(CanCreateMixin, CreateView):
"""
Create a club (for the Sith admin)
"""
"""Create a club (for the Sith admin)."""
model = Club
pk_url_kwarg = "club_id"
@ -506,9 +485,7 @@ class ClubCreateView(CanCreateMixin, CreateView):
class MembershipSetOldView(CanEditMixin, DetailView):
"""
Set a membership as beeing old
"""
"""Set a membership as beeing old."""
model = Membership
pk_url_kwarg = "membership_id"
@ -537,9 +514,7 @@ class MembershipSetOldView(CanEditMixin, DetailView):
class MembershipDeleteView(UserIsRootMixin, DeleteView):
"""
Delete a membership (for admins only)
"""
"""Delete a membership (for admins only)."""
model = Membership
pk_url_kwarg = "membership_id"
@ -553,15 +528,13 @@ class ClubStatView(TemplateView):
template_name = "club/stats.jinja"
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()
return kwargs
class ClubMailingView(ClubTabsMixin, CanEditMixin, DetailFormView):
"""
A list of mailing for a given club
"""
"""A list of mailing for a given club."""
model = Club
form_class = MailingForm
@ -570,7 +543,7 @@ class ClubMailingView(ClubTabsMixin, CanEditMixin, DetailFormView):
current_tab = "mailing"
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["user_id"] = self.request.user.id
kwargs["mailings"] = self.mailings
@ -578,10 +551,10 @@ class ClubMailingView(ClubTabsMixin, CanEditMixin, DetailFormView):
def dispatch(self, request, *args, **kwargs):
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):
kwargs = super(ClubMailingView, self).get_context_data(**kwargs)
kwargs = super().get_context_data(**kwargs)
kwargs["club"] = self.get_object()
kwargs["user"] = self.request.user
kwargs["mailings"] = self.mailings
@ -599,9 +572,7 @@ class ClubMailingView(ClubTabsMixin, CanEditMixin, DetailFormView):
return kwargs
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(
club=self.get_object(),
email=cleaned_data["mailing_email"],
@ -616,9 +587,7 @@ class ClubMailingView(ClubTabsMixin, CanEditMixin, DetailFormView):
return None
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 = []
for user in cleaned_data["subscription_users"]:
@ -652,20 +621,16 @@ class ClubMailingView(ClubTabsMixin, CanEditMixin, DetailFormView):
return None
def remove_subscription(self, cleaned_data):
"""
Remove specified users from a mailing list
"""
"""Remove specified users from a mailing list."""
fields = [
cleaned_data[key]
for key in cleaned_data.keys()
if key.startswith("removal_")
val for key, val in cleaned_data.items() if key.startswith("removal_")
]
for field in fields:
for sub in field:
sub.delete()
def form_valid(self, form):
resp = super(ClubMailingView, self).form_valid(form)
resp = super().form_valid(form)
cleaned_data = form.clean()
error = None
@ -697,7 +662,7 @@ class MailingDeleteView(CanEditMixin, DeleteView):
def dispatch(self, request, *args, **kwargs):
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):
if self.redirect_page:
@ -713,9 +678,7 @@ class MailingSubscriptionDeleteView(CanEditMixin, DeleteView):
def dispatch(self, request, *args, **kwargs):
self.club_id = self.get_object().mailing.club.id
return super(MailingSubscriptionDeleteView, self).dispatch(
request, *args, **kwargs
)
return super().dispatch(request, *args, **kwargs)
def get_success_url(self, **kwargs):
return reverse_lazy("club:mailing", kwargs={"club_id": self.club_id})
@ -726,7 +689,7 @@ class MailingAutoGenerationView(View):
self.mailing = get_object_or_404(Mailing, pk=kwargs["mailing_id"])
if not request.user.can_edit(self.mailing):
raise PermissionDenied
return super(MailingAutoGenerationView, self).dispatch(request, *args, **kwargs)
return super().dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
club = self.mailing.club
@ -740,25 +703,25 @@ class MailingAutoGenerationView(View):
class PosterListView(ClubTabsMixin, PosterListBaseView, CanViewMixin):
"""List communication posters"""
"""List communication posters."""
def get_object(self):
return self.club
def get_context_data(self, **kwargs):
kwargs = super(PosterListView, self).get_context_data(**kwargs)
kwargs = super().get_context_data(**kwargs)
kwargs["app"] = "club"
kwargs["club"] = self.club
return kwargs
class PosterCreateView(PosterCreateBaseView, CanCreateMixin):
"""Create communication poster"""
"""Create communication poster."""
pk_url_kwarg = "club_id"
def get_object(self):
obj = super(PosterCreateView, self).get_object()
obj = super().get_object()
if not obj:
return self.club
return obj
@ -768,19 +731,19 @@ class PosterCreateView(PosterCreateBaseView, CanCreateMixin):
class PosterEditView(ClubTabsMixin, PosterEditBaseView, CanEditMixin):
"""Edit communication poster"""
"""Edit communication poster."""
def get_success_url(self):
return reverse_lazy("club:poster_list", kwargs={"club_id": self.club.id})
def get_context_data(self, **kwargs):
kwargs = super(PosterEditView, self).get_context_data(**kwargs)
kwargs = super().get_context_data(**kwargs)
kwargs["app"] = "club"
return kwargs
class PosterDeleteView(PosterDeleteBaseView, ClubTabsMixin, CanEditMixin):
"""Delete communication poster"""
"""Delete communication poster."""
def get_success_url(self):
return reverse_lazy("club:poster_list", kwargs={"club_id": self.club.id})

23
club/widgets/select.py Normal file
View File

@ -0,0 +1,23 @@
from pydantic import TypeAdapter
from club.models import Club
from club.schemas import ClubSchema
from core.views.widgets.select import AutoCompleteSelect, AutoCompleteSelectMultiple
_js = ["bundled/club/components/ajax-select-index.ts"]
class AutoCompleteSelectClub(AutoCompleteSelect):
component_name = "club-ajax-select"
model = Club
adapter = TypeAdapter(list[ClubSchema])
js = _js
class AutoCompleteSelectMultipleClub(AutoCompleteSelectMultiple):
component_name = "club-ajax-select"
model = Club
adapter = TypeAdapter(list[ClubSchema])
js = _js

View File

@ -1,19 +1,14 @@
# -*- coding:utf-8 -*-
#
# Copyright 2023 © AE UTBM
# ae@utbm.fr / ae.info@utbm.fr
# All contributors are listed in the CONTRIBUTORS file.
#
# This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr.
#
# You can find the whole source code 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)
# 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"
#
# PREVIOUSLY LICENSED UNDER THE MIT LICENSE,
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE.old
# OR WITHIN THE LOCAL FILE "LICENSE.old"
#

View File

@ -1,46 +1,34 @@
# -*- coding:utf-8 -*-
#
# Copyright 2023 © AE UTBM
# ae@utbm.fr / ae.info@utbm.fr
# All contributors are listed in the CONTRIBUTORS file.
#
# This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr.
#
# You can find the whole source code 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)
# 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"
#
# PREVIOUSLY LICENSED UNDER THE MIT LICENSE,
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE.old
# OR WITHIN THE LOCAL FILE "LICENSE.old"
#
from ajax_select import make_ajax_form
from django.contrib import admin
from haystack.admin import SearchModelAdmin
from com.models import *
from com.models import News, Poster, Screen, Sith, Weekmail
@admin.register(News)
class NewsAdmin(SearchModelAdmin):
list_display = ("title", "type", "club", "author")
search_fields = ("title", "summary", "content")
form = make_ajax_form(
News,
{
"author": "users",
"moderator": "users",
},
)
autocomplete_fields = ("author", "moderator")
@admin.register(Poster)
class PosterAdmin(SearchModelAdmin):
list_display = ("name", "club", "date_begin", "date_end", "moderator")
form = make_ajax_form(Poster, {"moderator": "users"})
autocomplete_fields = ("moderator",)
@admin.register(Weekmail)

View File

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

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